effective C++笔记——构造/析构/赋值运算

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/rest_in_peace/article/details/83504321

c++对类默认编写并调用的函数

当C++处理过一个类后,编译器会为它声明(编译器的版本)没有声明的一个copy构造函数、一个赋值运算符函数和一个析构函数,此外如果没有声明任何构造函数,编译器也会生成一个默认的构造函数。所有这些函数都是public和inline的。
  值得注意的是赋值构造函数以及赋值运算符函数的默认版本编译器只是单纯的进行将对象的每一个非静态成员变量拷贝到目标对象,意味着也许类中变量存在非内置的类型,在做拷贝时就没有合适的规则而导致错误的产生。这些可以通过自己重载来解决。

明确拒绝编译器自动生成函数

有些情况可能并不需要类中出现拷贝情况的发生,但通常类可以自动声明自己的版本,这里就是想要阻止拷贝情况的发生:
  有一种比较取巧的方法是:因为所有编译器生成的版本都是public的,为了阻止这些函数被创建出来,可以自己声明这些函数,并且将其声明为private的,这样阻止了编译器自动生成其专属版本,也阻止用户来调用它。但是其成员函数和友元函数也是可以调用private函数的,为了阻止这样情况的发生,可以不对这些函数进行定义,那么如果进行调用,将发生一个连接错误。例如:

class HomeForSale{
public:
	...
private:
	...
	HomeForSale(const HomeForSale&);							//只有声明
	HomeForSale& operator=(const HomeForSale&);
};

有了上述定义,当在类的外部调用这些函数,编译器会阻止它,如果在成员函数或友元函数中调用它,连接器会阻止它。
  通常将连接期的错误移到编译期发现是更好更方便的,因此还可以为HomeForSale类定义一个基类:

class Base{
protect:
	Base();
	~Base();
private:
	Base(const Base&);
	Base& operator=(const Base&);
};
class HomeForSale :public Base{
	...
};

通过这种方式,HomeForSale类将不在声明拷贝构造函数或拷贝赋值运算符函数,只要任何人,包括HomeForSale的成员函数或是友元函数进行调用,都会被编译器拒绝。

以上方式是在effective C++这本书中看到的方法,在之前的学到C++11新标准中有另一种方式:将函数声明为delete

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

带多态性质的基类都应该声明一个virtual的析构函数,即如果一个class带有任何virtual函数,它就应该拥有一个virtual析构函数;但是如果一个类的目的不是作为基类使用,或不是为了具备多态性,就不该声明虚析构函数
  基类没有声明virtual析构函数可能带来的灾难是:当一个派生类对象经由一个父类的指针被删除时,而这个父类的析构函数不是虚函数,其结果将是无定义的——实际情况中的通常发生的是子类对象没有被销毁,这可能会造成资源泄露、数据结果被破坏、浪费调试时间等麻烦。

class Base{
public:
	virtual ~Base(){
		cout<<"~Base"<<endl;
	}		
};

class Son:public Base{
public:	
	~Son(){
		cout<<"~Son"<<endl;
	}
};

int main(void){
	Base* b = new Son();
	b->f(); 
	delete b;									
} 

别让异常逃离析构函数

C++并不禁止析构函数突出异常,但是并不鼓励这么做。例如:

class W{
public:
	...
	~W(){...}				//假设这里可能抛出异常
};
void dosomething(){
	vector<W> v;
	...
}							//v在这里自动销毁

当v被销毁的时候,它有责任销毁存的所有元素,假设它有10个W元素,在析构第一个元素的时候就抛出了异常,其他9个W元素还是应该被销毁,因此v应该继续调用它们的析构函数,但假设第二个元素在被销毁的时候又抛出了异常,现在将有两个同时作用的异常,这对C++而言太多了,可能造成程序结束执行或是导致不明确的行为。C++不喜欢析构函数抛出异常

绝不在构造和析构过程中调用virtual函数

这点其实还好理解,假设有一个基类和一个派生类,在基类的构造函数中调用了一个纯虚函数,在派生类中对继承而来的虚函数进行了自己的重写,但是众所周知的是,派生类对象的基类部分会先被构造出来,也就是先调用基类的构造函数,那么基类的构造函数中所调用的虚函数还不是派生类中的版本。其根本原因是派生类对象的基类部分构造期间,对象的类型实际上是基类类型而不是派生类类型
  相同的道理同样作用于析构函数。一旦派生类的析构函数开始执行,对象内的派生类部分的成员变量将呈现未定义值,C++视他们仿佛不存在,进入基类的析构部分后对象就成为了一个基类对象。

令operator=返回一个引用(reference to *this)

关于赋值,可以将其写成连锁形式:
x = y = z = 15
根据右结合律,上述行为应该被解析为:
x = (y = (z = 15))
为了实现这种连锁赋值的操作,返回引用时一个好的方法:

Widget& operator=(const Widget& rhs){
	...
	return *this;
}

这种做法同样适用于所有与赋值相关的操作,如+=。

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

自我赋值这种情况虽然看上去比较蠢,但是谁知道你的客户是个什么鬼,并且有些自我赋值不太容易能看出来,例如:
  a[i] = a[j];      //i和j的值相同
  *px = *py;     //px和py恰好指向同一个东西
  这种自我赋值出现最大的问题在于可能你动态分配的东西在赋值的时候被释放掉:

class Bitmap{...};
class Widget{
	...
private:
	Bitmap* pb;        //指向动态分配的对象
};

Widget& Widget::operator=(const Widget& rhs){
	delete pb;					
	pb = new Bitmap(*rhs.pb);					//拷贝构造,但是rhs被释放掉了
	return *this;
};

以上代码如果是做自我赋值的话,delete操作相当于也释放掉了rhs的pb,这就会造成之后的错误,向阻止这种情况的发生,只要简单的添加判断是否是自我赋值就行了。

Widget& Widget::operator=(const Widget& rhs){
	if(this == &rhs){							//证同测试
		return *this;
	}
	delete pb;					
	pb = new Bitmap(*rhs.pb);					//拷贝构造,但是rhs被释放掉了
	return *this;
};

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

写代码时常会遇到代码已经写好了,但是后来需要添加新的东西进去情况,比如为类新添加一个成员属性,这样原有的拷贝函数(拷贝构造函数和拷贝赋值运算符函数)中并没有对最新的这个成员进行拷贝,这可能会遗忘,从而在之后的运行中带来一些麻烦。又或者是在对某一个派生类做拷贝操作时,只复制了派生类自身的部分而忽略了其基类的部分。
  因此在编写拷贝函数的时候,请确保:1.复制每一个local变量;2.调用所有的基类中的适当的拷贝函数。

猜你喜欢

转载自blog.csdn.net/rest_in_peace/article/details/83504321