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内。
程序库的设计原则:一般性、可扩充性、可定制性、高效率、可重复运用性。