- Effective C++
- 视 C++ 为一个语言联邦
- C
- object-oriented C++面向对象:封装、继承、多态
- Template C++泛型编程:包括TMP(Template Metaprogramming 模板元编程)
- STL:是一个Template程序库,包括容器、迭代器、算法、函数对象
- 宁以编译器替换预处理器
尽量以 `const`、`enum`、`inline` 替换 `#define`
-
- 对于单纯常量,最好以const对象或enums替换#defines(宏定义出错时,编译器会给出替换之后的错误信息,避免因追踪变量而浪费时间)
- 对于形似函数的宏,最好改用inline函数替换#defines(#define单纯文本替换,函数语义不清晰,使用时多加小括号)
- 尽可能使用const
- const 写在T之前和之后无所谓,写在*之前表示指向常量的指针,写在*之后表示指针常量。STL的迭代器相当于一个T*指针,const_iterator相当于 const T*
- 将某些东西声明为const可以帮助编译器侦测出错误用法
使用const成员函数,以pass by reference-to-const方式传递对象,提升效率
- 两个成员函数如果只是常量性不同,可以被重载。调用时区分const.
- const成员函数不能调用non-const成员函数,当non-const和const成员函数有着实质等价的实现时,在non-const中调用const版本可以复用代码
- bitwise constness与logical constness:
bitwise constness:(C++编译器认为的cosntness)。const成员函数不改变对象的任意一个bit。bitwise constness允许发生此现象:当某个指针隶属于对象,而指针所指物不隶属于对象时,成员函数更改了指针所指物。
logical constness:一个const成员函数可以修改它所处理的对象的某些bits。
达到logical constness:可以通过mutable关键字实现,mutable修饰的变量,将永远处于可变的状态,即使在一个const函数中。mutable释放掉non-static成员变量的bitwise constness约束
- 确定对象被使用前已经初始化
- array(来自c part of c++)不保证其内容被初始化,vector(来自stl part of c++)有此保证
- 确保每一个构造函数都将每一个成员初始化
- 对象的成员变量的初始化动作发生在这些成员的default构造函数被自动调用之时,即进入构造函数本体之前。在构造函数内为成员变量赋值(xxx=y)的动作是赋值而不是初始化(相当于首先调default构造函数为成员变量设初值,然后立刻为它们赋予新值)
- 使用成员初值列(member initialization list)可以避免上述现象,初值列中针对各个成员变量而设的实参,被拿去作为各成员变量之构造函数的实参,进行copy构造。对大多数类型而言,只进行一次copy构造比上述先初始化再用拷贝赋值运算符高效许多。对内置类型而言,二者成本相同,但为了一致性,最好也采用initialization list的方式进行初始化
- 总是在初值列中列出所有成员变量,以免还得记住哪些成员变量可以无需初值。(如国内值类型成员在成员初值列遗漏了它,就没有初值,引发不明确行为)
- const或references成员变量,一定需要初值,不能赋值
- 为内置型对象进行手工初始化,因为c++编译器不保证初始化它们。
- 成员初始化次序:
base classes更早于derived classes被初始化
class成员变量按照其声明次序被初始化,即使它们在成员初值列中以不同的次序出现。
- local static对象与non-local static对象:
static对象不包括stack和heap-based对象。包括global对象,定义于namespace作用域内的对象,在classed内、在函数内、在file作用域内被声明为static的对象。
local static对象:函数内的static对象
non-local static对象:其他static对象
- 编译单元:产出单一目标文件(.o)的源码。基本上是单一源码文件加上其所含入的头文件。
问题:c++对“定义域不同编译单元内的non-local static对象”的初始化次序无明确定义。如果某编译单元内的某个non-local static对象的初始化动作使用了另一编译单元内的某个non-local static对象,它所用到的这个对象可能尚未被初始化。
解决:用local static对象替换non-loacl static对象:将每个non-local static对象搬到自己的专属函数内,这些函数返回一个reference指向它所含的对象,然后用户调用这些函数,而不是直接指涉这些对象。
- 了解c++编译器默默编写并调用哪些函数
- 如果自己没有声明,编译器就会为它声明一个copy构造函数、一个copy assignment操作符和一个析构函数。如果自己没有声明任何构造函数,编译器会为它声明一个default构造函数(如果自己有声明一个有参构造函数,则编译器不会再产出一个无参构造函数)。唯有当这些函数被调用时,它们才会被编译器创建出来。
- 编译器产出的析构函数是non-virtual的,除非该class的base class自身生命有virtual析构函数,此时这个函数的虚属性主要来自与其base class。
- 编译器产出的copy构造函数和copy assignment操作符只是单纯地将来源对象的每一个 non-static成员变量拷贝到目标对象
问题:c++并不允许将reference改指不同对象。如果打算在一个”内含reference成员”或“内含const成员”的class内支持赋值操作,必须自己定义copy assignment操作符
-
- 如果某个base classses将copy assignment操作符声明为private,编译器将拒绝为其derived classed生成一个copy assignment操作符(因为编译器为derived classes所生的copy assignment操作符想象中可以处理base class成分,但它们却无法调用base class 的assignment成员函数)
6.若不想使用编译器自动生成的函数,就要明确拒绝
- 所有编译器产出的函数都是public类型的,为阻止这些函数被创建出来,可将相应的成员函数声明为private并且不予实现。或在base class中声明private的函数然后在derived class中private继承。
- 为多态基类声明virtual析构函数
- 只有当class内至少一个virtual函数时,为它声明一个virtual析构函数
- 任何class只要带有virtual函数都应该有一个virtual的析构函数(当derived class对象经由一个base class指针被删除,而该base class带着一个non-virtual析构函数,其结果未定义——实际执行时通常发生的是对象的derived成分没被销毁)
- 如果class无virtual函数(通常表示不意图作为一个基类),则不用virtual的析构函数(vptr指针会使对象体积增加,同类型(例如c++中 Point类,c中自定义的Point类型)不能再传递至其他语言所写的函数,除非明确补偿vptr)
- virtual函数的实现:
为实现virtual函数,对象必须携带一些信息(主要用于在运行期决定哪一个virtual函数该被调用),这份信息有vptr(virtual table pointer)指针指出。vptr指向一个由函数指针构成的数组(vtbl virtual table),每个带有virtual函数的class都有一个vtbl。当对象调用某一virtual函数,编译器在该对象的vptr所指的那个vtbl中寻找适当的函数指针。
- 析构函数运作顺序:最深层派生(most derived)的那个class其析构函数最先被调用,然后是其每一个baseclass的析构函数被调用
- STL容器的析构函数均是non-virtual,所以不能继承包括STL容器在内的任一个带有non-virtual析构函数的class
- pure virtual 析构函数:当希望拥有一个抽象class,但手上没有合适的pure virtual函数是,可以为该class声明一个pure virtual析构函数。因为该class有一个pure virtual函数,所以它是抽象class,又由于它有个virtual析构函数,所以不需担心析构函数的问题。
8.别让异常逃离析构函数
- 析构函数吐出异常,程序可能过早结束或引发不明确行为。(假设对象含有多个元素,在析构第一个元素时发生异常,但其他9个元素也应该被销毁(否则会内存泄露),在析构第二个元素时又发生了异常,两个同时作用的异常导致不明确行为)
- 当析构函数中调用可能引发异常的函数时,析构函数可以:
- 如果该函数抛出异常就结束程序,通常通过调用abort函数完成
try(可能引发异常得函数)
catch(…){
// 记录信息之类的操作
std::abort();
}
- 吞下因函数调用失败引发的异常 // 不推荐
在catch(…){//不调用abort}
- 重新设计可能抛出异常的函数,使调用者有机会对可能出现的问题做出反应。
如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数(而非在析构函数中)执行该操作。
9.绝不在构造和析构函数中调用virtual函数
- base class构造期间virtual函数绝不会下降到derived class阶层(在derived class对象的base class构造(析构)期间,对象的类型是base class而不是derived class)
- 一旦derived class析构函数开始执行,对象内的dericed class成员变量便呈现未定义值
- 如果class有多个构造函数,每个都要执行某些相同的工作,可以将共同的初始化代码放进一个初始化函数中
- 当希望在base class中构造时需要调用适当版本的函数时,可以在base class中调用non-virtual函数,然后要求derived class的构造函数传递必要信息给base class构造函数
10.令operator= 返回一个reference to *this
包括+=、-=、*=等操作符也是一样,返回一个reference to *this 可以实现连续赋值
类名& operator=(const 类名& rhs){
return *this; //
}
11.在operator=中处理自我赋值
当
先进行证同测试 if(this == rhs) return *this;
- 类名& operator=(const 类名& rhs){
// 先记住原本的成员指针变量 //这样在new失败的情况下不会删除原本的内存
// 令原先的指针变量 = new T( );
// delete 记住的
// return *this;
}
- copy and swap
类名& operator=(const 类名& rhs){
类名 temp(rhs);
swap(temp);
return *this;
}
12.复制对象时勿忘其每一个成分
- copying函数应该确保复制”对象内的所有成员变量”及“所有base class成分”(调用base class copying函数)
不要尝试以某个copying函数实现另一个copying函数(即不能用operator=调用拷贝构造函数或者相反),应该将共同机能放进第三个函数中并由两个copying函数共同调用。