天天看点

c++ tips 100(有些可能不属于c++)

这是以前(什么时候?)做的一些零碎笔记(应该是在看c++ primer的时候),不一定正确,但可供参考~~

比较好的一个学习方法:

<1>.通过一个一个的实例学习(可以是每一章一个实例,始于一个简单的例子,最后变成一个参考价值较高的完善例子)

<2>.像下面一样记录tips,作为备忘(通过自己的语言描述出来,可以加深理解和映像)

<3>.无它,手熟尔.

1.extern "C":c++编译器能对其进行识别;被其标注的部分将按c的方式进行编译

个人认为,extern "C"被标注的函数将会有两个别名,所以这个函数在c++和c的代码中都能被引用。

2.我们在定义枚举时可以这样做:

typedef enum

{

 enum1=0,

 enum2,

}MY_ENUM;

也即给第一个枚举量赋值,之后的依次累加1

3.注意:c中并没有在编译层次支持函数重载,所以c++中的重载函数只能有一个用extern "C"描述。

4.被申明为内联的函数,不一定在编译期间被内嵌到引用处。inline关键字只是对编译器提出建议。

内联函数的好处:

可以把一个经常出现的操作或代码定义成一个函数,方便维护修改也方便阅读理解。所以内联函数一般要求短小精悍!

与非inline函数不同的是,inline函数必须在调用该函数的每个文本文件中定义。而且必须相同。所以最好的方式是把它定义在头文件中。

5.引用与指针的区别,引用不能为空,不能修改指向~~,优点是能提供操作的直观性,如果用指针的话,很可能让代码失去这种直观性。尤其是在重载操作符的时候。

6.如下申明

void aaa(int[10]);

被c++编译器看成

void aaa(int *);

所以,如下三个声明等价:

void aaa(int[10]);

void aaa(int *);

void aaa(int[]);

7.c的struct只是变量的集合体,不可以隐藏数据,不可以封装操作。c++的struct与class的差别是struct默认为public。

8.枚举类型的优点:不能把数字常量(整数)赋给枚举变量。这样就防止了误操作(比如当你用枚举变量作为参数时,传入该变量的值是限定的);

可以用枚举类型来代替宏定义,如:

enum{

 MAX_LEN=100,

 TYPE1=1,

 TYPE2=2

};

9.结构体的两种定义法:

当结构体定义在头文件或某个模块中而需要被其他模块使用时,最好使用typedef:

typedef struct _STRUCT_A{

 int i;

 int j;

}STRUCT_A,*pSTRUCT_A;

而当结构体只需在本模块使用时,可以直接定义并申明:

struct struct_s{

 int i;

 int j;

}mystruct1,mystruct2,mystructs[100],*pmystruct;

10.为什么要使用volatile关键字:因为编译器会对程序变量进行寄存器优化。比如一个经常被使用的变量可能保存在寄存器中。而volatile修饰的变量很可能在编译器能控制的范

围外被修改,比如它是一个寄存器相关值,被外部中断或始终更新。这时候要告诉cpu该变量不能进行寄存器优化,每次都要去读原值。

   volatile用法与const用法相似。volatile和const修饰的都是他们右边的变量.

11.complex类型与pair类型:

   #include <complex>  |   #include <utility>

   using: complex<double> r1(11,11.22);

  pair<string,string> p1("aa","bb");

   技巧: 可以使用typedef pair<string,string> PP;

12.指针的合理写法:char *ch1,ch2;而不是char* ch1,ch2.

13.为什么要定义宏或常量?

   方便修改;防止误操作(比如:if(MAXLEN=1));意义明了(可以取一个名字)

14.引用必须初始化指向一个对象。一经指定,就不能再指向其他对象。注意一下const引用的特点。引用的内部存放一个对象的地址,它是该对象的一个别名。对于不可寻址的值

,如文字常量,编译器会生成一个临时对象,但是用户无法访问该对象。

看下面引用:

const int ival=1024;

const int *const &pi_ref=&ival;

在c++中,引用类型主要被用作函数的形式参数。

引用必须指向一个对象,如果:char &ch=0;

则编译器把它翻译成:char temp=0;char &ch=&temp;

15.一个重要的错误源:

在c++中,类只是一个智能的数据结构。当一个对象拷贝给另一个对象,而该对象包含一个或多个指针时(指针的地址覆盖了,而不是指针指向的内容发生拷贝)。...

16.可以用一个数组(或部分)来初始化一个vector,比如:

int aa[8]={1,1,1,1,1,1,5,6};

vector<int> v(aa+3,aa+8);

vector有两种用法:内置数组用法和STL用法。

学会STL的设计模式和思维方法不是一件坏事~~

17.成员函数指针  R (T::*)(...)

18.系统为每个程序执行时可用的内存池。这个可用内存池被称为程序的空闲存储区(free store)或堆(heap)。dynamic memory allocation。

19.c++中有bitset操作,#inclued<bitset>非常的好用,用着试试就知道。

20.c++框架内的强制转换是不同于c的,类型转换被称为cast,有如下一些cast:

static_cast:

dynamic_cast:

const_cast:

reintepret_cast:

任何非const类的指针都可以转换成void*型的指针。

21.程序员必须显示地告诉编译器停止执行switch中的语句。break。大多数情况下,故意省略break标签的case语句应提供一条注释。我们的程序不光是要能够编译执行,而且对于

以后负责修改和扩展的程序员来说,也应该是可以理解的。

省略break也是必要的,当我们case一个集合时:

switch(i){

case 1:case 2:case 3:

 do 123;

 break;

case 4:case 5:

 do 45;

 break;

case 6:

 break;

default:

 do default;

}

22.可变参数,如:

printf(const char*,...);

print(...);

关于可变参数的解析另行参考~~

23.函数指针的最大用途是用于回调设计。这通常涉及到“函数注册”。

对一个函数如:void FF(void){retrun;}

定义一个函数指针void (*PFF)(void);

则PFF=FF;PFF=&FF都是可取的。

函数指针数组~~

24.不同程序文本文件的名字空间定义是可以积累起来的。名字空间支持嵌套。

c++中可以用未命名的名字空间声明一个局部于某一文件的实体。如:

namespace{

 void func(void){};

}

如果在c文件中,解决办法是为函数定义加上前缀static。

25.标准c++库中的所有组件都是在一个被称为std的名字空间中声明和定义的。在标准头文件中声明的函数、对象和类模板都被声明在名字空间std中。

26.关于函数模版。函数模版提供了一种用来生成各种类型函数实例的算法。对函数接口中的全部或部分类型进行参数化。同样的,类模版也与之类似。

27.c++的容器包括:sequence container和associative container;

前者:list和vector,deque

后者:map,set,multimap,multiset

容器支持比较操作符。

28.为了提高效率,像vector并不是每次插入元素时都要增加自己的长度。它需要增加自己的长度是,它总是预先多分配一些。所以它有两个属性:一是capacity而是size。实际表

明是按指数增长的。

遍历顺序容器时:iterator可以进行指针的任何操作,因为顺序容器支持随机访问内存是连续的。比如:iterator++;iterator--;iterator+N

而对list等内存不连续的容器:iterator不能进行iterator+/-N的运算,但可以进行++/--操作(应该是进行了操作符重载)

29.效能问题:当数据元素的长度比较小时vector的效能优势明显(insert),反之list更有优势。vector的增长机制是: 每次分配需要大小的二倍,然后把旧数据拷贝到新分配

的数据区。list的开销主要是内存分配,每插入一个,哪怕数据元素很小,也要进行数据分配。如果要提高vector的效能,当数据类型的长度比较大时,应该通过指针间接存储该

复杂的数据类型(这显然是通常的也是极为有效的一个做法)。

30.迭代器:a lot of thing!!

31.父类与子类是可以相互转化的,但不建议从父类强制转换为子类,以免产生不可知的操作。

32.通过编写一个简单的c++程序编译成exe后用反汇编软件反汇编,能够分析出c++的内部实现以及c++编译器的一些命名法则。比如:"cout<<"在编译器内部的名字是

“ostream::operator<<”

33.try{}模块内定义的变量由于局部性不能在catch块中引用。好的编程模式是try{}catch(A){}catch(B){}...

如果要捕捉所有的异常,则catch(...){}即可(这有点类似可变参数)

34.理解c++的异常抛出机制。可以把异常抛出机制理解为动态的函数调用。区别在于:函数调用在编译时确定,但是抛出则完全未知。如果检测到一个错误,则调用throw语句,该

语句复制一份异常对象,然后寻找对应的处理函数(catch语句),一层层展开堆栈找到对应的catch语句,没有则调用缺省的terminate函数。catch(Exp exp)相当于带传值参数的

函数,如果写成catch(Exp &exp)则相当于传址。实际上,后者是相当推荐使用的,因为它可以防止大的异常对象拷贝开销,同时也方便修改exp对象进行二次抛出。假如不采用引

用方式,则在如下代码catch(Exp exp){exp.value=1;throw;}的二次抛出中,抛出对象实际上并没有改变。terminate默认的调用了abort()函数中指程序。

35.异常规范:假如我写一个库,里面有很多函数,那么库的使用者如何知道哪些函数会抛出哪些异常呢?异常规范提供了一种解决方案,它能够随着函数申明列出函数可能抛出的

异常。形如:void pop(int &value) throw(popOnEmpty);这也就限定了该函数在运行期只能抛出popOnEmpty函数,否则将调用unexpected()->terminate()强行中止程序。但是有

几点需要澄清:函数内部抛出不符合异常规范的异常但内部把它解决了,不算违例;或者有那么一条抛出违例异常的语句但是永远不会执行,编译器不会报错;一个函数声明附带

了异常规范,则所有声明都必须附带。

36.异常与程序设计:虽然c++支持异常,但是并不是说一定要使用异常,程序还可以使用多种排错方式,比如:返回错误码;局部处理(使用if语句)等等。一切实事求是。

37. 关于assert宏:

#if defined NDEBUG

#define assert(condition) ((void) 0)

#else

#define assert(condition) _assert((condition), #condition, __FILE__, __LINE__)

#endif

值得学习的地方:_assert本身是宏,再在外面包装一层宏使之因与编译选项不同而定义不同,这是很多开关量的惯用方式。

38.assert永远出现在调试版本,不会出现在发行版本。所以要提高警惕了。在调试版本中某些错误可以简单的用assert来诊断,因为它只给开发者看;但是在发行版本,要么这些

错误不被允许,要么你建立代码能修正错误,assert诊断出来的错误你必须在发行前解决掉。

39.通常,编译器用EBP来指示当前活动的栈帧,如果main()->add();则堆栈先是main的栈帧后是add的栈帧,EBP指向add栈帧的开始处,ESP指向栈顶,add函数中的局部变量被解析

成相对与EBP的固定偏移,由于堆栈向下生长,所以多表示为EBP+N的形式(很有意思,当你压栈时,ESP实际上是减小的)。编译器编译一个函数时,会在它的开头添加一些代码来

为其创建并初始化栈桢,这些代码被称为序言(prologue);同样,它也会在函数的结尾处放上代码来清除栈桢,这些代码叫做尾声(epilogue)。

一般情况下,序言是这样的:

Push EBP ; 把原来的栈桢指针保存到栈上

Mov EBP, ESP ; 激活新的栈桢

Sub ESP, 10 ; 减去一个数字,让ESP指向栈桢的末尾:10是指函数所要用到的所有局部变量和临时对象大小和。编译时,编译器知道函数的所有局部对象的类型和“体积”。

尾声所做的正好和序言相反,它必须把当前栈桢从栈上清除掉:

Mov ESP, EBP

Pop EBP ; 激活主调函数的栈桢

Ret ; 返回主调函数(CALL指令执行时自动把其后指令的地址压入堆栈,RET恰好相反)

40.类的成员只能在构造函数内初始化,如果没有显式的初始化,会有一个默认的初始方法。(静态成员在构造前初始化,因为相当于全局变量)

而且在构造函数内部诸如a=1;b=2;的“初始化”过程并不是真正的初始化,而是赋值,真正的初始化是使用参数化列表的方式。也即在执行构造函数里面的语句前已经初始化了类

对象。

41.VC6.0的反编译功能真是棒极了!如果你想研究下c++编译器原理,比如其如何支持类?编个小程序反编译一下就豁然开朗了。c++相对于c表现的很庞大,很智能,但是我们必须

非常清楚的是,c++的这些新特性完全是依赖于编译器的强大,编译到机器码级(汇编级),c与c++没什么两样。为什么说c是半机器语言呢,因为它的语法概念相对于汇编来说并

没有太多扩展,而c++就不同,他有很多抽象的概念。最简单的,类的概念,其实你这样定义一个类class A{int a,int b};并生成一个实例A aa;对编译器而言,{A aa}={int

aa.a,int aa.b}。从某种意义来讲,c++只是简化了c(增加了语义,降低了程序编写难度),然后把复杂性交给了编译器。

42.再定义一个类时,通常把public区放前面,private区放后面

43.请一定要理解编译过程,严格区分编译与连接各自做了些什么,这样帮助自己在写程序时不犯错且在排错时能准确分析原因。比如,申明影响编译,定义会影响连接

44.一个类被编译后是这样的,就是一推函数体,这堆函数加了一个该对象的指针this。比如:

有代码:

class A{

public:

int a;

void add(){a++;}

}

那么编译后将会有类似于add(A *this){(*((int *)((char *)this-4)))++}的函数出现,由于类里面个成员变量相对于类指针所指地址的偏移是固定的,所以对成员变量的操作也

即对某个偏移上按某种类型进行操作。至于静态成员变量,压根就是全局变量,只是名字上加了前缀而已。

可以这么认为:类的实体是一对函数+静态变量;而对象的实体是一个结构体。也就是一堆结构体共用一组函数。

深切理解这句话:每个类对象都讲维护一份类数据成员的拷贝,但是成员函数只有一份拷贝。

45.关于const成员函数的局限性:如果类含有指针成员,则const成员函数能修改该指针所指的对象(编译器不会报错)

另:const成员函数能被非const同名函数重载

被申明为const的对象只能调用被const修饰的成员函数

46.mutable(易变的)修饰符。于const相对,用来抵制const的“霸道与迂腐”。

const,volatile修饰的成员函数实际上修饰的是this指针。因为修饰的是this,所以实际上修饰了所有的数据成员。所以在const函数中返回成员时返回的是const this->memberX

47.如果每个成员函数都返回(*this)的引用,那么对某个对象的操作可以连成串,just like this:

test.int().move(1,2).show();//这样对三个函数的调用在一条语句里面就完成了。

48.作为特例,有序型的const静态数据成员可以在类体中用一常量进行初始化。

如:

static const int namesize=16;

static const char name[namesize];

49.函数指针的一种应用模式:有很多操作An,它们有类似的接口,现在还有一个函数B需要调用这些操作,那么可以把那些操作函数An以指针的方式传给B使用。

当然,函数指针有很多用处,在很多有效的设计模式中使用它能有效的增加效率也降低结构的复杂性。

还有一个问题,为什么成员函数指针不能与定义在类外部的一个同结构的函数指针对等,其实这个最简单了,因为成员函数含有一个this指针。成员函数首先必须被绑定到一个对

象或指针上,才能得到被调用对象的指针,才能调用指针所指的成员函数上。虽然普通函数指针和成员函数指针都被称作指针,但它们是不同的事物。

50.关于类成员指针与类成员函数指针还要啰嗦几句。

class A(){public:int a; void f(){}}

则成员指针申明:int A::pa=&A::a;

成员函数指针申明:void (A::*pf)()=&A::f;

今有A ca;则可如下使用指针:

int i=ca.*pa;(ca.*pf)();其编译器内部过程请参阅有关书籍~~

解释一下:void (A::*pf)()这个很容易理解,告诉编译器pf函数要带一个A *this的指针参数,而(ca.*pf)中,ca.的意思告诉编译往函数传入&ca。

51.静态类成员的指针与非类成员的指针相同,关于这一点如果从编译器的角度来理解将很简单。

52.c++中的union与c中的union的差别。union通常用在类似于

{

 int ID;

 union value{

  ....

 }

}的结构中。有一种特殊的union叫做匿名union。匿名union去掉了一层成员访问操作符,所以匿名union不能有私有或保护成员,也不能定义成员函数。在全局域申明的命名union

必须申明在未命名的名字空间中(或者被申明为static)

53.bitset模板,c++建议在使用位操作的应用中使用bitset类模板,位操作通常可以和枚举,常量定义联系在一块,因为没有人愿意去记住每一位是什么意思。

54.关于局部类与嵌套类这些比较偏的东西,平时很少用到,所以无需仔细了解,但要认识到c++编译器支持这些性质。

55.建议使用缺省参数的构造函数,因为可以减少构造函数的个数。千万别错误的认为如果没有缺省的构造函数,编译器会自动添加。没有就是没有。

还有一个容易想当然的是,构造函数必须是共有的。实际上,私有的构造函数有它特定的用途。

56.一个小问题,我们永远不要这样去定义一个对象Class1 class1();去掉后面的括号就行了,否则编译器会认为这是一个函数申明。呵呵,你以前犯过这样的低级错误吗?

还有就是,当我们定义了构造函数,我们在定义对象实例时就必须调用其构造函数,除非有个构造函数带缺省参数(这样的话,编译器会自动添加缺省构造函数)

当要一次声明大量对象的时候,我们当然希望能有个缺省的构造函数,这页是缺省最好提供一个却性构造函数的原因。

57.反编译显示,vc++的编译器把引用类型转化为指针,但是你如果用sizeof去取得其大小,则不是指针的大小,而是指向的对象的大小。

58.拷贝构造函数。单独拿出来讲是因为它很c++。它的参数是指向该类对象的引用(通常被申明为const)。

注意:即使你没有申明拷贝构造函数,对一些简单的类还是可以用一个对象去初始化另一个对象的,编译器将执行缺省的按成员初始化。

59.析构函数:当你动态分配了内存,或是使用了操作系统的资源(比如互斥锁,比如Handle),析构函数就必须存在。但析构函数的功能不仅在于释放资源。要充分利用它的最后

必然执行这个特点。编译器记录一个类(在其生存期内)在哪个地方最后被调用,然后插入它的析构函数。

60.c++语言内部保证,不会用delete操作符删除不指向任何对象的指针。所以如下的判断是没必要的:if(pt) delete pt;这个判断编译器做了。

61.c++的强大在于指针,c++的灾难往往也是指针。所谓成也萧何败也萧何,因此市面上有很多商用的智能指针,可以关注一下它们的实现技术。不过c++本身提供了一个小巧的智

能指针auto_ptr,auto_ptr所做的事情,就是动态分配对象以及当对象不再需要时自动执行清理。鉴于这样一个事实,c++出现异常时,堆栈展开的过程当中会调用栈内对象的析构

函数。但是不会去清理new出来的对象。比如,a=new A();...delete a;很有可能在这两句间发生异常从而delete从来没有执行,这是一个典型的内存泄漏case。所以如果把指针封

装到一个类中,然后在它的析构函数内清理所指对象,那问题就可以很好的解决,new之后都不需要写delete。所以平时要多动动脑子~~

62.什么时候需要显式调用析构函数?因为delete一个对象时,不但调用了一个对象,也删除了堆中这块内存。有时候我们不希望如此,特别是多个对象共享某块内存的时候。那么

我们可以不delete,只是调用析构函数。

63.对象数组的初始化:

Account pols[]={

Account(...),

Account(...),

Account(...)

}

64.vc++的反编译结果表明,程序编译后会生成一张函数表:

jmp D::D(32 bits address)

jmp ...

这些跳到指定函数入口的指令紧密排列跟中断向量一样,所以我们call一个函数时,并不是直接跳到函数入口处,而是先通过该函数的编号(编译器编译时会进行)为索引跳到“

函数表”的对应jmp指令,通过jmp指令跳到目标函数。

65.vc++反编译表明,类成员函数的this指针是通过ecx寄存器,而不是使用参数压栈的方式,实际上函数在入口位置把ecx值作为栈内第一个临时变量,存放在ebp-4的位置。

66.成员初始化表对性能的影响。不要错误的认为,使用成员初始化表总是会提高构造函数的性能。这应该根据具体情况而定。先来了解下编译器到底做了些什么。如果一个函数含

有类对象成员(而不只是c++的简单对象如int等),那么在构造函数体执行前,编译器会生成隐式初始化表,会调用类成员的缺省构造函数。所以如果在构造体内对其赋值,则调

用的是拷贝构造函数,开销很大。但是对于简单变量则没必要使用成员初始化列表。

总之,对于类对象,在初始化和赋值之间的区别是很大的。其它倒无所谓,但使用初始化列表好像更清晰,特别的,const和引用成员必须使用初始化列表初始化。

一个一般性的规则是:在成员初始化列表中初始化所有的成员对象。

67.初始化顺序:与在列表中的顺序无关,与成员申明顺序相关。我们如果要屏蔽掉按成员初始化,在声明一个拷贝构造函数但是不对其定义。

68.重载赋值操作符时,应该注意一点:最好不要自己对自己拷贝,所以加一句if(this!=&copy)

69.c++语言不能有效的返回一个类对象,这被视为c++语言的一个重大缺陷。清理解这句话,并且想想除了从编译器的角度,有没有较好的解决办法。

70.操作符重载函数。很多情况下都要返回一个引用(其实很多成员函数也可以),目的是为了连续操作。比如cout<<a<<b<<c<<endl;为什么<<操作符能连续调用?因为前一个<<返

回的正是cout<<;又比如:(c=(a+=b)).showmenber();

只有在c++预定义操作符集中的操作符才能被重载。而且每个操作符的意义都应与预定义相关。

比较特殊的几个操作符是->和()。

71.根据c++编程规范,尽量使错误发生在编译时,尽量减少运行时错误的可能性。比如:充分利用c++的语言特性。如果一个变量不应改变,那么最好就是把它申明为const使编译

器认识到这一点,从而如果出现误操作或疏忽(在一个比较大的程序中这是难免的),编译器就能把它揪出来。所以,好的编码习惯很重要。

72.c++如何解释->操作符,这是一个递归解释过程,对语言元素A->,c++先查找A的语义,如果A是一个对象或引用,在调用它的成员操作符->取得返回对象,如果返回对象依然是

一个对象或引用,则递归这个过程。直到得到一个指针类型。然后->就变成了取成员的内定义语义。

73.模板定义的嵌套。之前很疑惑iterator是如何实现的。网上的人说是使用模板与操作符重载,操作符重载自不必说,但模板的使用上还是有点技巧的。

第一,就是定义在一个类内部申明,用public关键字抛出,比如:

class A{

public:

  typedef struct _B{} B;

}  然后可以用A::B来定义某个对象;

第二,模板嵌套,比如:

template <class T> class A{};

template <class T> class B{};

定义或申明时,使用B<A<int>>。iterator就是这么做的。

74.try...catch的简单使用,try块中可以抛出一个字符串,在catch块中打印该字符串,并优雅退出(这是个调试的好方法)

75.c++的容器与泛型算法的分离。还有函数对象。

考虑find算法,我们要解决三个问题:

a.需要某种方式来遍历;使用iterator

b.需要对元素进行比较;采用两个版本,一是使用元素底层的=操作符,二是使用函数指针来让用户自定义。

c.需要一个公共类型来表示元素在容器中的位置;

76.泛型算法接受普通指针或ieterator。a lot of things!

77.explicit,和构造函数一起使用.  

  explicit constructor指明构造函数只能显示使用,目的是为了防止不必要的隐式转化.  

  举个例子:  

  class A{  

  public:  

        A(int){};  

  };     int Test(const   A&){} 

  Test(2);   //正确  

  如果构造函数被声明为explicit,将会失败。

78.野指针,野指针是指向垃圾内存的指针!!不是NULL指针。比如,你在栈类定义了一个临时对象,把这个对象的地址传给了域外,则函数退出时那个指针就成了野指针。

79.函数对象。不错的好东东。没有数据成员,只有成员函数。所以不需要实例化。多用在泛型算法当中~~

不仅如此,而且它可以被看成是一个“万能操作符”。

80.预定义函数对象:#include<functional>

算术,关系,逻辑

81.刚刚看到Bjarne Stroustrup(c++之父)的论断,尽量少用宏,“如果看到了宏,说明程序语言、程序代码抑或是这个程序员存在问题”。

用const,enum,inline和模板来替换原来使用宏的地方。本人非常认同!

但是,宏仍然是几个重要任务的惟一解决方案,比如#include保护符(guard)),条件编译中的#ifdef和#if defined,以及assert的实现

81.何时不用泛型算法。不允许在关联容器中应用重新排序的泛型算法。因为关联容器为了提高速度,关联容器内部有个结构不能乱的。第二,鉴于list存储的非连续性,很多算法

list都以成员函数的方式实现了,故不应使用泛型算法。

82.什么时候需要allocator,默认的c++alloctor也即new和delete,new对分配小内存效率和内存利用率都不是很高。为提高性能,这时需要自己定义allocator,比如

map<int,int>的时候,然后在自定义的allocator中使用c的malloc和free函数来分配。

83.我们的设计原则:程序员不能显示的管理很多的东西。特别是一个合作项目,那么模块的划分,以及类的architecture,接口的定义等问题都是关键。什么叫程序员不应该显示

的管理太多的东西?其实质就是封装,而且是巧妙的封装。让整个程序结构更加“智能”。充分利用好c++编译器所支持的c++特性,尽量使程序更易于扩展,模块更易于使用。

比如,面向对象程序设计的典型特点就是:它把类型解析的负担从程序员转移到了编译器上。

84.再次强调一下多态与动态绑定。第一次感到多态的威力,是看BCM的代码的时候,后来分析MFC的时候也深有体会!再后来,自己在做一个项目的时候应用了多态技术,使得我写

的上层代码可扩展性非常强(实现了功能的类插件化)!

继承层次结构的好处是:我们可以针对抽象基类编程,而不是针对组成继承层次的个别类型(简化编程也方便扩展与升级)。

所谓c++的多态性,主要是指基类指针可以指向任意派生类的能力。(所以多态离不开指针和引用~~)

由程序员来管理的“多态”不是多态。c++编译器作为最复杂的编译器,那就不要浪费!

85.在派生一个类前,必须在其前向声明中定义了基类。光声明是不行的(因为类不是内定义的,编译器不知其大小,这跟在类中以本类做成员一样)。

86.面向对象设计的一个主要形式是抽象基类的定义以及它的公有派生。注意:基于对象与面向对象之间还有个鸿沟。

很多初学者(没有实际开发经验的),认为使用类就是面向对象。大错特错!

学软件工程的时候,说面向对象的特点:封装,继承,多态,消息;没错,四者缺一不可。其中封装是最基本的(也是很多初学者所理解的面向对象)。而后面三个特征没有实际

的开发经验观看书不写码是不可能真正一会的。

87.在基类申明为虚的函数,其子类不需要再声明为虚函数了,其子孙们自然继承了虚拟的特性!

88.基类与子类名字域的独立性:在基类CA中定义了int i,在CA子类CA1中定义int i;二者并不冲突。它们都限定在自己的类域中~~

同样的成员函数名也是如此。不要指望基类和派生类的成员函数构成一个重载函数集。重载

我们可以这样来调用基类的函数:CA1 ca1; ca1.CA::func();

89.假如有基类CA,从CA派生CA1,二者都定义了一个函数void test(){}(不是虚函数)。则:

CA *pca=new CA1();pca->test();

这种情况下,pca调用的是CA的test函数。这个如何理解?

让我们来看编译器怎么做:首先,new CA1()说明在内存中生成的是CA1对象(其VFT只指向的自然是CA1的),然后CA *pca=,编译器它认定pca是CA类的指针,它会忘记其实这是一

个CA1类的指针,所以你调用pca->test();编译器会把其转为CA::test(CA* pca);而如果调用虚函数呢?回去先查pca指向的对象的第一个元素(那个指向虚函数表的指针),根据

该指针去查调用的函数,查到的自然会是CA1这个类的。

所以c++的发明者很聪明~~

90.关于.操作符合。.操作符就是让编译器把.之前的对象的地址以指针的方式传入到函数(如果后面是函数)。

91.参考88. 我们如何让基类的成员函数与子类的成员函数构成一个重载集?最好的方式是使用using申明,比如:

class CA{void test(int i);}

class CA1:public CA{using CA::test;void test(string s);}

using申明的函数不能带参数。所以using把基类的重载集整个添加到了子类中~~

92.当一个基类对象定义了一个protected成员变量。而在该类对象的派生对象的某个成员函数中传入了该基类对象的引用,那么派生对象不能操作该成员变量。但如果传入的是派

生对象的引用,则可以(想想拷贝构造函数)

93.友元,友元,友元。蛮有用的,呵呵。建立暧昧关系的钥匙。

94.构造函数的调用顺序:

 基类的构造函数,如果有多个基类,按其在派生表中出现的顺序,不是成员初始化表中的顺序。

 如果有成员类对象,按它们申明的顺序进行构造。

 然后才是派生类对象的构造(就是那个构造函数的执行体)。

这个与数据在对象体中的内存排列顺序相关,先从内存小的开始构造,这符合我们的习惯。可以这样认为,基类子对象是派生类对象特殊的数据成员。

95.作为一般规则,不应该在派生类构造函数体中直接向一个基类对象的成员赋值,而应把值传递给基类对象的构造函数(在初始化参数列表中)。这是一种好的习惯,否则,基类

与子类变成紧耦合的,不利于代码修改,维护。

96.虚函数的越权。有派生类CA1的实例ca1,ca1想调用其基类CA的虚函数怎么办?很好办,加上域限制符CA::就可以了。

97.声明为纯虚的成员函数可以有定义。那么它的定义有意义吗。有的。可以在它的子类中以CLASSNAME::的静态方式进行调用(从某种程度上抑制了虚拟机制)。

而声明其为纯虚函数的目的是使该类变为抽象类,不能实例化。

98.涉及到多态的话,别忘了把析构函数声明为虚拟函数(嘿嘿,否则你会死得很惨的~~)

析构的顺序与构造的顺序相反。可以作一小程序,以打印字符串的方式(或跟踪调试)试验之。

编译器会自动往派生类的析构函数添加其基类和成员的析构函数。实际上你不写析构函数,默认的析构过程是会进行的。关于这个地方,应该进一步对编译器进行了解~~

了解清了写专题啊,呵呵(有小题大作的嫌疑~~)

99.对上层c++的概念一定要从汇编级来认识,这样你才知道编译器做了什么,c++为什么可以这样设计。你也不用去记很多语法细节,因为你了解内部机制。

100.反汇编结果表明(vc6.0)。对象调用虚拟函数时并没有使用vftable,只有在对象指针调用虚拟函数时才采用所谓的动态绑定。