More Effective C++读书小记

1、仔细区别pointer和references

不论pointer或是references都使你间接参考其它对象。

没有所谓的null reference。一个reference必须总代表某个对象。

如果你有一个变量,其目的是用来指向(代表)另一个对象,但是也有可能它不指向(代表)任何对象,那么你应该使用pointer,因为你可以将指针设为null。换个角度,如果这个变量总是代表一个对象,也就是说你的设计并不允许这个变量为null,那么你应该使用reference。

由于reference一定得代表某个对象,C++因此要求reference一定要有初值;但是pointer没有这样的限制。

另一个重要差异是:pointer可以被重新赋值,指向另一个对象,reference却总是指向(代表)它最初获得的那个对象(引用关系不能修改,但对象值可以修改)。

结论:

当你知道你需要指向某个东西,而且绝对不会改变指向其他东西,或是当你实现一个操作符而其语法需求无法由pointer达成,你就应该选择references。其他时候,请采用pointer。

2、最好使用C++转型操作符

旧式C转型的缺点:(1)几乎允许你将任何类型转换为其他任何类型,这是十分拙劣的,如果每次转型都能更精确地指明意图会更好;(2)第二个问题是它们难以辨识。

C++导入了4个新的转型操作符(cast operators):static_cast、const_cast、dynamic_cast和reinterpret_cast。

static_cast转型操作符使用很广,但是它不能移除表达式的常量性(constness),因为const_cast专司此职。

const_cast用来改变表达式中的常量性和变易性,将常量性去除。

dynamic_cast用来执行继承体系中“安全的向下转型或跨系转型动作”。也就是说你可以利用dynamic_cast,将“指向base class objects的pointer或references”转型为“指向derived(或sibling base) class objects的pointer或references”,并得知转型是否成功。如果失败,会以一个null指针(转型对象是指针时)或一个exception(当转型对象时references)表现出来。(dynamic_cast另一个作用是找出被某对象占用的内存的起始点)

reinterpret_cast与编译平台息息相关,不具移植性。其用途是转换“函数指针”类型。

总结:

C旧式转型语法仍然可以继续使用,但是不具备C++转型操作符所提供的严谨意义与易辨识度。使用新式转型法,比较容易被解析(不论是对人类还是对工具而言),编译器也因此得以诊断转型错误(那是旧式转型法侦测不到的)。

3、绝对不要以多态方式(polymorphically)处理数组

引用和指针一样,也会触发虚函数的多态效果。

通过base class指针删除一个由derived classes objects构成的数组,其结果未定义。

总结:

(1)多态(polymorphlism)和指针算术不能混用,而数组对象几乎总是会涉及指针的算术运算,所以数组和多态不要混用。

(2)如果你避免让一个具体类继承自另一个具体类,就不大能够犯“以多态方式来处理数组”的错误。

4、非必要不提供default constructor

Constructor用来对对象初始化,所以default constructor的意思是在没有任何外来信息的情况下将对象初始化。

在自定义了constructor后(带额外参数),会隐藏default constructor。可能会出现一些问题:(1)无法直接声明对象数组,因为没法为数组中的对象指定constructor自变量(参数);2、它们将不再适用于许多template-based container classes,对template而言,被实例化的目标类型必须要一个default constructors;(3)如果一个virtual base classes缺乏default constructor,与之合作将是一种刑罚发,因为virtual
base class constructor的自变量必须由欲产生的对象的派生层次最深的class提供。

解决无default constructor的方法:为所有classes都提供default constructors——给自定义的constructor参数都设置一个默认值。(这会带来一些问题,如允许产生无意义的对象等;添加无意义的default constructor,也会影响classes的效率——需要测试是否真的被初始化,这需要代价。)

总结:

如果default constructor可以确保对象的所有字段都被正确地初始化,那么可以提供default constructor;但是如果不能保证,那么最好避免让default constructor出现。

5、对定制的“类型转换函数”保持警觉

所谓隐式类型转换操作符,是一个拥有奇怪名称的member function:关键词operator之后加上一个类型名称。你不能为此函数指定返回值类型,因为其返回值类型基本已经表现于函数名称之上。如“operator double()”能将对象隐式转换为double。

最好不要提供任何类型转换函数,原因是:在你从未打算也未预期的情况下,此类函数可能会被调用,而其结果可能是不正确、不直观的程序行为,很难调试。(非预期的函数被调用)

解决办法就是以功能对等的另一个函数取代类型转换操作符:也就是用一个member function返回你要转换的结果。

通过单自变量constructors完成的隐式转换,较难消除(隐式调用构造函数生成临时对象);双自变量constructor是没法成为类型转换函数的。

解决单自变量constructors隐式转换的方法有两个:(1)简易法,支持explicit关键字时使用,只要将constructors声明为explicit,编译器便不能因隐式类型转换的需要而调用它们,不过显示类型转换仍然是允许的;(2)关于隐式转换有一条规则:没有任何一个转换程序可以内含一个以上的“用户定制转换行为”,利用这条规则可以让你希望拥有的“对象构造行为”合法化,而让你不希望允许的隐式构造非法化。(也就是设计好参数类型,是的希望转换的时候参数只用转型一次,而其他时候一般需要转换多次才行,如采用proxy
classes替代一般参数,从而避免)

总结:

允许编译器执行隐式类型转换,害处将多过好处。所以不要提供转换函数,除非你确定需要它们。

6、区别increment/decrement操作符的前置和后置形式

前置和后置式为了区分,规定后置式有一个int自变量,并且在它被调用时,编译器默默为该int指定一个0值。由于该int并不需要,所以在实现时,不用写它的变量名。(如果写了,编译器反而会报警未使用参数)

操作符的前置和后置式返回不同的类型,前置式返回一个reference,后置式返回一个const对象。

前置式:UPInt& UPInt::operator++(){*this+=1;return *this;}

后置式:const UPInt UPInt::operator++(int){UPInt oldValue=*this;++(*this);return oldValue;}

后置式之所以返回const类型,有两个原因:(1)如果不是const,和內建类型的行为不一致;(2)是为了防止一些违法操作,比如i++++,i值实际上只是增加了一次,并不是预期的两次。

单以效率因素而言,前置式比后置式效率高,因为后置式设计局部变量的构造和析构。(对内置类型而言,两者效率是一样的)

后置式increment和decrement操作符的实现应以其前置式兄弟为基础。(行为要一致)

总结:

掌握increment和decrement操作符的前置和后置式是很容易的。一旦你知道它们应该返回什么类型,以及后置式操作符应以前置操作符为实现基础,就几乎没什么更高阶的知识需要学习的。

7、千万不要重载&&,||和,操作符

“骤死式”评估判断:一旦该表达式的真假值确定,即使该表达式中还有部分尚未验证,整个评估工作仍告结束。

C++允许你为“用户定制类型”量身定做&&和||操作符,做法是对operator&& 和operator|| 两函数进行重载工作。

重载&&和||操作符后,“函数调用”语义会取代“骤死式”语义,也就是说重载后,没法保证函数调用中各参数的评估顺序。

如果你将&&和||重载,就没有办法提供程序员预期(甚至依赖)的某种行为模式,所以请不要重载&&和||。

逗号(,)也是一种操作符,情况类似。表达式内如果含有逗号,那么逗号左侧会先被评估,然后右侧再被评估,最后整个逗号表达式的结果以逗号右侧的值为代表。

如果你重载逗号操作符,同样的问题,你绝对无法保证左侧表达式一定比右侧表达式更早被评估,无论实现方式是member function还是non-member function,因为你无法控制一个函数的自变量评估顺序。

并不是所有操作符都可以被重载,你不能重载以下操作符:.     .*     ::     ?:     new     delete     sizeof     typeid     static_cast     dynamic_cast     const_cast     reinterpret_cast

operator new和operator delete也是操作符,与new和delete操作符不同。

总结:

如果你没有什么好的理由将某个操作符重载,就不要去做。面对&&,||和, 实在难有什么好的理由,因为不管你怎样努力,就是无法令其行为像它们应有的行为一样。

8、了解各种不同意义的new和delete

首先要区分new operator和operator new的区别:new一个对象是使用new operator,它由语言內建,就像sizeof一样,不能改变意义,动作分为两方面:(1)它分配足够的内存,用来存放某类型的对象;(2)它调用一个constructor,为刚才分配的内存中的额那个对象设定初值。new operator调用某个函数,执行必要的内存分配动作,你可以重写或重载那个函数,改变其行为,这个函数的名称叫做operator new。

operator new可以直接调用:void *rawMemory=operator new(sizeof(string)).

operator new的唯一任务就是分配内存,和malloc一样。取得operator new返回的额内存并将之转化为一个对象,是new operator的责任。

placement new是特殊版本的operator new,其定义可以是:void * operator new(size_t t,void *location);如果想让此定义的operator new构造对象,使用时可用new (buffer) Object(size).

结论就是:如果你希望将对象产生于heap,请使用new operator,它不但分配内存而且为该对象调用一个constructor;如果你只是打算分配内存,请调用operator new,那就没有任何constructor被调用;如果你打算在heap objects产生时自己决定内存分配方式,请写一个自己的operator new,并使用new operator,它将会自动调用你写的operator new;如果你打算在已分配(并拥有指针)的内存中构造对象,请使用placement
new。

delete operator对应两个动作:(1)析构指针所指对象;(2)释放被该对象占用的内存。

内存释放动作是由operator delete执行的。通常声明如下:void operator delete(void *memoryToBeDeallocate).

如果你只打算处理原始的、未设初值的内存,应该完全回避new operator和delete operator,改调用operator new和operator delete.(相当于调用malloc和free)

对数组来说,new operator分配内存时会调用operator new[]函数,释放时会对应调用operator delete[].

总结:

new operator和delete operator都是内建操作符,无法为你所控制,但是他们所调用的内存分配/释放函数则不然,当你想要定制new operator或delete operator的行为,记住,你其实无法真正办到。你只可以修改它们完成认为的方式。

9、利用destructors避免泄漏内存

使用longjmp在C++中有一个严重的缺陷:当它调整栈时,无法调用局部(local)对象的destructors。正是这个缺陷,是C++要支持exception的最初的主要原因。

局部对象总是会在其作用域结束时被析构。不论这个作用域如何结束(即使发生异常也一样),唯一的例外是使用longjmp。利用这个特点,可以利用destructor来释放heap资源,防止因异常导致的资源泄漏。(智能指针的职责就是这样)

隐藏在auto_ptr背后的观念——以一个对象存放“必须自动释放的资源”,并依赖该对象的destructor释放。(注意auto_ptr并不适合数组对象)

简单的auto_ptr模型如下:

template<class T>

class auto_ptr{

public:

auto_ptr(T* p=0):ptr(p){}     //存储对象

~auto_ptr(){delete ptr;}     //释放对象

private:

T* ptr;

};

总结:

把资源封装在对象里,通常便可以exceptions出现时避免泄漏资源。

10、在constructor内阻止内存泄漏

delete null指针是安全的额,因此不必在删除指针之前先检查它们是否真正指向某些东西。

C++只会析构已构造完成的对象。对象只有在其constructor执行完毕才算是完全构造妥当。即使用智能指针也一样。

由于C++不自动清理那些“构造期间抛出exceptions”的对象,所以你必须设计你的constructors,使它们在那种情况下亦能自我清理。通常这只需将所有可能的exception捕捉起来,执行某种清理工作,然后重新抛出exception,使它继续传播出去即可。

使用member initialization list初始化data members,是在进入构造函数之前,因此它们的资源释放由它们自己的构造函数和析构函数负责,进入到本类的构造函数就表明它们已经完成了构造,发生异常时,C++会自动清理。

如果用多个表达式做member initialization list为成员指针初始化,仍然有可能导致资源泄漏,并且无法捕获处理。可行的解决方法是用函数调用来取代表达式,初始化后面的变量的函数需要捕捉函数,确保发生异常时,前面初始化的资源能释放。

更好的做法是将指针所指对象视为资源,交给临时对象(智能指针)来管理。这样就可以既利用初始值列表,又不用担心异常发生时导致资源泄露。

结论:

(1)如果以auto_ptr对象来取代pointer class members,你便对你的constructor做了强化工作,免除“exceptions出现时发生资源泄漏”的危机,不再需要在destructor内亲自动手释放资源,并允许const member pointer得以和non-const member pointers有着一样优雅的处理方式。

(2)处理“构造过程中可能发生exceptions”,相当棘手。但是auto_ptr(以及与auto_ptr相似的classes)可以消除大部分劳役。

11、禁止异常流出destructor之外

两种情况下destructor会被调用:(1)当对象在正常状态下被销毁,也就是它离开它的生存空间或是被明确地删除;(2)当对象被exception处理机制——也就是exception传播过程中的stack-unwinding(栈展开)机制——销毁。

总结:

有两个理由支持“全力阻止exceptions传出destructors之外”:(1)它可以避免terminate函数在exception传播过程的栈展开机制中被调用;(2)它可以协助确保destructors完成其应该完成的所有事情。

12、了解“抛出一个异常(exception)”与“传递一个参数”或“调用一个虚函数”之间的差异

函数参数和exceptions的传递有相同点也有不同点。(1)相同点:函数参数和exceptions传递方式可选有3种(by value,by references,by pointer)。(2)不同点:调用一个函数时,控制权最终会回到调用端(除非函数失败或无法返回),但是当你抛出一个exception,控制权不会再回到抛出端;后者比前者慢。

不论被捕捉的exception是以by value或by reference方式传递(不可能以by pointer方式传递——那将造成类型不吻合),都会发生对象的复制行为,而交到catch子句手上的正是那个副本。也就是:C++特别声明,一个对象被抛出作为exception时,总是会发生复制(copy)。(因为一般情况下原对象会已经被析构,因为超过了其作用域范围。即使对象没有被销毁的危险,复制行为还是会发生。)

exception抛出复制时,执行copy constructor,这个copy constructor相对于该对象的“静态类型”而非“动态类型”。(复制动作永远是以对象的静态类型为本)

catch语句有三种形式:

(1)catch(T w);     (两个副本的代价)

(2)catch (T& w);      (一个副本的代价)

(3)catch(const T& w); (引用形式与函数有区别,意味exception总是会复制传递,所以实际上会接收一个临时对象,对于函数来说,用临时对象传递给一个non-const references是不允许的,但对exception来说都合法)

此外,exception也支持指针,和函数一样,不过要注意不要抛出一个局部对象指针。

函数传参数时会可能有隐式类型转换发生,但是exception基本不会。try语句中抛出的int exception绝不会被“用来捕捉double exception”的catch语句捕捉到。

exception与catch子句相匹配的过程,仅有两种转换可以发生:(1)“继承架构中的类型转换”,一个针对base class exceptions而写的catch子句,可以处理类型为derived class的exception;(2)另一个允许转换时从一个“有型指针”转为“无型指针”,因此对于一个const void*指针而设计的catch子句,可以捕获任何指针类型的exception。

与参数传递另一个不同,“传播exception”时,catch子句总是依出现顺序做匹配尝试。这带来一条规则:绝不要将“针对base class而设计的catch子句”放在“针对derived class而设计的catch子句”之前,那样做没有意义。

总结:

“传递参数到函数去,或是以对象调用虚函数”和“将对象抛出成为一个exception”之间,有3个主要差异:

(1)exception总是会被复制,如果是以by value方式捕捉,它们甚至被复制2次;传递给函数参数的对象不一定要复制(如引用方式);

(2)“被抛出为exceptions”的对象,其被允许的类型转换动作,比“被传递到函数去”的对象少;

(3)catch子句以其“出现于源代码的顺序”被编译器检验对比,其中一个匹配成功便自行,而虚函数匹配,或函数匹配,都是遵循“最佳匹配”原则。

13、以by references方式捕捉exception

throw by pointer是唯一在搬移“异常相关信息”时不需要复制对象的一种做法,理论上是最有效率的。它的最大问题是——让exception objects在控制权离开那个“抛出指针”的函数之后仍然存在,这一点常常容易忘记。“to delete or not to delete?”是个大问题!

有4个标准的exceptions:(1)bad_alloc——当operator new无法满足内存需求时发出;(2)bad_cast——当对一个reference实行dynamic_cast失败时发出;(3)bad_typeid——当dynamic_cast被实施于一个null指针时发出;(4)bad_exception——适用于未预知的异常情况。

标准exceptions都是对象,而不是指针,所以使用指针抛出异常与标准有所矛盾。总之,应该采用by value或者by reference方式捕捉它们。

catch by value有两个缺点:(1)每次捕捉时,都会复制两次,产生两个临时对象;(2)可能会引起切割问题,当用base class捕捉了一个derived class exception objects时会发生切割,完全失去派生部分(虚函数也没法表现)。

catch by references除了会复制一次,产生一个临时对象外,没有上面的任何缺点。没有临时对象删除问题,没有切割问题,只会复制一次等等。所以应当以by references方式捕捉exceptions。

总结:

如果catch by reference,你就可以避开对象删除问题,也可以避开exception objects的切割问题,还可以保留捕捉标准exception的能力,约束了复制次数,所以catch exceptions byreference吧!

14、明智运用exception specification

编译器有时候能够在编译期侦测到与exception specifications(异常规范)不一致的行为。如果函数抛出了一个并未列于其exception specification的exception,这个错误会在运行期被检验出来,于是特殊函数unexcepted会被自动调用。

然而,unexpected的默认行为只是调用terminate,而terminate的默认行为是调用abort,程序如果违反exception specification,默认结果是程序被终止,而局部变量不会获得销毁的机会。

不应该将templates和exception specification混合使用。因为templates几乎必然会以某种方式使用其类型参数,没法知道这些类型参数会抛出什么exception。

C++允许你以不同类型的exceptions取代预期的exception。利用此,可以利用set_unexpected()调用替换默认的unexpected函数,用自己的函数抛出一个统一的异常类型以满足exception specification要求,或者直接重新抛出当前的exception,该exception会被标准类型bad_exception取而代之,只要exception specification 包含bad_exception(或其基类exception)即可。

exception specification的另一个缺点就是,容易发生在预期的处理发生的exception执行之前,unexpected函数却被调用的现象。(有意捕捉,却没机会。。。)

结论:

避免unexpected函数调用的规则:(1)不应该将templates和exception specification混合使用;(2)如果A函数内调用了B函数,而B函数无exception specification,那么A函数本身也不要设立exception specification,特别是有回调函数时;(3)处理“系统”可能抛出的exception。

exception specification对于“函数希望抛出什么样的exception”提供了卓越的声明,而在“违反exception specification的下场十分悲惨,以至于需要立即结束程序”的形势下,它们提供了默认行为,它们很容易被违反,更会妨碍更上层的exception处理函数处理未预期的exception。所以,使用前要考虑清楚。

15、了解异常处理(exception handling)的成本

为了能够在运行期处理exceptions,函数必须做大量簿记工作:在每一个执行点,需要确认“如果发生exception,哪些对象需要析构”;必须在每一个try语句块的进入点和离开点做记号,针对每个try语句块它们必须记录对应的catch子句及能够处理的exception等。这些簿记工作需要付出代价运行期比对工作(以确保复合exception specification)不是免费的,exception抛出时找到合适的catch子句也不是免费的。总之,exception的处理需要成本,即使从未用过关键词try
    throw 或catch,你也必须付出至少某些成本。

“最低消费额”:必须付出一些空间,放置某些数据结构(记录着哪些对象已经被完全构造妥当);必须付出一些时间,随时保持那些数据结构的正确性。编译中如果没有加上对exception的支持,程序通常比较小,执行时也比较快;如果编译时加上了对exception的支持,程序就比较大执行时也比较慢。这个成本一般难以避免,除非你的编译器支持,在你确保程序中完全不涉及exception的处理,可以明确告知编译器进行优化节省成本。

exception处理机制的第二个成本——try语句块,只要出现一个,就得付出这个成本。使用try语句块会造成代码膨胀,执行速度下降等。因此,非必要的try语句尽量避免。对exception specification,编译器是和对待try语句块同等待遇,花费相同成本。

相较于函数返回动作,抛出exception而导致的函数返回,其速度可能比正常情况慢3个数量级。所以exception的出现应该是罕见的,不常出现的。

总结:

不论exception的处理过程需要多少成本,你都不应该付出比你该付出的部分更多。为了让exception的相关成本最小化,只要能够不支持exception,编译器便不支持;请将你对try语句块和exception specification的使用限制于非用不可的地点,并且在正常异常情况下才抛出exceptions。必要时请利用分析工具分析程序。

16、谨记80-20法则

80-20法则说:一个程序的80%资源用在了20%代码上。

不管具体比例是多少,重点在于:软件的整体性能总是由其构成要素(代码)的一小部分决定。

找出程序的瓶颈,是件很大的麻烦。可行之道是完全根据观察或实验识别出你心痛的20%代码,而辨识之道就是借助某个程序分析器。

尽可能地以最多的数据来分析你的软件,此外保证数据的可重制性。

17、考虑使用lazy evalution(缓式评估)

采用lazy evaluation,就是以某种方式撰写你的classes,使它们延缓运算,直到那些运行结果刻不容缓地被迫切需要为止。如果其运算结果一直不被需要,运算也就一直不执行。

lazy evaluation可在多种场合下派上用场,这里描述4种:(1)reference counting(引用计数)——在你真正需要之前,不必着急为某物做一个副本,取而代之的是,以拖延战术应付之,只要能够,就使用其他副本;(2)区分读和写——例如对一个reference-counted字符串做读取动作,代价十分低廉,而对这样一个字符串做写入动作,可能需要实现为该字符串做出一个副本
;(3)lazy fetching(缓式取出)——如做数据库数据恢复时,对于一个对象,一开始产生该对象的外壳,不从数据库或磁盘读取任何字段数据,只有当对象某个字段被需要时,程序才从数据库或磁盘中取回对应的数据;(4)lazy expression evaluation(表达式缓评估)。

mutable关键字告诉编译器,这样的字段可以在任何member function内被修改,甚至是在const member function内。如果编译器不支持mutable,可以采用“冒牌this”法:产生一个pointer-to-non-const指向this所指向的对象,修改这个指针实现数据修改。T* const fakeThis=const_cast<T*
const>(this);

总结:

(1)lazy evaluation在许多领域中都有可能有用途,可避免非必要的对象复制,区别读写动作、避免非必要的数据库读取动作、数据计算等等。

(2)lazy evaluation并非永远都是好主意。如果你的计算是必要的,lazy evaluation并不会为你的程序节省任何工作或任何时间。事实上,如果你的计算绝对必要,lazy evaluation甚至可能会使程序变缓慢,并增加内存用量,因为程序除了必须做的你原本希望避免的所有工作之外,还必须处理那些为了lazy evaluation而设计的数据结构。只有当有些计算可以避免时,lazy evaluation才有用处。

(3)lazy evaluation是程序值得考虑的一个程序优化方向。C++因为具有封装性质,所以适合考虑lazy evaluation,因为这些对客户并不知道。

18、分期摊还预期的计算成本

函数实现有三种方式:(1)eager evaluation(急式评估),被调用时才检查、计算;(2)lazy evaluation(缓式评估),在真正需要这些数值时才决定计算等操作;(3)over-eager evaluation(超急评估),随程序过程中预先计算某些值,在被调用时,就无需计算便可返回。

over-eager evaluation背后的观念就是,如果你预期程序常常会用到某个计算,你可以降低每次计算的平均成本,办法就是设计一份数据结构以便能够极有效率地处理需求。

over-eager evaluation实现:(1)一种简单的做法就是将“已经计算好有可能再被需要”的数值保留下来(所谓caching);(2)prefetching(预先取出)是另一种方法,如以块为单位读取磁盘等,再如vector的预分配等。

对stl迭代器iterator,在缺少信息的情况下最好使用“*(itrator).f”,而不要使用"->",因为iterator本身是个对象而不是指针,所以并不能保证后一种方式可施行于iterator身上。但是,STL明确规定,迭代器iterator保证“.”和“*”有效,因此前一种写法最具移植性。

总结:

(1)一条规律概括:较佳的速度往往导致较大的内存成本。“空间交换时间”(也不是总是成立)

(2)over-eager evaluation和lazy evaluation并不矛盾:当你需要支持某些运算而其结果并不总是需要的时候,lazy evaluation可以改善程序效率;当你必须支持某些计算而其结果几乎总是被需要,或其结果常常被多次需要时,over-eager evaluation可以改善程序效率。

19、了解临时对象的来源

C++真正的所谓临时对象是不可见的,不会出现在你的源代码中。只要你产生一个non-heap objects而没有为它命名,便诞生了一个临时对象。

匿名对象通常发生于两种情况:(1)隐式类型转换被施行起来以求函数调用能够成功(以为着如果类型不匹配,而且不能产生临时对象,那么函数调用往往不成功);(2)当函数返回对象的时候。

当对象以by value(传值)方式传递,或是当对象被传递给一个reference-to-const参数时,这些转换才会发生。

总结:

临时对象可能很耗成本,所以呢应该尽可能地消除它们。

(1)任何时候只要你看到一个reference-to-const参数,就极可能会有一个临时对象被产生出来绑定至该参数上;

(2)任何时候只要你看到函数返回一个对象就会产生临时对象(并于稍后销毁)。

20、协助完成“返回值优化(RVO)”

函数by value返回对象意味着背后隐藏的constructor和destructor都将无法消除。问题很简单:如果为了行为正确而不得不这样,函数可返回一个对象;否则就不要那么做。

有一条优化by value返回的技巧:返回所谓的constructor argument以取代对象。

如 const T operator(){T t(...);return t;}修改为const T operator(){return T(...);}

这样做是因为C++允许编译器将临时对象优化,使它们不存在。而前一种写法,是产生一个局部变量,无法优化。

总结:

可以利用函数的return点消除一个局部临时对象(并可能用函数调用端的某对象取代),它甚至有个专属名称:return value opyimization(返回值优化)。

21、利用重载技术(overload)避免隐式转换(implict conversions)

利用重载可以避免隐式类型转换,从而避免产生临时对象。

C++规定,每一个“重载”操作符必须获得至少一个“用户定制类型”的自变量。如const T operator+(int,int)就不是合法的重载,因为int并不是用户定制类型。(如果这个规则不存在,程序员就可以改变预先定义的操作符意义,导致天下大乱)

“用以避免产生临时对象”的此等重载技术,并不局限使用在操作符函数身上。

总结:

增加一大堆重载函数不见得是件好事,除非你有好的理由相信,使用重载函数后,程序的整体效率可以获得重大改善。

22、考虑以操作符复合形式(op=)取代其独身形式(op)

x+=y;x-=y等这些是操作符的复合形式,如果x和y属于定制类型,则上述操作不会成功。

到目前为止,C++并不考虑在operator+、operator=和operator+=之间设立任何互动关系。所以如果你希望这3个操作符存在你所期盼的互动关系,必须你自己实现。

要确保操作符的复合形式(如operator+=)和其独身形式(如operator+)之间的自然关系存在,一个好的办法就是以前者为基础实现后者。这样只需要维护符合形式的操作符就可以统一,还可以让独身形式以模板的方式放在全局范围内。

关于效率:(1)一般而言,复合操作符比其对应的独身版本效率高,因为独身版本通常必须返回一个新对象,因此需要负担临时对象的构造和析构函数,至于符合版本则是直接将结果写入其左端自变量,所以不需要产生一个临时变量来放置返回值;(2)如果同时提供某个操作符的复合形式和独身形式,便允许你的客户在效率和便利性之间做取舍;(3)提供复合版本,并以其为基础实现独身版本,可以方便利用“返回值初始化”。

自古历来匿名对象总是比命名对象更容易被消除,所以当你面临命名对象或临时对象的抉择时,最好选择临时对象。

总结:

操作符的“复合版本”比其对应的独身版本有着更高的效率。但是身为一个程序库设计者,你应该两者都提供;如果作为一个应用软件开发者,性能首要的话,应该考虑复合版本操作符取代其独身版本。

23、考虑使用其他程序库

总结:

程序库的设计,可以说是一种折中态度的练习。不同的程序库即使提供相似的机能,也往往表现出不同的性能取舍。

24、了解virtual functions、multiple inheritance、virtual base classes、runtime type identification的成本

每个类有一个或多个虚函数表,单纯继承下来的类有一个虚函数表,虚继承或多重继承下来的可能有多个虚函数表,对象没有虚函数表,但是有指向虚函数表的指针。

程序中每一个class凡声明(或继承)虚函数者,都有一个自己的虚函数表。

虚函数成本:(1)你必须为每一个拥有虚函数的class耗费一个vtbl空间,其大小视虚函数的个数(包括继承而来的)而定;

(2)你必须为每一个拥有虚函数的对象内付出“一个额外指针”的代价;

(3)虚函数真正的运行期成本发生在和inlining互动的时候,事实上你已经放弃了inlining。

vtbls放在哪:(1)暴力法是在每一个需要vtbl的目标文件中都产生一个vtbl副本,最后再由连接器删除重复的副本,使最后的可执行文件或程序库中只留下每个vtbl的单一实体;(2)探勘法是class‘s vtbl被产生于“内含其第一个non-inline,non-pure虚函数定义式”的目标文件中。vtbl具体放在哪由编译器决定。

应避免将虚函数声明为inline,而目前的编译器也会通常忽略虚函数的inline指示。

虚函数指针(vptr)可以指示出每个对象对应于哪一个vtbl。凡是声明有虚函数的class,其对象都含有一个隐藏的data member,用来指向该class的vtbl,这个隐藏的data member就是所谓的vptr。vptr在对象内存的具体哪个位置由编译器决定。

虚函数多态实现过程:(1)根据对象的vptr找出其vtbl;(2)找出被调用函数在vtbl内的对应指针;(3)调用步骤2所得指针所指向的函数。

调用虚函数的成本,基本上和“通过一个函数指针来调用函数”相同,虚函数本身并不构成性能上的瓶颈。

在多重继承时,“找出对象内的vptrs”会变得复杂,因为此时一个对象内会有多个vptrs(每个base class各对应一个);除了常规的vtbls之外,针对base classes而形成的特殊的vtbls也会被产生出来。

多重继承,往往导致virtual base classes(虚拟基类)的需要。为了消除重复复制现象,常常利用指针,指向“virtual base class成分”,而你的对象内可能会出现一个(或多个)这样的指针

virtual base classes可能会导致对象内的隐藏指针增加。

运行期类型识别(RTTI)也会造成成本。RTTI让我们得以在运行期获得objects和classes的相关信息,所以一定得有某个地方来存放信息才行——它们被存放在类型为type_info的对象内,利用typeid操作符可以取得某个class相应的type_info对象。RTTI是根据class的vtbl来实现的。(C++规范:只有当某种类型拥有至少一个虚函数,才保证我们能够检验该类型对象的动态类型)

 性质 对象大小增加 Class数据量增加 Inlining几率降低
虚函数(Virtual Functions)
多重继承(Mutiple Inheritance)
虚拟基类(Virtual Base Classes) 往往如此 有时候
运行期类型识别(RTTI)

总结:

如果你需要使用这些性质所提供的机能,你就必须忍受那些成本,毕竟是实难两全。从效率上看而言,你自己动手,不开可能做得比编译器所产生的代码更好。

25、将constructor和non-member function虚化

所谓Virtual constructor是某种函数,视其获得的输入,可产生不同类型的对象。

有一种特别的virtual constructor——virtual copy constructor——会返回一个指针,指向其调用者(某对象)的一个新的副本,通常以copySelf或cloneSelf命名。

virtual copy constructor往往只是调用真正的copy constructor。这样可以保持这两个copy函数的一致性。

“虚函数只返回类型”规则有一个宽松点:晚些才被接纳原则——但derived class重新定义其base class的一个虚函数时,不再需要一定得声明与原本相同的返回类型。如函数的返回类型是一个指针(或者reference),指向一个base class,那么derived class的函数可以返回一个指针(或者reference),指向该base class 的一个derived class。

non-menber functions的虚化十分容易:写一个虚函数做实际工作,再写一个什么都不做的非虚函数,只负责调用虚函数。

26、限制某个class所能产生的对象数目

阻止某个class产生对象:最简单办法就是将其constructor声明为private。

产生唯一的一个对象设计:(1)将类的constructor访问符设为private;(2)一个全局函数被声明为该class的友元函数,使其能访问private成员;(3)友元函数内含一个static类对象,意思是只有一个类对象会被产生出来。另一种设计就是取消全局函数,放到类里,或者放到一个namespace内;或者一个static成员用来计数,在constructor中递增,在destructor递减,在constructor里时判断数目,超过时抛出异常(这种方法也适合于限定数目多个时,缺点是继承体系混乱)。

namespace可以阻止名称冲突。

区分static对象在class内核在函数中的情形区别:(1)“class拥有一个static对象”的意思是,即使从未被用到,它也会被构造(和析构);(2)“函数拥有一个static对象”的意思是,此对象在函数的第一次被调用时才产生,如果该函数未被调用,这个对象也就绝对不会诞生。

含有static对象的函数最好不要inline,即使很简单。这是因为一个inline non-member functions,不仅意味着编译器应该将每一个调用动作以函数本身取代,还意味着,这个函数有内部连接,函数如果带有内部连接,可能在程序中会被复制。因此如果一个inline non-member function中含有一个local static对象,你的程序可能会拥有该static对象的多个副本。

带有private constructor的classes,不能被用来当做base classes,也不能被内嵌于其他对象内。(想一个类不被继承:private构造函数和析构函数、final修饰。想一个类不被实例化:private构造函数、纯虚函数)

在class定义区内为const static members指定初值,需要编译器支持,一定不支持non-const static member指定初值。

编写一个base class模板类专门用来计数,再让derived class采用private继承的方式继承,可以将计数工作交给基类去完成。

27、要求(或禁止)对象产生于heap之中

(1)要求对象产生于heap中

即阻止clients不得使用new以外的方法产生对象,方法是让那些被隐式调用的构造函数和析构函数不合法——比较好的办法是让destructor成为private,而constructor仍为public。(为了销毁对象,可以用pseudo(伪的)destructor函数,用来调用真正的destructor)。——继承问题:可令destructor为protected即可解决。

编译器产生的函数总为public。

(2)判断某个对象是否在heap中

在operator new(或operator new[])内检查某个位是否被设立起来,并不是“决定*this是否位于heap内”的一个可靠做法。

程序的地址空间以线性序列组织而成,其中stack(栈)高地址往低地址成长,heap(堆)由低地址往高地址成长。(也不是所有系统都这样)

利用栈地址和堆地址的特点,可以设计一个判断是否在heap中的思路:与一个local stack对象比较地址大小。(逻辑正确,但是不够完善。除了堆地址空间和栈地址空间,还有static对象,它无法区分static对象和heap对象)

所谓static对象,不止涵盖明白声明为static的对象,也包括global scope和namespace scope内的对象。

不只没有一个“绝对具移植性”的办法可以决定某对象是否位于heap内,甚至没有一个“颇具移植性”做法在大部分时候有用。

并非所有指向heap内的指针都可以被安全删除。判断指针删除动作是否安全的依据是:此指针是否由new返回。

当对象设计多重继承或者虚继承的基类时,对象拥有多个地址(此即非自然多态,unnatural polymorphism)。

所谓abstract base class是一个不能够被实例化的base class,也就是说它至少有一个纯虚函数。

(3)禁止对象产生于heap之中

产生于heap中的对象总是以new产生出来的,因此让clients无法调用new即可。因此,可以将operator new(operator new[])和operator dalete(operator delete[])设为private

28、Smart Pointer(智能指针)、

所谓smart pointer,是看起来、用起来、感受起来都像內建指针,但提供更多机能的一种对象。

当你用smart pointer取代C++的內建指针(亦即所谓的dumb pointer),你将获得以下各种指针行为的控制权:(1)构造和析构;(2)复制和赋值;(3)解引。

smart pointer的构造行为:确定一个目标物(通常是利用smart pointer的constructor自变量),然后让smart pointer内部的dumb pointer指向它。

auto_ptr被复制或被赋值时,其“对象拥有权”会转移。因此要防范以by value的方式传递auto_ptr,一般采用pass-by-reference-to-const可以取代by-value。

当smart pointer被复制,或是身为赋值动作的来源端时,它可能会被改变,因此其copy constructor和assignment constructor一般不采用const类型,与一般的数据结构不同。

在多态中,返回reference或者pointer而不是返回对象的原因:(1)返回对象会存在“切割”的问题,以该对象调用虚函数,将不会调用“被指之物”的动态类型响应函数;(2)references和pointer效率高,不需要构造临时对象。

测试smart pointer是否为null:(1)一种方法是提供一个隐式类型转换操作符(operator void*()),它可支持if(ptr==NULL)这样的操作,但是问题依然存在,提供这样的隐式转换操作,会导致允许不同类型的指针比较这样危险的事情;(2)对你的smart pointer提供operator!,并在其提供(某个smart
pointer)是null时,返回true,这种方法缺点是只允许if(!ptr)这样的判断,其他常用指针判断都是不允许的;(3)提供smart pointer到dumb pointer的隐式转换操作符,这样可以让不仅可以使智能指针在必要时自动转换为原始指针,而且还能解决(2)中的问题,这种方法的缺点就是直接暴露了原始指针,允许clients直接操作。

提供smart pointer转换到dumb pointer会有一个隐藏的问题,就是会允许delete一个smart pointer,从而容易导致资源释放两次。

所以重点:不要提供对dumb pointer的隐式转换操作符,除非不得已。

编译器禁止对一个对象一次施行一个以上的类型转换。

用有继承关系的class作为smart pointer模板参数所生成的是没有继承关系的。因此多态此时没有办法实现。解决思路:令每一个smart pointer class有一个隐式类型转换操作符,用来转换至另一个smart pointer class。如果你手上有一个dumb pointer类型为T1*,另一个dumb pointer类型为T2*,只要你能够将T1*隐式转换为T2*,你便能够将smart pointer-to-T1隐式转换为smart
pointer-to-T2.

template class里可以有template member function(当然这个函数需要时nonvirtual)

利用member templates来转换smart pointer,有一些缺点:(1)首选对于多级继承,如果存在多个转型函数,编译器是没法判断优劣的,模棱两可;(2)目前支持member template的编译器还不多,移植性不高;(3)其中蕴含的技术较复杂。

能够使得“smart pointer classes的行为”在“与继承相关的类型转换”上,能够和dumb pointer一样吗?不能够!!!(smart pointer虽然smart,却不是pointers)代价!!!

在const方面,面对smart pointer,只有一个地方能够放置const:只能释放于指针身上,不能及于其所指之物!(另一个大的区别)。它的解决方法很直观,实例化smart时指定类型是const T。

smart_ptr<T> p;const smart_ptr<T> p;smart_ptr<const T> p;const smart_ptr<const T> p对应普通指针的4种const情形。但是还是存在区别。smart_ptr<T>和smart_ptr<const T>是完全不相同的类型。因此普通指针里将non-const指针作为初值赋给const指针之类的操作是不可能的,需要提供一个转型函数。

29、References counting(引用计数)

reference counting技术允许多个等值对象共享同一实值。此技术的发展有两个动机:(1)为了简化heap objects周边的簿记工作,构造出垃圾回收机制的一个简单形式;(2)实现一种常识——如果许多对象有相同的值,将那个值存储多次是一件愚蠢的事。

一个技巧:将一个struct嵌套放入一个class的private段落内,可以很方便地让该class的所有members有权处理这个struct,而又能禁止任何其他人访问这个struct。

引用计数实现:给class加一个private struct专用于计数,内容包含一个计数器和计数对象的值。

写时复制(copy-on-write):对于const对象,访问([])很简单,只有读操作;对于non-const对象,访问要分读和写,但是C++并没有区分读或写的机制,所以要假设所有访问都是写,此时需要对之前的内容执行计数减操作,重新做一份副本,在副本上操作。

间接引用或指针可能会导致多个对象同时被修改(因为实质上它们共享数据)。解决方案:(1)忽略这个问题,这是很多程序库采用的,视而不见;(2)做出警告提示;(3)在计数器内设置标志,用以指示是否允许被共享,初始化为true,一旦non-const operator[]作用于对象值身上就将标志值清除,一旦清除,永远不可能再改变状态。

任何class如果其不同的对象可能拥有相同的值,都适用引用计数技术。

为了让引用计数功能方便运用到其它class上,可以将其设计成一个base class,其它class想要有引用计数功能就需要继承它。

可以让一个嵌套类继承另一个类,而后者与外围类完全无关。

reference counting是个优化技术,其适用前提是:对象常常共享实值。如果这个假设失败,reference counting反而会赔上更多内存,执行更多代码。反过来,如果你的对象有“共同实值”的倾向,reference counting可同时节省你的时间和空间。

使用reference counting改善效率的最适当时机:(1)相对多数的对象共享相对少量的实值;(2)对象实值的产生或销毁成本很高,或是它们使用许多内存。

30、Proxy classes(替代类、代理类)

二维数组data[m][n]可以看成是一维数组:由m个元素的一维数组,该数组的每个元素又是n个元素构成的数组。

凡“用来代表(象征)其它对象”的对象,常被称为proxy objects(替身对象),而用以表现proxy objects者,我们通常称为proxy classes。

operator []可以在两种不同情境下调用:用来读取一个元素,或是用来写一个元素。读取动作是所谓的右值运用(rvalue usages),写动作是所谓的左值运用(lvalue usages)。一般而言,以一个对象作为lvalue,意思是它可以被修改,而以之作为rvalue的意思是它不能被修改。

很不幸,没有人能够在operator[]内区分它是左值运算或是右值运算。

proxy class很适合用来区分operator[]的左值运用和右值运用。

一般而言,“对proxy取址”所获得的指针类型和“对真实对象取址”所获得的指针类型不同。

总结:

proxy classes允许我们完成某些十分困难或几乎不可能完成的行为:(1)多维数组;(2)左值、右值的区分;(3)压抑隐式转换。

proxy classes也有缺点:proxy objects是一种临时对象,需要被产生和被销毁;proxy classes的存在也增加了软件系统的复杂度,因为额外的classes使产品更难设计、实现、了解、维护;当class的身份从“与真实对象合作”转移到“与替身对象(proxies)合作”,往往会造成class语义的改变。

31、让函数根据一个以上的对象类型来决定如何虚化

“以类型为行事准则”会造成程序难以维护。欲对这种程序再扩充(指加入新的类型),基本上是很麻烦的,这便是虚函数当初被发明的主要原因:把生产及维护“以类型为行事准则之函数”的负荷,从程序员肩上转移给编译器。

编译器通常以指针来实现references,pass-by-reference通常是利用“传递一个指向对象的指针”完成的。当对象拥有多个base classes,并以by reference方式传递给函数,编译器是否传递了正确的地址(此地址相应于被调用函数的参数声明类型),将是非常关键的。

匿名namespace内的每样东西对其所驻在的编译单元(文件)而言都是私有的,其效果就好像在文件头里将函数声明为static一样。

make_pair和pair的区别:make_pair可避免构造一个pair对象时“必须指定类型”的麻烦。

32、在未来时态下发展程序

好的软件对于变化有良好的适应能力。好的软件能够容纳新的性质,可以移植到新的平台,可以适应新的需求,可以掌握新的输入。软件具备如此的弹性、健壮性、可依赖性,是靠设计者和实现者共同努力的结果。

程序的维护者通常都不是当初的开发者,所以设计和实现时应该注意到如何帮助其他人理解、修改、强化你的程序。

避免“demand-paged”式的虚函数,你应该决定函数的意义。并决定它是否适合在derived classes内被重新定义。如果是,就把它声明为virtual,即使眼前并没有人重新定义它。如果不是,就把他声明为nonvirtual,并且不要只为了图某人方便就改变其定义。

请为每一个class处理assignment和copy constructor,即使没有人使用那样的动作。(以防止编译器产生默认的错误的版本)

请努力让classes的操作符和函数拥有自然的语法和直观的语义。请和內建类型的行为保持一致。

请让你的classes容易被正确使用,不容易被误用。请接受“客户会犯错”的事实,并设计你的classes有预防、侦测或甚至更正的能力。

请努力写出可移植的代码。

请设计你的代码,使“系统改变所带来的冲击”得以局部化。尽可能采用封装性质,并尽可能让实现细目成为private。

尽量避免设计出virtual base classes,因为这种classes必须被其每一个derived classes(即使是间接派生者)初始化。避免以RTTI(运行期类型)作为设计基础因而导致层层if...else...,因为每当class继承体系一有改变,每组这样的语句都得更新,如果你忘了其中一个,编译器不会给你任何警告。

总结:

未来式思维只不过是加上一些额外的考虑:

(1)提供完整的classes——即使某些部分目前用不上,当新的需求进来,你不太需要回头去修改那些classes;

(2)设计你的接口,使有利于共同的操作行为,阻止共同的错误;

(3)尽量使你的代码一般化(泛化),除非有巨大的不良后果。

未来式思维可增加你的代码重用性、增加其可维护性、使它跟健壮,并促使在一个“改变实乃必然”的环境中有着优雅的改变。

33、将非尾端类(non-leaf-classes)设计成抽象类(abstract classes)

用两个基类指针指向两个派生类对象,然后对这两个指针进行assignment操作,会造成“部分赋值”的问题,也就是只有基类的部分会被赋值。

如果让基类的assignment操作符成为虚函数,我们便打开了“异型赋值”的一扇门,因为这导致assignment函数里的参数都必须是一样的(基类对象)。使用dynamic_cast进行类型转换时解决问题的一种方案。

derived classes的assignment操作符有义务调用base classes的操作符,因此如果基类的assignment操作符设为private,派生类的assignment操作符即使声明为public也是没法进行正常的赋值动作的。

如果一个被设计成抽象类的类里,没有任何member functions可以很自然地被声明为纯虚函数,这种情况下,传统做法是让destructor成为纯虚函数。因为为了支持通过指针而形成的多态特性,base classes无论如何都需要一个virtual destructor。唯一的成本是必须在class定义式之外实现其内容。

将函数声明为纯虚函数,并非暗示它没有实现码,而是意味着:(1)目前这个class是抽象的;(2)任何继承此class的具体类,都必须将该纯虚函数重新声明为一个正常的虚函数(也就是,不可以再令它=0)。

大部分的纯虚函数没有实现码,但是pure virtual destructor是个例外,它必须被实现出来,因为只要有一个derived class destructor被调用,它们便会被调用。因此对于pure virtual destructor而言,实现不仅是平常的事,甚至是必要的事。

只有当你有能力设计某种class,使未来的classes可以继承它而它不需要任何改变时,你才能从一个“抽象类”中获得利益。

一旦你需要将两个具体类以public inheritance的方式产生关联,通常的确就表示你需要一个新的抽象类了。

总结:

继承体系中的non-leaf(非尾端)类应该是抽象类。坚持这个法则可以为你带来许多好处,并提升整个软件的可靠度、健壮度、精巧度、扩充度。

34、如何在同一个程序中结合C++和C

C和C++混合使用前,请确定你的C++和C编译器产生兼容的目标文件。

此外,还有4件事情需要考虑:(1)name mangling(名称重整)、(2)static(静态对象)初始化、(3)动态内存分配、(4)数据结构的兼容性。

(1)name mangling(名称重整)

name mangling是一种程序:通过它,你的C++编译器为程序内的每一个函数编出独一无二的名称。而在C中,此程序并无必要,因为你无法将函数名重载,但是几乎所有C++程序都至少有一些函数拥有相同的名称。重载并不兼容于大部分的连接器,因为连接器往往将多个同名函数视为不正常。Name mangling是对连接器的一个退让,更明确地说是对“连接器往往坚持所有的函数名称必须独一无二”的一个让步。

你需要某种方法告诉你的C++编译器,叫它不要重整某些函数名称。绝对不要重整以其他语言撰写的函数的名称。

要压抑name mangling,必须使用C++的extern "C"指令。这是一个说明,说明该函数应该以C语言的方式调用。

只要压抑你的C++函数名称的name mangling程序,你的客户就可以使用你所选择的那个自然而直观的名称,而非经过编译器重整后的名称。

预处理器符号区分C++还是C编译器:__cplusplus(C++)、__STDC__(C)

没有所谓的“name mangling标准算法”。不同的编译器可自由地以不同的方法来重整名称,而各家编译器也的确各行其道。

(2)static(静态对象)初始化

许多代码会在main之前或者之后执行起来:static class对象、全局对象、namespace内的对象以及文件范围(file scope)内的对象,其constructor总是在main之前就获得执行。这个过程成为static initialization。通过static initialization产生出来的对象,其destructor必须在所谓的static destruction过程中被调用,这发生在main结束之后。

因为main才是程序的起点,而有些对象却必须在main之前构造。为了解决这个问题,许多编译器在main一开始处安插了一个函数调用,调用了一个由编译器提供的特殊函数。正是这个特殊函数完成了static initialization。同样道理,编译器往往在main的最尾端安插一个函数调用,调用一个特殊函数,其中完成static对象的析构。

因为C++中涉及到static初始化,而这依赖于main函数,因此最好尽量在C++中撰写main,然后把C中的mian改名,有C++中的main直接调用即可。

如果无法在项目中的C++部分撰写main,会遇到一个问题,因为没有其他具有移植性的办法可以确保static对象的constructors和destructions或被调用。有些编译器厂商会提供自己办法来解决这个问题,需要查看文档或者与厂商联系。

(3)动态内存分配

动态内存分配的规则很简单:程序的C++部分使用new和delete,程序的C部分则使用malloc和free。只要内存是以new分配而得,就以delete删除之;只要内存是以malloc分配而得,就以free释放之。

严密地将你的new/delete和malloc/free分隔开来。

(4)数据结构的兼容性

C++和C之间有可能传递数据吗?

由于C无法了解C++的特性,因此两个语言的对话层次必须限制在C能接受的范围:由于C了解一般的指针,所以如果你的C++和C编译器有着兼容的输出,两个语言的函数便可以安全地交换对象指针、non-member函数指针,或是static函数指针;structs以及內建类型的变量也可以安全地跨越C++/C边界。

C++中的struct(或class)之中如果只含非虚函数,其对象应兼容于C struct(因为C++ member functions并不在对象布局中留下任何蛛丝马迹)。如果加上虚函数,或是有base class,会造成其对象采用不同的内存布局,无法和C函数交换数据。

从数据结构的观点来看:在C和C++之间对数据结构做双向交流,应该是安全的——前提是那些结构的定义式在C和C++中都可编译。

总结:

如果你打算在同一个程序中混用C++和C,请记住以下几个简单守则:

(1)确定将你的C++和C编译器产生兼容的目标文件(objects files);

(2)将双方都适用的函数声明为extern "C";

(3)如果有可能,尽量在C++中撰写main;

(4)总是以delete删除new返回的内存;总是以free释放malloc返回的内存;

(5)将两个语言间的“数据结构传递”限制于C所能了解的形式;C++ structs如果内含非虚函数,倒是不受此影响。

35、让自己习惯标准C++语言

C++标准程序库特质:(1)标准程序库中的每一样东西几乎都是template;(2)它的所有成分都位于namespace std内。

程序库的设计原则:一般性、可扩充性、可定制性、高效率、可重复运用性。

时间: 2024-10-24 07:32:46

More Effective C++读书小记的相关文章

Effective C++读书小记

1.视C++为一个语言联邦 对于内置类型而言,pass-by-value通常比pass-by-reference高效.()内置类型在按值传参时,只是将变量的值传递到栈上. 然后被调用函数将值取出后,使用即可.在按引用传参时,需要将被引用的变量的地址压栈, 然后被调用函数首先取出地址,然后再次根据地址寻址获取值.) C++可分为四个部分:(1)C:(2)Objected-Orientated C++,面向对象的设计:(3)Template C++,泛型编程的部分:(4)STL. 注意: (1)C+

Effective Objective-C 读书笔记

一本不错的书,给出了52条建议来优化程序的性能,对初学者有不错的指导作用,但是对高级阶段的程序员可能帮助不是很大.这里贴出部分笔记: 第2条: 使用#improt导入头文件会把头文件的内容全部暴露到目标文件中,而且如果两个类之间存在循环引用则会出现编译错误,所以要尽量使用@class进行类声明. 如果需要实现一个协议,则必须#improt这个协议的头文件,所以可以将协议单独定义在一个.h文件当中.如果这个协议是代理模式协议的一部分,即需要与类捆绑使用才有实际意义,则建议定义在类当中,并以类名为前

东哥读书小记 之 《一个广告人的自白》

掰着指头一算,端午假期确实完成不少事情,过的太尼玛充实鸟: 去健身房2小时,且老夫的平板支撑终于能坚持超过1分钟,普大喜奔有木有: 给合租的室友买蛋糕过了个生日: 去 去哪儿 参加W3ctech的技术交流会: 精读<一个广告人的自白>: 正在读<卓有成效的管理者>: PS:此书不是光给管理者读的,而且俺目前更不是个管理者.可,其实每个人都是 自我的管理者,管理好自己是走向卓越的必要条件. 京东上挑了N久,买了几本书.鞋.运动穿的衣裤: 规划某Topic的技术方案初稿: 平均每天刷微

Effective Java 读书笔记(2创建和销毁对象)

第一章是引言,所以这里不做笔记,总结一下书中第一章的主要内容是向我们解释了这本书所做的事情:指导Java程序员如何编写出清晰.正确.可用.健壮.灵活和可维护的程序. 2.1考虑用静态工厂方法代替构造器 静态工厂方法与构造器相比有四大优势: (1)静态工厂方法有名称,具有适当名称的静态工厂方法易于使用.易于阅读: (2)不必每次在调用它们的时候都创建一个新的对象: (3)可以返回原返回类型的任何子类型的对象: (4)在创建参数化类型实例的时候,它们使代码变得更加简洁. 同时静态工厂方法也有两大缺点

Effective java读书札记第一条之 考虑用静态工厂方法代替构造器

对于类而言,为了让客户端获取它资深的一个实例,最常用的方法就是提供一个共有的构造器.还有一种放你发,也应该子每个程序员的工具箱中占有一席之地.类可以提供一个共有的静态 工厂方法,它只是返回类的实例的静态方法. 类可以通过静态工厂方法类提供它的客户端(对象),而不是通过构造器.提这样做的好处有: 1.静态工厂方法与构造器不同的第一大优势在于,它们有名称.比如构造器BigInteger(int,int,Random)返回的BigInteger可能为素数,如果用名为BigInteger.probabl

Effective Java读书笔记(4 类和接口)

4.1 使类和成员的可访问性最小化 要区别设计良好的模块和设计不好的模块,最重要的因素在于,这个模块对于外部的其他模块而言,是否隐藏其内部数据和其他实现细节.设计良好的模块会隐藏所有的实现细节,把它的API与它的实现清晰的隔离开来,然后模块之间只通过API进行通信,一个模块不需要知道其他模块内部的工作情况,这个概念被称为信息隐藏或封装,是软件设计的基本原则之一. 4.2 在公有类中使用访问方法而非公有域 坚持面向对象程序设计思想:如果类可以在它所在的包的外部进行访问,就提供访问方法,以保留将来改

Effective Java读书笔记(3对于所有对象都通用的方法)

3.1 覆盖equals时请遵守通用约定 什么时候应该覆盖Object.equals()方法呢? 如果类具有自己特有的"逻辑相等"概念(不同于对象等同的概念),而且超类还没有覆盖equals以实现期望的行为,这时我们就需要覆盖equals方法. Object.equals()方法具有自反性.对称性.传递性.一致性和与null比较返回false的特点. 实现高质量equals方法的诀窍: (1)使用==操作符检查"参数是否为这个对象的引用".如果是,则返回true,这

Effective C++读书笔记之十二:复制对象时勿忘其每一个成分

Item 12:Copy all parts of an object 如果你声明自己的copying函数,意思就是告诉编译器你并不喜欢缺省显示中的某些行为.而编译器会对"你自己写出copying函数"做出一种复仇的行为:既然你拒绝它们为你写出copying函数,如果你的代码不完全,它们也不会告诉你.结论很明显:如果你为class添加一个成员变量,你必须同时修改copying函数.如果你忘记,编译器不太可能提醒你. 一下提供一种正确的模版: class Date{...}; class

Effective C++读书笔记之十三:以对象管理资源

Item 13:Use objects to manage resources 假设我们使用一个用来塑膜投资行为的程序库,其中各式各样的投资类型继承自一个root class: class Investment { ... };  //"投资类型"继承体系中的root class 进一步假设,这个程序系通过一个工厂函数(工厂函数会"返回一个base class指针,指向新生成的derived class 对象),供应我们某特定的Investment对象: Investment