效率c++总结 参照2011版
关于c++基本
1、将c++视为语言联邦 它有4个层次:c、面向对象、泛型、stl库
2、对于单纯常量,最好用const对象或enums替换#define enums为用户刻画了一组有范围的值
3、对于形似函数的宏,用inline代替#define
4、尽量、大胆地使用const,编译器强制实行bitwise constness(保证物理常量性),表现为在编译阶段检测有无非法的赋值语句,但编程应使用“逻辑常量性” mutable 关键字可以解决部分问题
5、当const和non-const成员函数有实质等价的实现时,令non-const版本调用const版本来避免重复代码,这需要两次强转
6、上述条款涉及到 指针常量、常量指针、const iterator、const_iterator、返回常量的函数(避免a*b=c这样的代码)、const成员函数(不可修改this指针指向的内容)等知识
7、两个成员函数如果只是常量性不同,可以被重载:const char& operator[] (int size) const 与 char& operator[] (int size), 当对象为常量时 比如 const A a,将调用const版本的成员函数,尤其在void print(const A& a) 这类函数中体现。
8、为内置型对象手工初始化,c++不保证初始化它们,构造函数最好 使用成员初值列表(在构造函数中赋值可以达到同等效果,但不高效,因为调用了成员变量的赋值构造函数),次序最好与在类中的声明次序相同。对于const 成员变量和引用型成员变量由于不能赋值,只能在初值列表中初始化。
9、“跨编译单元值初始化次序问题”,用local static对象替换non-local static 对象。(单例模式的常见实现手法),local static 是函数内部的静态变量,在使用该变量前利用包裹它的函数来初始化它,首次调用该函数能初始化该静态变量.
关于构造函数
10 编译器可能为你生成默认构造函数(没有自己声明任何构造函数时【包括带参、拷贝、赋值】)、拷贝构造、赋值构造、析构函数 这些都是public且inline的,如果bass类没有virtual析构函数,编译器生成的析构版本将是non-virtual的。
11、编译器会拒绝生成赋值构造函数,需要用户自己定义,这些情况包括:成员变量含有引用类型或const类型(因为这些变量不能被赋值,拷贝构造不会产生问题,因为它用初始化的方式为这些变量产生初值);bass类中=操作符声明为私有
12、编译器会拒绝生成拷贝构造函数,这些情况包括:bass类中拷贝构造函数为私有
13、若不想用编译器自动生成的函数,就明确拒绝:将拷贝、赋值构造函数声明为private且不实现,调用它们时将产生一个编译错误(无法访问)。但如果成员函数和友元函数调用它们,由于这些函数能访问到private,则会报链接错误(没有定义)。将链接错误移至编译期错误是可能的(这是好事):
设计一个bass类,将拷贝、赋值构造函数声明为private,去继承它们,这样友元函数和成员函数就没有访问权了。
14、为多态基类声明virtual析构函数
15、关于多态,本条是自己的总结:基类为A,子类为B 。point&A=B 时,将调用A的成员函数,如果A的成员函数是非虚的(虽然B的相同名字的成员函数是虚的),就不显现多态性,当A的成员函数是虚的,则去虚函数表去找函数入口,但是该对象的vpt依然指向B的虚函数表,故呈现多态性。
16、析构函数不应该吐出异常,这将导致程序意外终止或不明确的行为。如果析构函数可能吐出异常,应在析构函数中捕捉并吞下它们,但这样做没有提供客户处理异常的机会,解决方案是,提供一个普通函数,将可能产生异常的代码放入其中,由客户手动调用,这样就将处理异常的任务转给了客户。
17、在构造和析构期间,不应直接和间接调用任何虚函数,多态效果并不呈现:原因是构造完成前,会认为该类是base类(子类的成员变量并未被初始化),将调用base类的成员函数,一个代替方案是:在构造子类时传递必要信息给基类(在基类构造函数传参),使基类构造和子类构造有不同的结果 (比如打印信息不同)
18、令 operator= 返回 reference to *this
19、在 operator= 中处理“自我赋值”问题,这里引起两个安全问题:自我赋值安全性,异常安全性。加入“证同测试” 使其 自我赋值安全。必要的解释如下:
A& A:operator=(const A& rhs) {
if(this==&ths) return *this; // 证同测试
delete pb;
pb=new B(*ths.pb);
return *this;
}
如果没有证同测试且ths==this,将导致 *this.pb 指向被删除内存。(自我赋值不安全)
但是如果 pb=new B(*ths.pb) 引起异常,最终导致 pb指向一块被删除的内存。(异常不安全)
解决异常不安全通常也能解决自我赋值不安全: 将delete置于分配新空间之后
A& A:operator=(const A& rhs) {
B * pOrig=pb;
pb=new B(*ths.pb);
delete pOrig;
return *this;
}
或者采用 copy and swap 技术。
20、复制对象时勿忘其每个成分,尤其容易忽略的地方是,子类忘记调用基类的拷贝构造和赋值构造。
关于资源管理
21、以对象管理资源 (RAII)例子是 智能指针,给要管理的对象比如A,穿上马甲 class A_Manager{private:A* m_a;} 在构造函数传入实例 ,在析构函数中释放。
22、小心在资源管理类中的copying行为:抑制copying、引用计数法
23、提供对原始底层的资源 访问接口。可以显式的写成get成员函数,也可以隐式的重载-> 和* , 提供隐式转换函数 operater() ;
24、成对使用new 和 delete 时 ,保证使用相同的形式
25、以独立语句将newed对象植入智能指针,考虑如下代码:
A(shared_ptr<B>(new B), fun());
编译器需要做三件事:执行new B,调用 智能指针构造函数 ,调用fun()。 但是编译完成这三句的次序不定(可以肯定 2在1之后,但是3 可以在任意位置,这取决于编译器) ,当次序为 1 3 2 时且 3 发送异常,导致 2并没有置入智能指针内,将发送内存泄漏。 所以最后分开写为:
shared_ptr<B> pw(new B);
A(pw,fun());
这样编译器没有调整执行次序的自由。
设计与声明
26、让接口容易被正确使用,不易误用:
27、设计 class犹如设计type
新type对象该如何创建和销毁:关系到operator new ,operator new[],operator delete ,operator delete [] 的重载
对象的初始化和赋值的差别:决定了构造函数和赋值操作符的行为
新type对象如果按值传递:决定了拷贝构造函数的实现
新type的合法值:要求成员函数做出错误性检查
新type需要配合某个继承图系:考虑析构函数的虚拟性
什么样的操作符对新type是合理的:重载哪些操作符
哪些标准函数应该驳回:用private 去屏蔽某些 函数
谁该取用新type的成员变量:决定哪些成员是public、private、protect,friend
什么是新type的未声明接口
新type有多么一般化:决定是否模板化,泛型化
真的需要新type吗:有没有其他能完成的方案,比如多个普通函数或模板函数
28、用传常引用方式(使用const的原因是避免实参改动)代替传值规避切割问题(传值通常调用了拷贝和赋值函数,导致新的对象产生,从而新对象的ptr指向基类虚函数表丢失多态性),但是对于内置类型和stl使用传值
29、不要返回指针或引用指向 局部栈上的变量,或者堆上分配的变量,返回*this是可以的。
30、将成员变量设为priavte,主要原因是封装,屏蔽用户对内部的可见性。
31、name space 是可以跨越多个cpp的,而class 不可以
32、尽量延后变量的定义式,尽量初始化,因为通过构造函数传参比 先用无参构造函数再赋值的效率高
33、const_cast 解除常量性,dynamic_cast 安全向下转型,成本很高,static_cast 强制隐式转型 ,reinterpret_cast 执行低级转型,例如将int转为int*
尽量少做转型动作,特别是成本很高的安全向下转型。一个有趣的事实:
当base*指向derived对象时,base*的值与原本的derived*的值可能不一样,它们存在一个偏移量,这个偏移量由平台决定。另一个问题是,调用base的虚函数,多态性将起作用,即便用了强制的隐式转换。
34、避免返回handles(包括:指针、引用等)指向对象内部成分,因为可能会影响封装性,即:这样做使客户可访问内部私有数据,返回的handles也可能发生虚吊(或称为 悬空)
35、关于异常安全性:不泄露任何资源(比如某代码抛出异常,导致后面的互斥锁无法释放,或者后面的释放堆空间代码无法执行),不破坏任何数据(某代码异常,导致前面的释放堆空间代码执行后,破坏了原数据,但后面的重新分配代码无法执行) 参看第19条
36、异常安全的代码必须提供三种保证之一,按保证强到弱分为:不抛出异常,强烈保证(如果异常抛出,程序状态并不改变,保证函数要么成功要么回滚),基本承诺(抛出异常时,保证)
37、copy and swap 技术(先对原对象copy,修改copy的副本,待完全成功后,用不抛异常的swap函数交换二者)可以达到强烈保证效果,但是不是所有的函数都可以实现
38、考虑写一个高效的不抛异常的swap方法,他是第37条的基础:对类提供一个成员函数swap,以高效的方式实现它(比如 只是对 指针内容的交换),提供一个非成员函数swap(特有模板化)去调用上述成员函数的版本。
39、将大多数inline函数现在在小型的频繁调用的函数身上。inline可以隐式指出和显式指出。
40、将文件间的编译依存关系降至最低的一般构想是相依与声明式,不要相依与定义式,两个做法是handle class 和 interface class,这解释了qt库的用法。考察下列代码:
#include<A.h>
#include<B.h>
class C{
A a;
B b;
C( A ,B);
}
上述代码中,若A的定义改变了,或A.h中有任何变化,将会导致本头文件和包括本头文件的其他文件重新编译。
解决方案是:使用引用指针(handle) 代替对象,以class声明代替class定义,上述代码改变为:
class A;
class B; //前置声明
class C{
A* a; //handle 类型
B* b;
C( A ,B);
}
这样,class C 的实现cpp将包含 A.h B.h 。不会导致大规模的重新编译
面向对象
41、public 的继承关系将会以 is a 塑模
42、避免遮掩继承而来的名称,本条款与15条不要混淆: 分析的时候先利用15条,再利用本条
class A{
public:
virtual void x(){ cout<<"A::x"<<endl;}
virtual void x(int i){cout<<i<<endl;}
void y(){ x();cout<<"A::y"<<endl;}
~A(){
cout<<"d A"<<endl;
}
};
class B:public A{
public:
virtual void x(){ cout<<"B::x"<<endl;}
virtual void y(){cout<<"B::y"<<endl;}
~B(){
cout<<"d B"<<endl;
}
};
B bbb;
A*aaa=&bbb;
aaa->y(); //由15条:y()为非虚函数,不应呈现多态性,调用A::y(),A::y()中调用x(),有x()是虚函数,呈现多态性,调用B::x()
aaa->x(1); //由15条:本该呈现多态性,但B并没有重写x(int),虚函数表中依然填入A::x(int)的地址,调用A::x(int)
bbb.x(1); //由于名称覆盖,B中并没有x(int) 编译不通过
43、声明纯虚函数的目的是为了只继承接口,非纯虚的函数是为了继承接口和默认的实现(非纯虚函数提供了一个实现,但你可以用自己的实现),实函数的目的是为了继承接口和一份强制的实现(如果自己再实现一份,便会出现遮掩问题),私有的虚函数是为了仅仅继承一份实现而不继承接口。
44、绝不重新定义继承而来的实函数(non-virtual),这将发生遮掩(不会造成编译上的错误,但这表明基类的实函数可能是不必要的,或者应该是虚拟的):所以,多态性的基类析构函数如果没有写成虚函数,将违背此条款。
45、遮掩问题不仅是44条中描述的那样,另一个发生遮掩的情况如42条阐述的那样
46、绝不重新定义继承而来的缺省参数值:
class A{
virtual void x(int a=1) const=0;
}
class B:public A{
virtual void x(int a=2){};
}
当基类指针A指向B的对象时,调用x() 实际将调用x(int a=1),及默认的缺省参数没有改变,原因是缺省参数是在编译期就决定好了的,无法支持运行时的动态行为,解决方案是 在x()外面套一个函数外衣,该函数是非虚的且有缺省参数。
47、多重继承比单一继承复杂,导致了新的 歧义性 问题,以及对虚基类的需要,虚基类继承会增加大小、速度、初始化、、赋值等成本,如果虚基类不带任何数据,是最好的做法,多重继承有正确用途,更多请查阅相关资料
定制new和delete
48、set_new_handler 允许客户指定一个函数,在内存分配无法获得满足时被调用
49、利用定制的new和delete(即重载new operator) 可用于:
检测运用错误
收集统计信息
增加分配与归还速度
降低默认内存分配器带来的额外空间开销
弥补默认分配器的非最近对齐
将相关对象 集簇
获得非传统行为
50、定制new的规则:operator new 应该包含一个无穷循环,不停地尝试分配内存,如果无法满足将调用new-handler。
51、定制delete的规则:要能够处理null指针
52、写placement operator new(含有额外的参数) 时 勿忘给出对应的delete 版本,在类中写placement版本时 不要覆盖标准的new(同时给出来即可),placement delete版本只会在伴随placement new调用成功,但随后的构造函数抛出异常时调用。
53、标准的operator new 包含:
void * operator new(std::size_t) throw(std::bad_alloc);
void * operator new(std::size_t,void*) throw();
void * operator new(std::size_t,std::notrow_t&) throw();
模板与泛型
54、对类而言,接口是显示的,多态性通过虚函数技术发生于运行期,对模板而言,接口是隐式的,奠基于有效表达式,多态是通过目标具现化和函数重载,发生于编译期。
略