条款05:了解C++默默编写并调用的哪些函数
- 如果写了一个空的类,C++编译器会为这个类自当产生四个函数,并且这些函数都是inline的:
(1)默认构造函数,不带参数;
(2)析构函数;
(3)拷贝构造函数;
(4)赋值运算符;
实际上在现代C++(C++11之后)还会产生一个移动构造函数; - 开发者如果实现了一个类的构造函数(带参数),则编译器就不会为这个类产生默认构造函数了。
- 编译器在子类产生默认赋值运算符的时候也会考虑到父类中成员变量的const属性,如果父类中有const成员,则编译器不会为子类产生默认的赋值运算符,如下:
class B {
private:
const int value = 100;
};
class D : public B {
};
// 编译器不会自动为B产生赋值运算符!
条款06:若不想用编译器自动生成的函数,就该明确拒绝
如果在声明一个类后,没有声明拷贝构造函数或者赋值运算符的情况下,按照上一条规则,编译器是会为你自动生成对应的函数的。(注意:这里的自动是指你的代码里产生了这样的调用的时候)
但是,实际场景下,往往存在不能拷贝构造或者赋值运算的情况(这两种操作没有实际意义),就需要将这两个函数定义成不能被使用。
- 方法1:把拷贝构造函数或者赋值运算符定义为类的private成员
class A {
public:
private:
A(const A& other); // 不定义它
A& operator=(const A& other); // 不定义它
};
外部代码肯定不能使用这两个函数了,并且A类内部成员函数使用的时候会在链接的时候报错,因为我们没有定义他们。
- 方法2:私有继承(实现继承而非接口继承)一个uncopyable的类,这个方法比较巧妙
class UnCopy {
public:
UnCopy() {}
~UnCopy() {}
private:
UnCopy(const UnCopy&);
UnCopy& operator=(const UnCopy&);
};
class MyClass : private UnCopy { // 这里如果用public继承也可以达到同样的效果
};
MyClass obj1, obj2;
obj1 = obj2; // 错误,编译器自动为MyClass生成赋值运算符的时候会调用UnCopy的赋值运算符,而它在MyClass类中是不可见的。
这一条规则在现代C++(11之后),实现起来就非常简单了,使用delete关键字即可做到,如下所示:
class MyClass {
public:
MyClass() {}
~MyClass() {}
MyClass(const MyClass& other) = delete;
MyClass& operator=(const MyClass& other) = delete;
};
MyClass obj1, obj2;
obj1= obj2; // 错误
条款07:多态基类声明virtual析构函数
- 如果要声明一个支持多态特性的基类,其析构函数要加上virtual关键字,否则在销毁对象时会产生局部销毁的问题,如下:
class B {
public:
B() {}
~B() {}
};
class D : public B {
};
B* p = new D();
delete p; // 将会产生局部销毁的问题,产生资源泄露
- 如果基类不是作为多态场景来使用,其析构函数不要加virtual关键字,以便减少内存占用。
条款08:别让异常逃离析构函数
这条规则背后的原因是如果在类的析构函数中抛出异常,外部的代码很难捕捉到,考察以下代码:
class A {
public:
A() {}
~A() {
throw 1;
}
};
int main()
{
try {
A tmp;
} catch (...) {
std::cout << "Caught" << std::endl;
}
return 0;
}
上面这段代码在A的析构函数中抛出了异常,外面的代码虽然进行了捕捉,但是是捕捉不到的,程序会直接退出。
如果类的析构函数真的可能会出现抛出异常的情况,推荐的处理方法有两种:
- 方法1:在析构函数内部进行异常处理,直接退出或者吞掉异常。
class A {
public:
A() {}
~A() {
try {
DoSomething(); // 可能产生异常的函数
} catch(...) {
std::abort(); // 直接退出或者在这里吞掉异常
}
}
};
- 方法2:在方法1的基础之上,把析构函数中可能产生异常的函数独立出来作为类的一个成员函数,由用户在资源销毁时显式调用。
class A {
public:
A() {}
~A() {
try {
DoSomething(); // 可能产生异常的函数
} catch(...) {
std::abort(); // 直接退出或者在这里吞掉异常
}
}
void DoSomething() {...}
};
条款09:不在构造、析构函数中调用virtual函数
- 不在构造函数中调用virtual函数原因:当定义子类对象时,构造过程是从基类对象构造开始,然后再执行子类对象部分构造。如果在基类的构造函数中调用了一个virtual函数,这时候该virtual函数还是指向基类对应的函数,因为子类对象还没有初始化好,这样就没有达到多态的预期效果。
- 不在析构函数中调用virtual函数原因:当销毁一个子类对象时,先销毁子类对象部分,然后再销毁基类部分。如果在基类的析构函数中调用了virtual函数,则会调用子类中对应的该函数,而这时候子类部分已经被释放了,成员变量处于未定义状态,如果这个virtual函数访问了这些成员变量,则会出现位定义的行为。
条款10:令operator=返回一个引用指向*this
这一条不是一个强制性的规范,而是大家都遵循的协议,比如你在类的定义中实现operator=的时候,最好是返回一个T&引用,如:
class A {
public:
...
A& operator=(const A& other)
{
...;
return *this;
}
};
A a1, a2, a3;
a1 = a2 = a3; //用户可能这样写
条款11:operator=处理自我赋值
这条规则是接上一条规则的,如果在实现operator=的时候,如果针对自我赋值的情况没有处理好,会出现以下两种结果:
- 错误1:类内对象指针变量指向了被销毁的对象
class T;
class A {
public:
A& operator=(const A& other)
{
delete pObj; // 销毁原有的T对象
pObj = new T(*other.pObj); // T的拷贝构造函数
return *this;
}
private:
T* pObj;
};
很显然,如果是自我赋值的情况,上面的语句会将自己的T对象释放掉,显然后面就会出问题了。
- 错误2:new对象时出现异常不安全问题
class T;
class A {
public:
A& operator=(const A& other)
{
if (this == &other) return *this; // 加入一个是否是自我赋值的判断
delete pObj; // 销毁原有的T对象
pObj = new T(*other.pObj); // T的拷贝构造函数,这个过程可能出现异常,如果出现异常,那么pObj就指向了一个已经销毁了的对象。
return *this;
}
private:
T* pObj;
};
- 正确的版本:能够处理自我赋值且异常安全的实现
class T;
class A {
public:
A& operator=(const A& other)
{
T* pObjOrig = pObj;
pObj = new T(*other.pObj);
delete pObjOrig;
return *this;
}
private:
T* pObj;
};
条款12:复制对象时勿忘其每一部分
这里的复制对象是指类的拷贝构造函数(copying)和赋值运算符(copy assignment),前者用于一个类实例对象的定义,后者用于两个已经定义的类实例之间的赋值。
- 子类的拷贝构造函数定义时,如果没有定义基类部分的构造,则默认调用基类的默认构造函数初始化基类部分,如下:
class B {
public:
B() {}
virtual ~B() {}
B(const B& other) {}
};
class D : public B {
public:
D(const D& other) {} // 这里没有定义基类部分的初始化,则默认调用基类的默认构造函数,即B()进行初始化
};
按照上面的代码,当使用D的拷贝构造函数进行初始化一个D类实例对象时,对于基类部分可能没有达到预期的复制效果,一般建议如下方式进行子类的拷贝构造函数定义:
class B {
public:
B() {}
virtual ~B() {}
B(const B& other) {}
};
class D : public B {
public:
D(const D& other) : B(other) {} // 调用B类的拷贝构造函数对基类部分进行初始化
- 子类的赋值运算符定义时,如果没有定义基类部分的赋值,则保持不变,如果要实现基类部分同时赋值,则按照如下方式编写:
class B {
public:
B& operator=(const B& other) {}
};
class D : public B {
public:
D& operator=(const D& other)
{
B::operator=(other); // 调用基类的赋值运算符,对基类部分进行赋值
...
}
};