第一句子网 - 唯美句子、句子迷、好句子大全
第一句子网 > effective c++_【阅读笔记】Effective C++()

effective c++_【阅读笔记】Effective C++()

时间:2022-10-28 07:10:42

相关推荐

effective c++_【阅读笔记】Effective C++()

全文参考自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),例如:

template<typenameT>inlineTcallWithMax(constT&a,constT&b){

returna>b?a:b;

}

intmain(){

inta=callWithMax<int>(1,2);

cout<endl;

}

3. 尽可能使用 const

声明为const可以让编译器帮助检查错误。const可以施加于任何作用域内的对象、函数参数、函数返回类型、成员函数。编译器强制实施bitwise constness,但编写程序时,应该使用“概念上的常量性”conceptual constness。当constnon-const成员函数有实质等价的实现时,要用non-const版本去调用const版本,这样可减少代码重复。

const如果出现在*左边,那么表示被指物是常量;如果在*右边,那么表示指针是常量;如果出现在两边,那么表示指针和被指物都是常量。例如:

intmain(){

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自身是常量,不能指向其他东西了

}

试着习惯这样的写法:

voidf1(constint*i);//指向一个不能修改内容的i

voidf2(intconst*i);//一猫猫一样

这俩写法效果是一样的,都是指向一个内容不可变的数据(指针本身可以再修改指向的对象)。

对于第三点,一个很好的例子如下:

classText{

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声明

}

};

4. 确定对象被使用前已被初始化

为内置对象进行手工初始化,因为C++并不能保证完全初始化好它们。构造函数最好使用成员初值列(member initialization list),而不要在构造函数本体内使用赋值操作(assignment)。初始列列出的成员变量,其排列顺序要和它们在类声明中的一致。为免除“跨编译单元初始化次序”问题,使用 local static 对象来代替 non-local static 对象。

如果成员变量是const的或者references的,那么它们一定需要有初值,不能被赋值。例如:

classX{

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...

}

由于定义于不同编译单元内的non-local static对象的初始化顺序并未明确定义,因此会出现这样情况:

定义在File1.hh中一个静态全局变量tfs中使用tfs

那么如果File1.hh中的tfs还没初始化好呢,中就想使用了,那么就会出现大问题!例如下面这个例子:

//File1.hh

#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<}

这个时候编译器就会报错:

CMakeFiles\local_static.dir/objects.a(.obj)::(.rdata$.refptr.tfs[.refptr.tfs]+0x0):undefinedreferenceto`tfs'

很明显,在local_static目录中没有找到该引用,因此报错了,那么该怎样做呢?

使用方法(类似于工厂方法)来获取这个值,而不是依赖编译器初始化

例如:

//File1.hh

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<}

这样一来,就不用担心了☺。

不过,这样还是有另外一个问题,例如多线程环境下还是有不确定情况,处理这种麻烦情况的做法之一是:在单线程启动阶段,手动调用一遍所有的reference-returning方法。这样可以消除与初始化有关的“竞速形式(race conditions)”

5. 了解C++默默编写并调用哪些函数?

编译器可以暗自为class创建default构造函数、copy构造函数,copy assignment操作符,以及析构函数。

首先,开门见山地说,C++默认编写了默认构造函数、默认析构函数、拷贝构造函数,以及拷贝赋值函数,而且它们默认都是inline的。当然,这些函数的默认创建在一定时期是失效的,例如:

默认构造函数:当提供了一个构造函数后,编译器不再为类提供默认构造函数,而且默认。默认析构函数:当提供了一个析构函数后,编译器就不再提供默认析构函数,默认析构函数是non-virtual的。拷贝构造函数:只要没提供,而且满足可拷贝构造的条件,那么就提供,否则不提供。拷贝赋值函数:只要没提供,而且满足可拷贝复制的条件,那么就提供,否则不提供。

上面说了两个条件,那么具体是什么条件呢?

5.1 可拷贝构造&可拷贝赋值?

先来看一下满足这俩条件的例子:

template<typenameT>

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=

}

那么此时,即便自己不提供拷贝构造以及拷贝赋值构造操作符,编译器也会对成员变量进行递归的拷贝赋值过来。但是在遇到成员变量是const或者reference类型时,编译器就两手一摊,无能为力了(具体可参考Effective C++, 3th, P37)。

例如下面的例子:

template<typenameT>

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&)'

}

当然,这只是编译器不再提供了而已,用户自己还是可以设计如何去复制拷贝以及构造拷贝的,这完全取决于自己怎么处理成员变量。

除此之外,如果基类把拷贝构造函数设置成了private那么在派生类中也是没办法操作的。

6. 若不想使用编译器自动生成的函数,那该明确拒绝

为驳回编译器自动(暗自)提供的机能,可将相应的成员函数声明为private并且不予实现。或者使用继承Uncopyable这样的基类。

如果不想让一个类支持拷贝构造或者赋值构造,那么我们可以将函数声明但不实现,例如这样子:

classUncopyable{

private:

Uncopyable(constUncopyable&);

Uncopyable&operator=(constUncopyable&);

};

当然,对于每一个想实现这个功能的类都能去单独这样声明,不过,还可以使用继承方法去实现,例如:

classUncopyable{

protected:

Uncopyable()=default;

private:

Uncopyable(constUncopyable&);

Uncopyable&operator=(constUncopyable&);

};

classSubClass:publicUncopyable{

//默认不允许拷贝构造和赋值运算符

};

intmain(){

SubClasss1,s2;

s1=s2;//error!

}

7. 为多态基类声明virtual析构函数

带多态性质的基类应该声明一个virtual析构函数。如果类带有任何virtual函数,那么它就应该拥有一个virtual析构函数。如果类的设计目的不是用来做基类的,那么就不应该声明virtual析构函数。

当使用基类指针指向派生类对象时,没有问题,但是要是想把这个基类指针给删掉,这时候问题就来了,例如下面这个例子:

classBaseClass{

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;

}

上面的程序乍一看看不出个毛病来,现在对BaseClass *obj = new DeriveClass(16);设置断点,进行单步调试,可以观察到构造函数过程是:

newDeriveClass(16)

|

BaseClass(16)

|

DeriveClass(16)

|

end

这个顺序完全正确,先构造基类再构造派生类嘛,执行完后,内存状态是这样的:

可以得知操作系统给这两个对象中的成员分配内存到了name : 0x1061980count : 0x10619c0

那么执行delete obj;时,顺序是这样的:

deleteobj

|

~BaseClass()

|

end

从上面可以看出来,竟然只执行了基类的析构函数,而没有执行派生类的析构函数,那么这时的内存表示是怎么样的?见下图:

由此可见,在不经意间,就造成了内存泄漏问题,那么该如何解决这个问题呢?

很简单,只需要把基类的析构函数声明为virtual就可以了,这样强制去执行子类的析构函数。

不过,这样还是有两种结果,例如下面是一种结果:

classBaseClass{

//其他都一样

virtual~BaseClass(){deletename;}

};

classDeriveClass:publicBaseClass{

//其他都一样

~DeriveClass()override{deletecount;}

};

这个时候,是先执行的派生类析构函数,再执行基类析构函数。

另一种结果是:

classBaseClass{

virtual~BaseClass(){deletename;}

};

classDeriveClass:publicBaseClass{

//删掉了自己的析构函数

};

这个情况下,才是先执行基类析构函数,再执行派生类析构函数。

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=操作符时,可以写成:

classBaseClass{

public:

BaseClass&operator=(constBaseClass&rhs){

//随便干点什么

return*this;//关键在于这里

}

};

当然啦,也可以不做返回,不过既然这是一个好的实践,那么没有确切的理由不去做,最好就去做。

11. 在 operator= 中处理“自我赋值”

确保当对象自我赋值时,operator=有可预估的行为。其中需要注意的包括比较“来源对象”和“目标对象”的地址、精心周到的语句顺序以及拷贝交换。确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。

简而言之,就是需要考虑操作符两边是否是同一个对象,因为如果是同一个对象,会出现类似下面的问题:

classBaseClass{

private:

char*data;

public:

BaseClass&operator=(constBaseClass&rhs){

deletedata;

data=rhs.data;

return*this;

}

};

intmain(){

BaseClassbaseClass;

baseClass=baseClass;

}

自己给自己赋值,没毛病,但是在运算符函数的delete data却带来了问题,因为它删除掉了自己的内存空间,却在下面那行data = rhs.data又想用了,而这时系统已经收回了这块空间,这样一来操作系统肯定是不干的,所以程序就报错了。

那么该如何解决呢?这样:

classBaseClass{

private:

char*data;

public:

BaseClass&operator=(constBaseClass&rhs){

//多一个检查是否是自己的操作就可以了,也称证同测试

if(&rhs==this)return*this;

deletedata;

data=rhs.data;

return*this;

}

};

12. 复制对象时勿忘其每一个成分

拷贝函数应该确保复制了“对象内的所有成员变量”以及“所有的base class成员”。不要尝试以某个拷贝函数去实现另一个拷贝函数,应该将两者共同的部分抽取到一个新的函数中去完成,然后由两个拷贝函数共用。

一般而言,如果自己不声明拷贝构造函数和拷贝赋值操作符,那么编译器会帮自己生成的,但是!重点来了!如果选择了自己去声明定义,那么麻烦事就来了(因为即便可能出错编译器也不会告诉你)。

尤其是一个类派生自基类的时候,就需要小心谨慎地去处理基类的对象,然而有些是private的,因此复制起来比较麻烦,这个时候可以使用这样的方式来解决问题:

对于拷贝构造函数,在初始化列表中显式地去调用基类的拷贝构造函数,然后在子类的拷贝构造函数内部处理好自己的问题。对于赋值拷贝操作符,在合适的位置显式调用基类的operator=()函数。

具体例子见下面:

classBaseClass{

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

*/

}

总而言之,一旦选择了自己去完成拷贝构造函数复制拷贝操作符,那么就别怪编译器不厚道了,需要自己去谨慎操作。

软考之后终于可以静下心来看看书了?(开)?(心)

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。