effective C++笔记
第一部分
第一条:C++语言包括C部分、C++面向对象部分、C++模板、STL(标准模板库)四个部分。
C部分: 有代码块、语句、预编译、内置数据类型、数组、指针等,没有重载、模板、异常
C++面向对象部分: 构造函数、析构函数、封装、继承、多态、virtual(动态绑定)等
模板: 模板元编程
STL: 是一个库,包括容器、迭代器、算法、函数对象
第二条:使用const、enum替换#define定义的常量的宏,inline替换#define的类似函数的宏
#define TEST (1.653)//少用 #define AuthorName ("TOM")//少用 #define CALL_WITH_MAX(a,b) func((a)>b?(a):(b))//func为一函数 一、使用const替换 //1.全局的 const int TEST=1.653; const char* const AuthorName="TOM"; //或者 const std::string AuthorName("TOM); //2.class里的 //GamePlayer.h中 class GamePlayer { private: static const int NumArrays=5;//声明 //或者static const int NumArrays;//声明 int score[NumArrays]; }; //GamePlayer.cpp const int GamePlayer::NumArrays;//定义 //或者const int GamePlayer::NumArrays=5; 二、使用enum替换 class GamePlayer { private: enum{ NumArrays=5};//声明 int score[NumArrays]; }; 三、使用inline替换 int a=5,b=0; CALL_WITH_MAX(++a,b)//a将累加两次 CALL_WITH_MAX(++a,b+10)//a将累加一次 //建议: template<typename T> inline void callWithMax(const T& a,const T& b) { func((a>b?a:b)); }
第三条:多使用const修饰
char greeting[]="hello"; char* p=greeting;//可变指针指向,可变数据 const char* p=greeting;//可变指针指向,不可变数据 //char const * p=greeting;//可变指针指向,不可变数据 char* const p=greeting;//不可变指针指向,可变数据 const char* const p=greeting;//不可变指针指向,不可变数据 //const在*号左边数据不可变,在*号右边指向不可变
1.令函数的返回值为常量或不可变,可以在函数的返回类型前加const,减少bug
如:
class Rationnal(...); const Rationnal operator*(const Rationnal&lhs,const Rationnal&rhs); Rationnal a,b,c; if((a*b)=c){...}//报错,原意想if((a*b)==c){...}
2.const成员函数与非const成员函数可以被重载,const对象会专门调用const成员函数。
3.有流派认为const函数不应该更改对象内任何一个bit(或者任何非静态成员变量),编译器就是这样实现的;另一派认为const函数可以更改对象内某些bit,但只有在客户端侦查不出的情况。
4.使用mutable解决const与non-const冲突问题.
5.使用static_cast将non-const对象转换为const对象,const_cast将const转除。non-const 版本经过安全转型后调用const版本的成员函数以避免代码重复,但不能用const版本调用non-const版本的成员函数来避免代码重复,注意要谨慎使用static_cast、const_cast。
第四条:确定对象使用前先被初始化
对于C part of C++内置类型,必须手动初始化
对于对象类型,由构造函数初始化,确保每一个成员都初始化。
对于C part of C++的内置类型成员变量的初始化在进入构造函数前,这样效率高,内置类型也建议用初始化的方法。在构造函数内给这些成员变量赋初值是晚一点的。基类的成员变量先初始化
非本文件的对象初始化次序无法保证,可以使用单例模式,保证其使用的对象已经被完全初始化了。以下例子在多线程环境中是不安全的
第二部分:
第五条:C++默认编写并调用了那些函数
如果没有显示声明,编译器会声明一个copy构造函数、一个copy assignment("=")操作符和一个析构函数、default构造函数,这些函数都是public和inline的,当这些函数被创建时才会被编译器创建出来。
当你创建了构造函数,编译器将不会再生成默认的拷贝构造函数。
当一个类内含reference成员或内含const成员,要支持copy assignment操作符,必须自己定义。编译器无法创建默认的copy assignment操作符。
当基类将copy assignment操作符声明为private,编译器将拒绝为继承的子类生成默认的copy assignment操作符。
第六条:若不想使用编译器自动生成的函数,就该明确地拒绝。
当想让其他人不能调用该类的copy构造函数和copy assignment函数,将这两个函数声明为private,且不去实现这两个函数。当member函数或friend函数尝试调用这两个函数时,将会链接错误。也可以将链接错误移至编译错误,将基类的copy构造函数和copy assignment函数声明为private,且不去实现它,子类继承这个基类,且该子类不声明copy构造函数和copy assignment函数。
第七条:为多态基类声明virtual析构函数
使用工厂模式创建类,当delete返回的堆对象时,基类的析构函数没有声明为virtual,且继承的子类的删除是由基类指针被删除的,则基类调用了析构函数,完全删除了,但未调用子类的析构函数,可能子类部分存在未删除的东西。
可以将基类的析构函数声明为virtual
如果一个类不含virtual 通常表示它并不想被用作一个基类。当一个类不会用来做基类时,声明其析构函数为virtual是错误的。
可以将析构函数声明为纯虚析构函数,这样它既是一个抽象类,也有virtual析构函数,但你必须为它的析构函数提供定义。
最深层的派生类析构函数最先调用,然后每一个基类的析构函数调用,与构造函数的顺序相反。
并非所有类适合多态用途,std::string和STL容器不能作为基类以继承使用。或者,某些类是作为基类,但不是为了多态使用,也就是并非被设计用来经基类的接口处理子类的对象。
第八条:别让异常逃离析构函数
C++允许析构函数抛出异常,但不最好不要在析构函数中抛出异常,因为用这个类的客户无法第一时间处理这个异常。
先让使用者捕获close这样的异常做出处理,但如果他们不处理,则由类的析构函数捕获异常且吐下异常或结束程序。
第九条:绝不在构造或析构过程中调用virtual函数
某些时候,需要在构造函数或析构函数调用某些功能的函数,而这些功能函数是虚函数或纯虚函数。如下
或者这样
以上例子是说明构造函数调用虚函数或纯虚函数,或者析构函数调用了成员函数,而成员函数又调用了虚函数或纯虚函数,要求使用对象内部未初始化成分,是产生不明确的行为的,在基类构造函数早于派生类,而对象的类型属于基类,则被调用的函数是属于基类的,这都体现在编译和链接阶段。
析构函数中,一旦析构函数开始执行,派生类对象的成员变量呈现未定义值,进入基类的析构函数后,该对象就成为一个基类对象了。
解决办法:
将基类原本的virtual函数声明为non-virtual,派生类的构造函数必须传递与该non-virtual函数相关参数给基类的构造函数,向基类“向上调用”避免不确定情况发生。
第十条:让operator返回一个reference to *this
实现连锁赋值
即:a=b=c=15;
在类中,赋值操作符必须返回一个reference指向操作符的左实参。内置类型和标准库(string、vector、complex、tr1::share_prt)都遵照这样的准则。
也适用于其他赋值运算
第十一条:在操作符=中处理自我赋值
//以下存在隐性自我赋值 class_a[i]=class_a[j]; *p_classA1=*p_classA2;
在操作符=的函数中,一般会先判断指针是不是相等,相等则直接返回,否则先删除旧的,再构造新的对象,但这种方法不妥,因为new 对象可能会异常。
可以用以下方法更好:
(1)指针或引用传值时,先new再删除
(2)指针或引用传值时,使用交换技术
(3)使用值传实参
第十二条:复制对象时勿忘其每一个成分