第二章 构造、析构、赋值运算
本章共有如下8个条款:
- 条款5 了解 C++ 默默编写并调用哪些函数
- 条款6 若不想使用编译器自动生成的函数,就该明确拒绝
- 条款7 为多态基类声明 virtual 析构函数
- 条款8 别让异常逃离析构函数
- 条款9 绝不在构造和析构过程中调用 virtual 函数
- 条款10 令 operator= 返回一个 reference to *this
- 条款11 在 operator= 中处理“自我赋值”
- 条款12 复制对象时勿忘其每一个成分
条款5 了解 C++ 默默编写并调用哪些函数
一个空类编译器会默认产生如下6个成员函数:
- 默认构造函数
- 拷贝构造函数
- 拷贝赋值操作符
- 析构函数(none virtual)
- 取地址运算符
- const 取地址运算符重载函数
class Empty()
{
public:
Empty();//构造函数
Empty(const Empty&);//拷贝构造函数
Empty& operator=(const Empty&);//拷贝赋值操作符重载
~Empty();//析构函数
Empty * operator&();//取址运算符
const Empty * operator&() const;//取址运算符 const
}
1)、如果自己创建了构造函数,编译器不会产生默认的构造函数;
2)、base class 如果把拷贝构造函数或者赋值操作符设为 private ,则编译器不会为其 derived class 生成这两个函数;
3)、含有引用成员变量或 const 成员变量则不产生赋值操作符。
class A{
private:
string& str;//引用定义后不能修改绑定对象
const int value;//const对象定义后不能修改
};
条款6 若不想使用编译器自动生成的函数,就该明确拒绝
有两种方法可以不使用编译器自动生成的函数:
方法1:将相应的成员函数声明为private并且不予实现,阻止编译器生成和用户调用他们,不定义是因为成员函数和友元函数有可能还会调用他们。如果不慎被调用会获得一个连接错误。
class A
{
public:
...
private:
...
A(const A&);//只有声明
A& operator=(const A&);
}
方法一改进:将连接期错误移至编译期间,能更早侦测出错误。方法是定义一个基类,缺点可能会导致多重继承。
class Uncopyable()
{
protected:
Uncopyable() {}//允许派生类对象构造和析构
~Uncopyable() {}
private:
Uncopyable(const Uncopyable&);//阻止copying
Uncopyable& operator=(const Uncopyable&);
};
class A :private Uncopyable//不再声明copy构造函数或copy assigment操作符
{
};
条款7 为多态基类声明 virtual 析构函数
析构函数设为虚函数原因:当基类指针指向动态创建的子类对象时,通过 delete 释放内存空间时,只会调用基类的析构函数而不会调用子类的析构函数,造成资源释放不彻底。
class A
{ }
class B:public A
{ }
A *a = new B;
delete a;//用delete释放动态存储空间
请记住下面两条建议:
- 带多态性质的 base classes 应该声明一个 virtual 析构函数。如果 class 带有任何 virtaul 函数,他就应该拥有一个 virtual 析构函数。
- classes 的设计目的如果不是作为 base classes 使用,或不是为了具备多态性,就不该声明 virtual 析构函数。
条款8 别让异常逃离析构函数
构造函数可以抛出异常,但不建议这样做:
- 构造函数中抛出异常,会导致析构函数不能被调用,但对象本身已申请到的内存资源会被系统释放(已申请到资源的内部成员变量会被系统依次逆序调用其析构函数)。
- 因为析构函数不能被调用,所以可能会造成内存泄露或系统资源未被释放。
- 构造函数中可以抛出异常,但必须保证在构造函数抛出异常之前,把系统资源释放掉,防止内存泄露。
析构函数不可以抛出异常,原因在《More Effective C++》中提到两个:
- 如果析构函数抛出异常,则异常点之后的程序不会执行,如果析构函数在异常点之后执行了某些必要的动作比如释放某些资源,则这些动作不会执行,会造成诸如资源泄漏的问题。
- 通常异常发生时,c++的异常处理机制在异常的传播过程中会进行栈展开(stack-unwinding),因发生异常而逐步退出复合语句和函数定义的过程,被称为栈展开。在栈展开的过程中就会调用已经在栈构造好的对象的析构函数来释放资源,此时若其他析构函数本身也抛出异常,则前一个异常尚未处理,又有新的异常,会造成程序崩溃。
更详细内容可以看下面文章:
构造函数、析构函数抛出异常的问题
C++构造函数和析构函数中抛出异常的注意事项
是否能在构造函数,析构函数中抛出异常?
条款9 绝不在构造和析构过程中调用 virtual 函数
- 不要在构造函数中调用虚函数的原因:因为父类对象会在子类之前进行构造,此时子类部分的数据成员还未初始化, 因此调用子类的虚函数是不安全的,而且调用虚函数是不会呈现出多态的。
- 不要在析构函数中调用虚函数的原因:析构函数是用来销毁一个对象的,在销毁一个对象时,先调用子类的析构函数,然后再调用基类的析构函数。所以在调用基类的析构函数时,派生类对象的数据成员已经“销毁”,这个时再调用子类的虚函数已经没有意义了,也不会呈现多态。
class A
{
public:
A() {
show();
}
virtual void show(){
cout<<"in A"<<endl;
}
virtual ~A(){
show();
}
};
class B:public A{
public:
B() {
show();
}
void show(){
cout<<"in B"<<endl;
}
};
int main(){
A *a = new A;
delete a;
cout << "*****************" << endl;
A *b = new B;
delete b;
}
输出结果,可以看到没有预想的多态效果:
in A
in A
*****************
in A
in B
in A
结论:构造函数和析构函数调用虚函数时都不使用动态联编,如果在构造函数或析构函数中调用虚函数,则运行的是为构造函数或析构函数自身类型定义的版本。
条款10 令 operator= 返回一个 reference to *this
为了实现连锁赋值,赋值操作符必须返回一个 reference 指向操作符的左侧实参。
class A
{
public:
...
A& operator=(const A& other)//返回类型是个 reference,指向当前对象
{
...
return *this;//返回左侧对象
}
}
条款11 在 operator= 中处理“自我赋值”
不同的变量指涉同一对象,可能会造成在停止使用资源之前意外释放了它。
class Bitmap
{
};
class Widget {
private:
Bitmap* pb;
public:
Widget& operator=(const Widget& rhs);
};
Widget& Widget::operator=(const Widget& rhs)
{
delete pb;//rhs和*this有可能是同一个对象,会删除rhs
pb = new Bitmap(*rhs.pb);//rhs's bitmap的副本
return *this;
}
//改进
Widget& Widget::operator=(const Widget& rhs)
{
if(this == &rhs) return *this;//相同测试,如果是自我赋值,不做任何事情
delete pb;//rhs和*this有可能是同一个对象,会删除rhs
pb = new Bitmap(*rhs.pb);//rhs's bitmap的副本
return *this;
}
条款12 复制对象时勿忘其每一个成分
将 copy 构造函数和 copy 赋值操作符称作 copying 函数,如果声明自己的 coping 函数,要注意:
- copying 函数应该确保复制“对象内的所有成员变量”及“所有 base class 成分”;
- 不要尝试以某个 copying 函数实现另一个 copying 函数。应该将共同机能放进第三个函数,并由两个 coping 函数共同调用(减少重复性代码)。
class A
{
public:
...
private:
int age;
string name;
};
class B:public A
{
public:
...
B(const B& rhs);
B& operator=(const B& rhs);
...
private:
int x;
}
B(const B& rhs):x(rhs.x){ };
B& operator=(const B& rhs)
{
x=rhs.x;
return *this;
}
以上 coping 函数看似 B 内的每一样东西,但每个 B 还内含它所继承的 A 成员变量副本,而那些成员变量并未被复制。B 的 copy 构造函数并没有指定实参传给其 base class 构造函数,也就是说 B 的初始化列表中没有提到 A,因此 B 对象的 A 成分会被不带实参的 A 构造函数(即 默认构造函数——必定有一个否则无法通过编译)初始化,默认构造函数对 age 和 name 执行缺省的初始化动作。
正确的做法是:让 derived class 的 coping 函数调用相应的 base class 函数
B(const B& rhs):A(rhs),x(rhs.x){ };
B& operator=(const B& rhs)
{
A::operator=(rhs);
x=rhs.x;
return *this;
}
所以自己写一个 coping 函数,要确保:1)复制所有 local 成员变量,2)、调用所有 base classes 内的适当的 coping 函数。
这里再总结一下拷贝构造函数和复制运算符重载的区别:
- 拷贝构造函数是函数,赋值运算符是运算符重载
- 拷贝构造函数会生产新的类对象,赋值运算符不能
- 拷贝构造函数是直接构造一个新的类对象,所以在初始化对象前不需要检查源对象和新建对象是否相同,赋值运算符需要上述操作并提供两套不同的复制策略,另外赋值运算符中如果原来的对象有内存分配则需要先把内存释放掉。
- 形参传递是调用拷贝构造函数,并不是所有出现“=”的地方都是使用赋值运算符,要看是否有新对象生成。
参考文章
1、https://www.cnblogs.com/biterror/p/6909577.html
2、https://blog.csdn.net/vict_wang/article/details/81637048
3、https://www.cnblogs.com/bonelee/p/5826196.html
4、https://blog.csdn.net/xtzmm1215/article/details/45130929
5、https://blog.csdn.net/sinat_21107433/article/details/81836602