Effective C++ 学习笔记
- 一、习惯C++
- 1、C++是一个语言的组合
- 2、尽量使用const、enum、inline替换#define
- 3、尽可能的使用const
- 4、对象使用前要初始化
- 二、构造、析构和赋值运算
- 5、C++默认生成的函数
- 6、不想要编译器自动生成的函数,要明确约束
- 7、多态基类析构函数必须声明为virtual
- 8、析构函数不要抛出异常
- 9、构造和虚构函数中不要调用virtual函数
- 10、operator=返回自身的引用
- 11、实现安全的赋值运算符
- 12、复制对象时需要复制每一个成分
- 三、资源管理
- 13、以对象管理资源
- 14、RAII对象的拷贝行为
- 15、RAII类中提供对原始资源的访问
- 16、new和delete要以相同方式使用
- 17、使用独立语句将new的对象置入智能指针中
- 四、设计与声明
- 18、让接口更容易使用,不易被误用
- 19、如何高效设计类
- 20、const引用传递优于按值传递
- 21、不要错误的返回对象引用
- 22、将成员变量声明为私有(private)
- 23、非成员函数、非友元函数替换成员函数
- 24、如果所有参数都需要类型转换,那么使用非成员函数
- 25、自定义swap函数
- 五、实现
- 26、尽可能延后变量的定义
- 27、尽量少做类型转换
- 28、避免返回对象成员变量的引用
- 29、尽量实现异常安全的代码
- 30、透彻理解inline
- 31、最小化文件依赖关系
- 六、继承与面向对象设计
- 32、确定public继承建模出is-a关系
- 33、避免遮掩继承而来的名称
- 34、区分接口继承和实现继承
- 35、考虑虚函数的替代方案
- 36、不要重写继承来的非虚函数
- 37、不要修改重写函数的默认参数的值
- 38、通过组合实现出has-a的关系
- 39、明智的使用private继承
- 40、明智的使用多重继承
- 七、模板与泛型编程
- 41、了解隐式接口和编译器多态
- 42、了解typename的双重含义
- 43、处理模板化基类内的名称
- 44、将与参数无关的代码脱离模板
- 45、使用成员函数模板接受所有兼容类型
- 46、需要类型转换时为模板定义非成员函数
- 47、使用traits类表现类型信息
- 48、模板元编程
- 八、自定义new和delete
- 49、了解new-handle的行为
- 50、了解new和delete的替换时机
- 51、自定义new和delete应该遵守的规则
- 52、实现了替代new也要是配套的delete
- 九、其他
- 53、不要轻视编译器的警告
- 54、熟悉TR1在内的标准程序库
- 55、熟悉Boost库
一、习惯C++
1、C++是一个语言的组合
C++可视为一个语言联邦,包含c语言;C++特性封装、继承、多态等功能;模块和STL。
2、尽量使用const、enum、inline替换#define
- #define定义的信息不会被记录在符号表中,当出现错误时难以定位对应的具体宏。
- 对于浮点常量,使用const定义比使用宏可以减少目标码的量,因为宏定义会有多份替换,而常量只有一份。
- const可以定义类的专属常量,而#define不能。
- 使用inline函数替换#define定义的函数,#define的函数常会因为使用方式不当而得不到预期结果,且难以排查。
3、尽可能的使用const
- 对于明确其值或其地址不应该被改变时,就需要使用const使其明确表达出其为常量、指针常量(指针所指地址的值不可以被改变)或常量指针(指针指向的地址不可以改变),可以帮助编译器检测出错误的用法。
- const出现在号的左边表示其为指针常量,const出现在号右侧表示其为常量指针
- const修饰函数参数约束函数内参数被改变,const修饰返回值可避免客户修改返回值而引发错误
- 类成员函数后加const修饰可以防止避免函数内修改成员变量
- 当常量成员函数 和非常量成员函数相同时,用非常量成员函数调用常量成员函数来避免代码重复
4、对象使用前要初始化
- 内置类型手动初始化,内置类型以外的在构造函数中初始化
- 使用初始化列表初始化,可保证进入构造函数体之前被初始化;赋值操作则需要在函数体内执行。初始化列表比赋值效率高
- 多个构造函数具有相同初始化列表时,可以将使用初始化列和赋值操作效率相等的变量提取到一个私有函数内,多个构造函数调用私有函数初始化,以此来减少代码重复。
- 初始化列表初始化成员变量时,此时与其在类中的定义顺序一致。
- 跨单元使用变量时,以静态成员变量的方式使用,避免因初始化顺序引起未知的异常。
二、构造、析构和赋值运算
5、C++默认生成的函数
- 当列没有定义无参构造函数时,创建对象会默认生成默认构造函数。析构函数、拷贝构造函数、移动构造函数、移动赋值和拷贝赋值函数同理
- 默认生成的拷贝构造函数和拷贝赋值函数实现的为浅拷贝,当类中含有堆中分配的内存时会有拷贝后,堆中数据将不会被拷贝。
- 默认生成的析构函数为非虚析构函数,在含有继承关系的对象析构时可能会造成内存泄漏。
- 当类成员变量中有常量时,编译器不会默认生成拷贝赋值和移动赋值函数
- 当类成员变量中有引用时,编译器不会默认生成默认拷贝构造和移动构造函数。
- 如果类成员变量中有不支持拷贝和赋值的成员,如mutex时,编码器则不会默认生成拷贝构造、移动构造、拷贝赋值和移动赋值函数。
6、不想要编译器自动生成的函数,要明确约束
- C++98中可以通过将默认构造函数、拷贝构造函数声明为私有类型类来避免默认生成。C++11中可以在函数后加=default来约束其不能默认生成。
7、多态基类析构函数必须声明为virtual
- 避免在基类指针指向派生类对象时,使用基类指针释放资源造成资源泄漏。(具有多态性质的基类一定要定义析构函数为virtual)
- 对于不具有多态性质的类,不要将其析构函数定义为virtual
8、析构函数不要抛出异常
- 虚构函数抛出异常会导致对象数组释放时,出现异常之后的对象资源无法释放,造成资源泄漏。
- 可以在析构函数中通过try…catch语句吞掉异常或者结束程序。
- 提供操作函数给客户,使客户可以对异常进行处理,而不是在析构函数中处理
9、构造和虚构函数中不要调用virtual函数
- 构造函数初始化的顺序为先构造基类,再构造派生类,如果基类构造函数中调用virtual函数,此时派生类的变量还没有初始化,基类却在使用则会出现未定义的行为。
- 析构函数的调用顺序为先调用派生类的析构函数,再调用基类的析构函数,如果在基类析构函数中调用多态函数,会出现派生类成员变量已经析构了,基类却在使用则会出现未定义行为。
10、operator=返回自身的引用
- 返回自身的引用可以可以实现连续赋值的操作
- 同理也适用与+=、-=等操作符的重载
11、实现安全的赋值运算符
- 避免自我赋值时出现异常
12、复制对象时需要复制每一个成分
- 派生类的拷贝构造函数、移动构造函数、拷贝赋值函数和移动赋值函数要明确指定调用基类的构造函数或赋值函数(即要拷贝或赋值派生类和基类的所有成分),否则会出现基类中变量未被构造或赋值的情况。
三、资源管理
13、以对象管理资源
- 利用对象的作用域来管理资源,即创建一个类,在类的构造函数内创建资源,在析构函数中释放资源。
- 在定义对象时自动申请资源,离开作用域时,自动释放资源。以此来避免资源泄漏的发生。
14、RAII对象的拷贝行为
- 对于RAII对象行为,禁止拷贝、移动所有资源管理权、使用引用计数进行管理、拷贝底层资源(深拷贝),对于不同对象采用不同行为。
15、RAII类中提供对原始资源的访问
- 每个RAII类中都应该提供一个获取原始资源的接口
16、new和delete要以相同方式使用
- 使用new申请的资源要以delete释放,以new[]申请的资源要以delete[]释放
- 不要使用对数组形式使用typedef,然后使用new申请,此时使用delete释放会出现内存泄漏
17、使用独立语句将new的对象置入智能指针中
- 如果new为非独立语句,那么一旦其他语句有异常抛出会导致new的对象不能被正常释放,造成资源泄漏
四、设计与声明
18、让接口更容易使用,不易被误用
- 以接口一致性,以及与内置类型的行为兼容来促进正确使用接口
- 通过建立新类型、限制类型上的操作、束缚值对象,来消除客户的资源管理责任。
- shared_ptr支持定制型删除器。可以防范一个DLL中申请资源,另一个DLL释放资源的问题。还可被用来自动解除互斥锁。
19、如何高效设计类
- 新的对象应该如何被创建和销毁,涉及构造函数和析构函数,以及内存如何的分配和释放
- 理解对象的初始化和赋值的区别,不可混淆
- 对象以值传递应该注意的事项,如何定义以值传递的拷贝构造函数和移动构造
- 对成员变量如何正确检测其值的合法性
- 对象是需要使用到继承,需要继承的话析构函数需要设置为virtual。或被继承的类也要将析构函数设置为virtual
- 是否需要设置特殊的类型转换接口,需要显示转换的则定义显示转换函数,不支持隐式转换的添加explicit进行限定。
- 考虑类需要哪些操作运算符
- 不该使用的标准函数,要明确编译器不能自动生成
- 明确成员变量和成员函数的访问范围,即是public、private或protected
- 如果类对外有线程安全、异常安全等承诺,那么就要在类内实现其承诺的约束
- 新类是否通用,可在类和模板类之间选择
- 确定是否必须要定义一个新的类,是否可以通过实现非成员函数或模板函数来实现扩展。
20、const引用传递优于按值传递
- const引用传递效率更高,不会调用构造析构函数增加开销
- 可以避免出现切片情况发生,即需要多态时,按值传入时会导致多态失效
- 内置类型、STL迭代器和函数对象按值传递比较合适
21、不要错误的返回对象引用
- 不要返回指针或引用指向一个局部栈上的对象,或者指向堆上的对象;栈上的对象返回后会被释放掉,堆上的对象不知道应该在什么地方被释放。
22、将成员变量声明为私有(private)
- 成员变量定义为private可以保持客户访问数据的一致性、可细微划分访问控制、使约束条件获得保证,并提供类以充分的实现弹性
- protected并不比public更具封装性
23、非成员函数、非友元函数替换成员函数
- 对外接口越少,封装程度越高;对外接口越多,封装程度越低
- 封装程度越高,类就会有越大的弹性去改变;封装程度越低,类就会有越小的弹性去改变。
- 可以提高封装性、包裹弹性和功能扩充性
24、如果所有参数都需要类型转换,那么使用非成员函数
- 如果需要为某个函数的所有参数(包括被this指针所指的隐喻参数)进行类型转换,那么这个函数必须是个非成员函数
25、自定义swap函数
- 当std::swap效率不高是,需要提供一个swap成员函数,但要确定其不会抛出异常
- 如果提供一个swap成员函数,那么应该提供一个非成员函数swap去调用成员函数swap。对于类而言要特例化std::swap
- 使用std::swap时,要先取消命名空间再使用,而不要直接使用std::swap
- 不要尝试在std中加入全新的东西
五、实现
26、尽可能延后变量的定义
- 可以增加程序的清晰度并改善程序的运行效率
27、尽量少做类型转换
- const_cast:通常用来将对象的常量性移除(唯一能消除常量特性的转型);dynamic_cast:运行时用于安全向下转型,即基类指针转派生类指针,开销比较高;reinterpret_cast:将指针指向的内容重新解释为一种类型,主要用于指针与指针或指针与整形间转换;static_cast:用来强制隐式转换,非常量转常量、int转double、void*转具体类型等。
- 尽量避免使用转型,特别注重效率的程序不要使用dynamic_cast,使用其他方式进行代替。
- 尽量使用c++类型转换替换c的强制类型转换
28、避免返回对象成员变量的引用
- 避免外部修改对象的内部变量,可以增加封装性
29、尽量实现异常安全的代码
- 当异常抛出时,异常安全性的函数不会造成资源的泄露,不会破坏原有数据。
- 异常安全函数会提供三种保证之一。基本承诺:出现异常前后程序内的任何事物都保持在有效的状态下。强烈保证:函数执行要么成功,要么失败。成功则是完全成功,失败则会恢复到调用函数前的状态。不抛出异常保证:不会抛出任何异常
- 强烈保证一般可以使用copy and swap的方式实现,即创建一个副本进行修改当完全修改成功后再使用swap将swap内容替换到目标对象中,以此来实现异常安全。
- 异常安全函数的异常安全保证最高取决于异常安全最弱的函数。
30、透彻理解inline
- 将大多数inline限制在小型的、被频繁调用的函数上,可使调试过程和二进制升级更容易,也可使潜在的代码膨胀问题最小化,使程序的速度提升机会最大化
- inline函数具体会不会被inline取决于编译器
- 头文件中声明并定义函数不适用inline时,如果在其他多个文件中引用次头文件,编译时会提示函数重定义。头文件中的函数加上inline修饰符则不会出现函数重定义问题
- inline修饰static变量时可以直接进行赋值(C++17支持,之前版本不支持)
- 运用在命名空间嵌套中进行版本控制,可以直接展开命名空间
#define VERSION 2
namespace lib
{
#if VERSION > 1inline
#endifnamespace lib_one{void func(){printf("lib_one");}};
#if VERSION <= 1inline
#endifnamespace lib_two{void func(){printf("lib_one");}};
};
using namespace lib;
int main()
{func();//此处会调用lib_one下边的函数func(),必须要使用lib_one::func()进行调用
}
31、最小化文件依赖关系
- 使用include包含头文件时,如果修改了头文件那么编译项目时就需要编译所有涉及改变头文件的头文件,造成编译效率低。可以使用类的前置声明而减小依赖关系。
- 可以使用引用或指针时,就不要使用对象;靠声明式就可以定义出引用或指针;但是对象,必须使用到对象类型的定义式。
- 声明函数时,使用到某个类时,只需要声明式即可,不需要定义式。
- 尽量将函数声明式放进一个头文件中,提供给客户使用。
六、继承与面向对象设计
32、确定public继承建模出is-a关系
- public继承要保证基类中的每一个成分都能应用到派生类中。
33、避免遮掩继承而来的名称
- public继承情况下,当派生类中重写基类中被重载的函数时,会造成基类中的被重载的函数被遮掩,即基类中被重载的函数无法正常使用。但可以在派生类中使用using Base::name(base为基类名称,name为重载的函数名)此方式使基类中重载的函数在派生类中可见。
- 在private继承情况下,可以在派生类中重写基类函数,并在函数内指明基类被重写的函数(Base::name,base为基类名称,name为重载的函数名),从而达到将基类中重载的函数私有化。
34、区分接口继承和实现继承
- 纯虚函数的目的是为了让派生类只继承接口;虚函数的目的是为了使派生类继承接口和缺省实现;非虚函数目的是为了使派生类继承函数的接口和强制实现。。
- 不要将没有虚函数的类作为基类,也不要将基类的所有函数设置为虚函数。
35、考虑虚函数的替代方案
- 使用NVI(non-virual interface)手法替换虚函数,是模板方法设计模式的一种特殊形式,以非虚函数封装private或proctected的虚函数
- 虚函数替换为函数指针,即策略模式的一种实现方式
- 仿函数成员变量替换虚函数,即策略模式的一种实现方式
36、不要重写继承来的非虚函数
- 派生类重写基类废墟函数后,使用基类或派生类对象指针调用被重写的函数会表现出不一样的现象(基类指针调用执行的时基类的函数,派生类指针调用执行的为派生类的函数);如果为虚函数那么用基类或派生类对象指针调用被重写的函数时会表现一致的现象。
37、不要修改重写函数的默认参数的值
- 编译阶段会确定默认参数的值(静态绑定),但是虚函数的调用是在运行时才决定的(动态绑定),因此在多态的情况下会表现出不一样的结果。
- 可以使用NVI手法解决此问题,将默认参数提取出到基类中的非虚函数中。
38、通过组合实现出has-a的关系
39、明智的使用private继承
- private继承会将基类中的所有public和protected属性变为private,会破坏is-a的关系。
- 编译器不会将派生类的对象转换为基类对象,而public继承则相反。
- private继承可以造成empty base最优化,可以实现对象空间最小化。
40、明智的使用多重继承
- 多重继承会出现菱形关系导致二意性,可以采用虚继承的方式解决二意性
- 虚继承会增加类的大小、速度和初始化的复杂度。可考虑空基类策略(EBD)
七、模板与泛型编程
41、了解隐式接口和编译器多态
- 对类而言接口是显示的,多态是通过virtual函数在运行期而发生的。
- 对模板而言接口是隐式的,多态是通过具体化和函数重载在编译期而实现的。
42、了解typename的双重含义
- 声明模板时,使用class和typename关键字效果一样。
- 使用typename可以解决模板嵌套问题。但是不能再基类列或初始化列中使用。
43、处理模板化基类内的名称
- 模板类有继承时,编译期会出现模板类中调用基类的函数不存在的问题。但可以通过三种方式解决此问题
1)在基类函数调用前添加this->
2)使用using命令告诉编译器函数位于基类中
3)直接使用域作用符(::)调用基类函数
44、将与参数无关的代码脱离模板
- 模板生成多个类和多个函数,以此任何模板代码不应该和某个造成膨胀的模板参数产生依赖关系。
- 因非类型模板参数造成的代码膨胀,可以通过函数参数或者成员变量的方式替换模板参数
- 因类型参数造成的代码膨胀,可以通过完全相同二进制表述的具体类型共享实现码,以此来减少膨胀。
45、使用成员函数模板接受所有兼容类型
- 模板类在基类和派生类指针间不具有隐式转换的功能。可以通过在模板类中实现成员函数模板来实现以基类来构造一个基类对象(泛化拷贝构造函数)。同时要保证只能以派生类生成基类对象,而不能以基类对象生成派生类对象。
- 生成泛化拷贝构造函数和泛化拷贝赋值操作,也需要实现正常的拷贝构造函数和拷贝赋值操作符
46、需要类型转换时为模板定义非成员函数
- 编写模板类时,如果需要与模板类相关函数支持所有参数的隐式转换的话,需要将函数定义在模板类中,并声明为友元函数。
47、使用traits类表现类型信息
48、模板元编程
- 模板元编程可以将部分运行期的工作转移到编译器,可以时某些错误在编译期就被发现
- 模板元编程可以减小可执行文件、减少运行期、减少内存的占用;但是会增加编译时间。
八、自定义new和delete
49、了解new-handle的行为
- 客户可以使用set_new_handler函数指定函数,在内存分配失败的时候被调用。
50、了解new和delete的替换时机
- 自定义new和delete的时机。
1)用来检测运用上的错误。
2)用来收集动态分配内存和使用的信息统计。
3)增加内存的分配和释放速度。
4)降低默认内存管理方式带来的空间的额外开销。
5)可以实现按字节对其方式分配内存。
6)可以将相关的对象组织在一起,可以降低内存使用时的缺页中断。
51、自定义new和delete应该遵守的规则
- operator new中应该包含一个无穷循环,并在其中尝试分配内存,如果无法满足内存需求,则调用new-handler。并且有能力处理0字节大小的内存申请。可以处理“比正确大小更大的申请”
- operator delete应该在收到null指针时不做任何处理。可以处理“比正确大小更大的释放”
52、实现了替代new也要是配套的delete
- 当自定义一个new操作时,一定要自定义配对的delete操作。否则会出现隐含的内存泄漏问题
- 自定义new和delete操作时,一定要注意不要覆盖默认正常版本的new和delete操作。
九、其他
53、不要轻视编译器的警告
- 严肃对待编译器警告信息,努力写出无任何警告的代码。
- 不要过度依赖编译器的警告能力,不同的编译器会有差异,特别移植编译器时。
54、熟悉TR1在内的标准程序库
- 智能指针
- 函数对象tr1::function
- 绑定器tr1::bind
- 哈希表
- 正则表达式
- 变量组tuples
- tr1::array
- tr1::mem_fn
- tr1::reference_wrapper
- 随机数
- 数学特殊函数
- 类型 traits,用于提供类型的编译器信息
- tr1::result_of,用来推动函数调用的返回类型