effective c++ 笔记 条款5-12

条款5 了解C++默默编写并调用哪些函数

当没有声明时,编译器会自动为类创建默认构造函数、析构函数、复制构造函数和赋值构造函数,这些函数都是public且inline的。并且默认声明的函数只有在实际调用时才被实现

编译器默认创建的拷贝是浅拷贝,即目标对象和被拷贝对象共享一块内存。因此如果类内有引用成员或const成员,你需要自己定义拷贝行为
编译器默认创建的析构函数是非虚函数,如果有多态需求,要主动声明虚析构函数条款07

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

由于条款5,若不想使用编译器自动生成的函数,可将对应函数声明为private且不予实现,用来防止来自类外的调用。如果类内不小心调用(成员函数、友元),会得到一个链接错误。
或者继承一个类似uncopyable的基类,该基类的对应函数为private且不予实现,这样子类调用时会去调用基类的该函数,将调用错误转移至编译期。但是可能导致多重继承
c+11直接使用= delete来声明拷贝构造函数,显式禁止编译器生成该函数

条款7 为多态基类声明虚析构函数

一个带多态性质的类,基类的设计目的就是为了通过基类接口处理子类对象。因此带有多态性质的基类必须将析构函数声明为虚函数,否则当基类指针指向派生类的时候,在析构时,只会调用基类而不会调用派生类的析构函数,从而导致内存泄露。

普通的基类不应该有虚析构函数。如STL容器,string等。虚函数会带有一个指向虚函数表的虚函数指针。浪费内存。

析构函数的运作方式是,从最深层派生的那个 class 的析构函数最先被调用,然后每一个 base class 的析构函数被调用。不需要额外的虚函数指针来确保内存安全。而当使用多态特性的时候,基类指针指向派生类时,析构函数的调用是静态的,故需要将析构声明为虚的,进行重写。定制子类的析构函数
(对于所有派生类的析构函数,编译器都会插入调用直接基类的析构函数的代码)

c+11,使用final声明一个未设计成基类,又有误继承风险的类。来禁止派生
编译器自动生成的析构函数时非虚的,所以多态基类必须将析构函数显示声明为virtual条款5

条款8 别让异常逃离析构函数

析构函数绝对不要吐出异常,因为很大可能发生各种未定义的问题,包括但不限于内存泄露、程序异常崩溃、所有权被锁死等。
因为析构函数是一个对象生存期的最后一刻,经常负责如线程,连接和内存等各种资源所有权的归还。如果析构函数执行期间某个时刻抛出了异常,就说明抛出异常后的代码无法再继续执行。

如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序。

如果客户需要对某个操作函数运行期间抛出的异常做出反应,如资源的归还等,又不想把异常吞掉,那么 class 应该提供一个普通函数(而非在析构函数中)执行该操作。由客户自己调用函数并且处理异常,将责任交给了客户。

条款9 绝不在构造和析构过程中调用虚函数

一个子类对象开始创建时,首先调用的是基类的构造函数,子类对象的基类构造期间,对象的类型是基类不是自子类,基类的构造函数中调用的虚函数将会是基类的虚函数版本,即在构造函数中虚函数并不是虚函数,在不同的构造函数中,调用的虚函数版本并不同
同理,析构函数在调用的过程中,子类对象的类型从子类退化到基类
一个容易忽略的问题是,为了避免代码重复,将构造函数的注意工作抽象成一个init()函数,但是init()函数中调用了虚函数,往往不容易被发觉。析构函数同理
如果想在构造子类时,多态调用父类的函数来做一些事情,通常在子类调用父类构造函数时,向上传递一个值到父类的构造函数给对应函数使用。
假设一个派生类的对象进行析构,首先调用了派生类的析构,然后在调用基类的析构时,遇到了一个虚函数,这个时候有两种选择:第一种是编译器调用这个虚函数的基类版本,那么虚函数则失去了运行时调用正确版本的意义;第二种是编译器调用这个虚函数的派生类版本,但是此时对象的派生类部分已经完成析构,“数据成员就被视为未定义的值”,这个函数调用会导致未知行为。实际情况中编译器使用的是第一种,如果虚函数的基类版本不是纯虚实现,不会有严重错误发生,但失去了虚函数本身多态含义

条款10 令 operator= 返回一个reference to *this

c++中
赋值采用右结合律。
连锁赋值

int x, y, z;
x = y = z = 15;

相当于

x=(y=(z=15));

为了遵循这种特性(让自己的接口和内置类型相同功能的接口尽可能相似),必须返回一个reference指向操作符的左侧实参。
即class重载赋值操作符应该遵循的协议。适用于所有赋值相关的运算(+=,-=,*=,/=,<<=,…)

class Object{
public:
	Object& operator=(const Object& rhs){
		return *this;
	}
	Object& operator=(int rhs){
		//...
		return *this;
	}
	Object& operator+=(int rhs){
		//...
		return *this;
	}
};

条款11 在 operator= 中处理自我赋值

代码可能存在不好辨识的自我赋值,当代码有 指针,引用 用于指向多个相同或者处于同一继承体系下的类型对象,就可能存在别名情况

a[i]=a[j];
*px=*py;

因此在 operator= 函数中要注意异常安全性。
如果rhs与this未同一对象,rhs.p也已经被删除,new Entity将抛出异常。

Object& Object::operator=(const Object&rhs){
	delete p;
	p =new Entity(rhs.p);
	return *this;
}

可将证同测试,但如果new Entity(rhs.p)出现异常,赋值失败,将导致p指向被删除的内存

Object& Object::operator=(const Object&rhs){
	if (this == &rhs) return *this;
	delete p;
	p = new Entity(rhs.p);
	return *this;
}

通过调整代码时序,即使new Entity(rhs.p)出现异常赋值失败,但delete不被执行,p扔指向原来的对象

Object& Object::operator=(const Object&rhs){
	Entity* temp= p;
	p=new Entity(rhs.p);
	delete temp;
	return *this;
}

比较好的替代方案是使用 copy and swap 技术。

Object& Object::operator=(const Object&rhs){
	Object obj(rhs);
	swap(*this, obj);
	return *this;
}

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

拷贝构造函数和拷贝赋值函数要确保复制了对象内的所有成员变量和所有基类成分
即如果自定义以上构造函数,那么每增加成员变量,都要同步修改以上构造函数,且要调用基类的相应构造函数。如果忘记处理,编译器也不会报错

拷贝构造函数在构造一个对象,这个对象在调用之前并不存在。如果其中调用了赋值操作符,是在给一个还未构造好的对象赋值。
而赋值操作符在改变一个对象,这个对象是已经构造好了的。如果其中调用了拷贝构造符,是在构造一个已经存在了的对象。
但两个函数代码上往往具有很大的相似度。一个明智的做法是:封装相同的代码,建立一个新的函数给两者调用。这样的函数往往为private且命名类似init。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值