全文参考自Effective C++, Scott Meyers
程序来自本人
/hxz1998/ccl
1. 让自己习惯C++
C++高效编程守则视状况而变化,取决于使用C++的哪一部分。
C++四大块:
CObject-Oriented C++:面向对象Template C++:泛型编程,模板元编程STL:容器,迭代器,算法,函数对象
2. 尽量以const,enum,inline替换 #define
对于单纯常量,最好使用const
或者enums
来替换#define
对于形似函数的宏(macros),最好使用inline
函数来替换#define
有一种方法,可以获得宏带来的效率,以及一般函数带来的可预料行为以及类型安全性(Type Safety),例如:
returna>b?a:b; } intmain(){ inta=callWithMax<int>(1,2); cout<endl; }template<typenameT>inlineTcallWithMax(constT&a,constT&b){
3. 尽可能使用 const
声明为const
可以让编译器帮助检查错误。const
可以施加于任何作用域内的对象、函数参数、函数返回类型、成员函数。编译器强制实施bitwise constness,但编写程序时,应该使用“概念上的常量性”conceptual constness。当const
和non-const
成员函数有实质等价的实现时,要用non-const
版本去调用const
版本,这样可减少代码重复。
const
如果出现在*
左边,那么表示被指物是常量;如果在*
右边,那么表示指针是常量;如果出现在两边,那么表示指针和被指物都是常量。例如:
inta=0,b=1; intconst*p1=&a; int*constp2=&a; *p1=2;//不行!因为 p1 指向的内容是常量 *p2=2;//可以,p2自身是常量,p2只能指向a,但是a中的内容可以变 p1=&b;//可以,p1指向另一个内容,并声称这个内容不可变 p2=&b;//不可以,p2自身是常量,不能指向其他东西了 }intmain(){
试着习惯这样的写法:
voidf2(intconst*i);//一猫猫一样voidf1(constint*i);//指向一个不能修改内容的i
这俩写法效果是一样的,都是指向一个内容不可变的数据(指针本身可以再修改指向的对象)。
对于第三点,一个很好的例子如下:
private: std::stringtext; public: constchar&operator[](std::size_tpos)const{ returntext[pos]; } char&operator[](std::size_tpos){ returnconst_cast<char&>(//使用const_cast去掉const声明 static_cast<constText&>//使用static_cast把*this转换成const对象 (*this)[pos]);//使用(*this)[]方法返回(这个时候是const结果,经过const_cast去掉const声明 } };classText{
4. 确定对象被使用前已被初始化
为内置对象进行手工初始化,因为C++并不能保证完全初始化好它们。构造函数最好使用成员初值列(member initialization list),而不要在构造函数本体内使用赋值操作(assignment)。初始列列出的成员变量,其排列顺序要和它们在类声明中的一致。为免除“跨编译单元初始化次序”问题,使用 local static 对象来代替 non-local static 对象。
如果成员变量是const
的或者references
的,那么它们一定需要有初值,不能被赋值。例如:
constintval; int&re_val; public: X(intval_,int&re_val_):val(val_),re_val(re_val_){} };classX{
基类总是比派生类要先初始化好,例如:
constintval; int&re_val; public: X(intval_,int&re_val_):val(val_),re_val(re_val_){ cout<"Xinitialization..."<endl; } }; classY:publicX{ public: Y(intval,int&re_val):X(val,re_val){ cout<"Yinitialization..."<endl; }; }; intmain(){ intre_v=1; Yy(1,re_v); //>:Xinitialization... //Yinitialization... }classX{
由于定义于不同编译单元内的non-local static
对象的初始化顺序并未明确定义,因此会出现这样情况:
定义在File1.hh
中一个静态全局变量tfs
在中使用
tfs
那么如果File1.hh
中的tfs
还没初始化好呢,中就想使用了,那么就会出现大问题!例如下面这个例子:
#include usingnamespacestd; classFileSystem{ public: size_tnumDisks()const{return0;} }; externFileSystemtfs; // #include #include"File1.hh" usingnamespacestd; classDirectory{ size_tdisks; public: Directory(){disks=tfs.numDisks();} size_tgetDisks()const{returndisks;} }; intmain(){ Directorydirectory; cout<}//File1.hh
这个时候编译器就会报错:
CMakeFiles\local_static.dir/objects.a(.obj)::(.rdata$.refptr.tfs[.refptr.tfs]+0x0):undefinedreferenceto`tfs'
很明显,在local_static
目录中没有找到该引用,因此报错了,那么该怎样做呢?
使用方法(类似于工厂方法)来获取这个值,而不是依赖编译器初始化
例如:
classFileSystem{ public: size_tnumDisks()const{return0;} }; FileSystem&getFS(){ staticFileSystemtfs; returntfs; } // classDirectory{ size_tdisks; public: Directory(){disks=getFS().numDisks();} size_tgetDisks()const{returndisks;} }; intmain(){ Directorydirectory; cout<}//File1.hh
这样一来,就不用担心了☺。
不过,这样还是有另外一个问题,例如多线程环境下还是有不确定情况,处理这种麻烦情况的做法之一是:在单线程启动阶段,手动调用一遍所有的reference-returning
方法。这样可以消除与初始化有关的“竞速形式(race conditions)”
5. 了解C++默默编写并调用哪些函数?
编译器可以暗自为
class
创建default
构造函数、copy
构造函数,copy assignment
操作符,以及析构函数。
首先,开门见山地说,C++默认编写了默认构造函数、默认析构函数、拷贝构造函数,以及拷贝赋值函数,而且它们默认都是inline
的。当然,这些函数的默认创建在一定时期是失效的,例如:
默认构造函数:当提供了一个构造函数后,编译器不再为类提供默认构造函数,而且默认。默认析构函数:当提供了一个析构函数后,编译器就不再提供默认析构函数,默认析构函数是non-virtual
的。拷贝构造函数:只要没提供,而且满足可拷贝构造的条件,那么就提供,否则不提供。拷贝赋值函数:只要没提供,而且满足可拷贝复制的条件,那么就提供,否则不提供。
上面说了两个条件,那么具体是什么条件呢?
5.1 可拷贝构造&可拷贝赋值?
先来看一下满足这俩条件的例子:
classNamedObject{ private: TobjectValue; stringname; public: NamedObject(stringn,Tval):name(n),objectValue(val){} NamedObject(constNamedObject&rhs){ objectValue=rhs.objectValue; name=rhs.name+"copy"; }friendostream&operator<constNamedObject&rhs){ os<""<returnos; } NamedObject&operator=(constNamedObject&rhs){ objectValue=rhs.objectValue; name=rhs.name+"=";return*this; } };intmain(){stringnewDog="newDog";stringoldDog="oldDog";NamedObject<int>od(oldDog,1);NamedObject<int>nd(newDog,2);cout<":"<endl;//>:2newDog:1oldDog nd=od;cout<//>:1oldDog= }template<typenameT>
那么此时,即便自己不提供拷贝构造以及拷贝赋值构造操作符,编译器也会对成员变量进行递归的拷贝赋值过来。但是在遇到成员变量是const
或者reference
类型时,编译器就两手一摊,无能为力了(具体可参考Effective C++, 3th, P37)。
例如下面的例子:
classNamedObject{ private: constTobjectValue; string&name; public: //其他函数都一样 NamedObject(constNamedObject&rhs){ objectValue=rhs.objectValue; name=rhs.name+"copy"; } NamedObject&operator=(constNamedObject&rhs){ objectValue=rhs.objectValue;//不能对一个 const 对象赋值! name=rhs.name+"=";return*this; } };intmain(){stringnewDog="newDog";stringoldDog="oldDog";NamedObject<int>od(oldDog,1);NamedObject<int>nd(newDog,2); nd=od;//>:error:useofdeletedfunction'NamedObject&NamedObject::operator=(constNamedObject&)' }template<typenameT>
当然,这只是编译器不再提供了而已,用户自己还是可以设计如何去复制拷贝以及构造拷贝的,这完全取决于自己怎么处理成员变量。
除此之外,如果基类把拷贝构造函数设置成了private
那么在派生类中也是没办法操作的。
6. 若不想使用编译器自动生成的函数,那该明确拒绝
为驳回编译器自动(暗自)提供的机能,可将相应的成员函数声明为
private
并且不予实现。或者使用继承Uncopyable
这样的基类。
如果不想让一个类支持拷贝构造或者赋值构造,那么我们可以将函数声明但不实现,例如这样子:
private: Uncopyable(constUncopyable&); Uncopyable&operator=(constUncopyable&); };classUncopyable{
当然,对于每一个想实现这个功能的类都能去单独这样声明,不过,还可以使用继承方法去实现,例如:
protected: Uncopyable()=default; private: Uncopyable(constUncopyable&); Uncopyable&operator=(constUncopyable&); }; classSubClass:publicUncopyable{ //默认不允许拷贝构造和赋值运算符 }; intmain(){ SubClasss1,s2; s1=s2;//error! }classUncopyable{
7. 为多态基类声明virtual析构函数
带多态性质的基类应该声明一个virtual
析构函数。如果类带有任何virtual
函数,那么它就应该拥有一个virtual
析构函数。如果类的设计目的不是用来做基类的,那么就不应该声明virtual
析构函数。
当使用基类指针指向派生类对象时,没有问题,但是要是想把这个基类指针给删掉,这时候问题就来了,例如下面这个例子:
private: char*name; public: BaseClass(intsize){ name=newchar[size]; for(inti=0;i'a'; } //基类的析构函数 ~BaseClass(){deletename;} }; classDeriveClass:publicBaseClass{ private: char*count; public: DeriveClass(intsize):BaseClass(size){ count=newchar[size]; for(inti=0;i'b'; } //派生类的析构函数 ~DeriveClass(){deletecount;} }; intmain(){ //多态用法,基类指针指向派生类对象,没毛病 BaseClass*obj=newDeriveClass(16); //删除基类指针,出现了问题! deleteobj; return0; }classBaseClass{
上面的程序乍一看看不出个毛病来,现在对BaseClass *obj = new DeriveClass(16);
设置断点,进行单步调试,可以观察到构造函数过程是:
| BaseClass(16) | DeriveClass(16) | endnewDeriveClass(16)
这个顺序完全正确,先构造基类再构造派生类嘛,执行完后,内存状态是这样的:
可以得知操作系统给这两个对象中的成员分配内存到了name : 0x1061980
和count : 0x10619c0
。
那么执行delete obj;
时,顺序是这样的:
| ~BaseClass() | enddeleteobj
从上面可以看出来,竟然只执行了基类的析构函数,而没有执行派生类的析构函数,那么这时的内存表示是怎么样的?见下图:
由此可见,在不经意间,就造成了内存泄漏问题,那么该如何解决这个问题呢?
很简单,只需要把基类的析构函数声明为virtual
就可以了,这样强制去执行子类的析构函数。
不过,这样还是有两种结果,例如下面是一种结果:
//其他都一样 virtual~BaseClass(){deletename;} }; classDeriveClass:publicBaseClass{ //其他都一样 ~DeriveClass()override{deletecount;} };classBaseClass{
这个时候,是先执行的派生类析构函数,再执行基类析构函数。
另一种结果是:
virtual~BaseClass(){deletename;} }; classDeriveClass:publicBaseClass{ //删掉了自己的析构函数 };classBaseClass{
这个情况下,才是先执行基类析构函数,再执行派生类析构函数。
8. 别让异常逃离析构函数
析构函数绝对不要抛出异常,如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下他们(不传播)或结束程序。如果接口使用者需要对某个操作函数运行期间抛出的异常做出反应,那么class
应该提供一个普通函数(而不是在析构函数中)执行操作。
即便C++允许析构函数抛出异常,但是最好不要这样做。当然,吞掉异常也是有争议的,比如“草率地结束程序”可能会带来更严重的问题,或者“不明确的行为带来的风险”可能会带来不安全的问题等等,具体问题具体分析是比较好的。
但是,通常可以提供一个让用户在析构函数前控制异常的机会,例如使用“双重保险”来尽最大化确保问题得到解决。
9. 绝不在构造函数和析构函数过程中调用virtual函数
在构造和析构期间不要调用
virtual
函数,因为这类调用从不下降至派生类。
不管怎样,都不应该在构造函数和析构函数内部去调用一个virtual
函数,因为这样的操作是不可预估的,带来意想不到的结果。为什么这样?因为在基类中,构造函数执行阶段或者析构函数执行阶段只能看到基类的内容,所以在派生类中实现的程序,是不可用的。下面这句话直白且有效的指出了问题的所在:
在基类(base-class)构造期间,
virtual
函数不是virtual
函数。
也正是因为这样一个“对象在derived class
构造函数开始执行前,不会成为一个derived class
对象”的规则,所以最好在构造期间对virtual
函数视而不见。
那么如何科学有效地去解决这个问题?当然是在基类中把需要在构造函数内执行地函数设置成非virtual
函数。
总之就是,在基类构造和析构期间调用的virtual
函数不可下降至派生类。
10. 令 operator= 返回一个 reference to *this
令赋值(assignment)操作符(
=
)返回一个 reference to*this
。
为什么这样做呢?是因为可以实现类似于这样的程序:
a=b=c=10;
因此,我们在编写类的operator=
操作符时,可以写成:
public: BaseClass&operator=(constBaseClass&rhs){ //随便干点什么 return*this;//关键在于这里 } };classBaseClass{
当然啦,也可以不做返回,不过既然这是一个好的实践,那么没有确切的理由不去做,最好就去做。
11. 在 operator= 中处理“自我赋值”
确保当对象自我赋值时,operator=
有可预估的行为。其中需要注意的包括比较“来源对象”和“目标对象”的地址、精心周到的语句顺序以及拷贝交换。确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。
简而言之,就是需要考虑操作符两边是否是同一个对象,因为如果是同一个对象,会出现类似下面的问题:
private: char*data; public: BaseClass&operator=(constBaseClass&rhs){ deletedata; data=rhs.data; return*this; } }; intmain(){ BaseClassbaseClass; baseClass=baseClass; }classBaseClass{
自己给自己赋值,没毛病,但是在运算符函数的delete data
却带来了问题,因为它删除掉了自己的内存空间,却在下面那行data = rhs.data
又想用了,而这时系统已经收回了这块空间,这样一来操作系统肯定是不干的,所以程序就报错了。
那么该如何解决呢?这样:
private: char*data; public: BaseClass&operator=(constBaseClass&rhs){ //多一个检查是否是自己的操作就可以了,也称证同测试 if(&rhs==this)return*this; deletedata; data=rhs.data; return*this; } };classBaseClass{
12. 复制对象时勿忘其每一个成分
拷贝函数应该确保复制了“对象内的所有成员变量”以及“所有的base class
成员”。不要尝试以某个拷贝函数去实现另一个拷贝函数,应该将两者共同的部分抽取到一个新的函数中去完成,然后由两个拷贝函数共用。
一般而言,如果自己不声明拷贝构造函数和拷贝赋值操作符,那么编译器会帮自己生成的,但是!重点来了!如果选择了自己去声明定义,那么麻烦事就来了(因为即便可能出错编译器也不会告诉你)。
尤其是一个类派生自基类的时候,就需要小心谨慎地去处理基类的对象,然而有些是private
的,因此复制起来比较麻烦,这个时候可以使用这样的方式来解决问题:
对于拷贝构造函数,在初始化列表中显式地去调用基类的拷贝构造函数,然后在子类的拷贝构造函数内部处理好自己的问题。对于赋值拷贝操作符,在合适的位置显式调用基类的operator=()
函数。
具体例子见下面:
private: stringname; public: BaseClass()=default; BaseClass(intsz,charc){name=string(sz,c);} BaseClass(constBaseClass&rhs):name(rhs.name){} BaseClass&operator=(constBaseClass&rhs){ if(this==&rhs)return*this; this->name=rhs.name; return*this; } friendostream&operator<constBaseClass&rhs){ os<returnos; } }; classDeriveClass:publicBaseClass{ private: intage; public: DeriveClass(inta,intsz,charc):BaseClass(sz,c),age(a){} //必须要调用基类的拷贝构造函数,否则不会拷贝构造完全 DeriveClass(constDeriveClass&rhs):age(rhs.age),BaseClass(rhs){} DeriveClass&operator=(constDeriveClass&rhs){ if(&rhs==this)return*this; age=rhs.age; //如果不调用下面这句,将会出现没有拷贝基类 name 值的问题! BaseClass::operator=(rhs); return*this; } friendostream&operator<constDeriveClass&rhs){ os<"\t"<returnos; } }; intmain(){ DeriveClassd1(18,3,'1'); DeriveClassd2(20,5,'2'); DeriveClassd3(d1); d1=d2; cout<endl<endl</** *正常输出: *2222220 *2222220 *11118 */ /** *如果按照前两点建议,那么出现这样的情况概不负责; *11120 *2222220 *18 */ }classBaseClass{
总而言之,一旦选择了自己去完成拷贝构造函数和复制拷贝操作符,那么就别怪编译器不厚道了,需要自己去谨慎操作。
软考之后终于可以静下心来看看书了?(开)?(心)