二:构造、析构、赋值运算一
几乎你写的每一个class都会有一个或多个构造函数、一个析构函数、一个拷贝赋值运算符,确保它们的行为正确是生死攸关的大事。
条款五:链接C++默默编写并调用哪些函数
记住,如果你自己没声明,编译器会帮你声明定义四个函数:
- 默认构造函数
- 拷贝构造函数
- 拷贝赋值运算符
- 析构函数
因此,如果你写下:
其实它的样子如下(经过编译器处理后):class Empty{};
当然,只有当这四个函数被调用时,它们才会被编译器创建出来(完全不浪费),例如下面代码造成上面每一个函数都被编译器产出:class Empty{ public: Empty() {...} Empty(const Empty& rhs){...} Empty& operator=(const Empty& rhs){...} ~Empty(){...} };
注意:编译器产出的析构函数不是虚函数,除非该类继承自一个带有虚析构函数的类。Empty e1; //默认构造函数、析构函数 Empty e2(e1); //拷贝构造函数 e2 = e1; //拷贝赋值运算符
现在我们知道了编译器会默默帮我们声明4个函数,那么,这四个函数到底做了什么呢?
- 默认构造函数和析构函数:
调用基类和非静态成员变量的构造函数和析构函数 - 拷贝构造函数和拷贝赋值运算符:
将来源对象的每一个非静态成员变量拷贝到目标对象
正因为拷贝构造函数将来源对象的每一个非静态成员变量拷贝到目标对象,我们就要保证成员变量均有拷贝构造函数(不然你就要自定义拷贝构造函数)
下面我们来看一种编译器拒绝生成拷贝赋值运算符的例子:
template<class T>
class NameObject{
public:
NameObject(string& name, const T& value); //注意是引用
private:
string& nameValue; //引用
const T objectValue; //const
};
现在考虑以下代码会生成拷贝赋值运算符吗
string str1("SYF"), str2("XCY");
NameObject<int> p(str1, 10), s(str2, 12);
p = s; //这句话会成功吗?编译器会帮着生成拷贝赋值运算符吗
C++ 中的很多问题我们都可以这样来考虑,假设它成功了,那么p对象中的引用nameValue就会改成指向 "XCY",我们不允许引用改指向不同对象,所以它失败了,编译器不会生成。况且里面还有个const。
所以啊,编译器也是有节操的,它会认真考虑它生成的函数是否符合规范,而不是一味地瞎生成。
上面这种情况说明了:如果你打算在有引用成员或者const成员的类中支持拷贝赋值运算符,你不能依赖编译器,得自己实现。
最后还有一种情况:如果某个基类将拷贝赋值运算符声明为private,那么编译器将拒绝为其派生类生成拷贝赋值运算符
很好理解嘛:编译器要是生成了派生类的拷贝赋值运算符,它肯定要去调基类的拷贝赋值运算符用于拷贝基类成分,结果发现你***的居然是private,编译器就罢工了,老子不生成了。
小结:
编译器会有节操地帮你声明定义四个函数:默认构造函数、拷贝构造函数、拷贝赋值运算符、析构函数
条款六:若不想使用编译器自动生成的函数,就该明确拒绝
人生也该如此啊
有时候在某些应用场景中,每个对象是独一无二的,也就是说,应该禁止拷贝构造函数和拷贝赋值运算符。
我们的方法是:将拷贝构造函数和拷贝赋值运算符声明为private的,避免了默默生成,又不让外部调用,当然,成员函数和友元函数还是可以调用的。
当然,你可以选择只声明,不实现,这样的话,当友元和成员函数调用它时,会由连接器发出错误。
更完美的做法是,声明一个基类,在基类中将拷贝构造函数和拷贝赋值运算符声明为private,这样的话,即便是成员函数和友元函数调用,编译器也能发出错误提醒
小结:
为驳回编译器自动提供的机能,可将相应的成员函数声明为private并且不定义实现,当然,使用基类只声明更好
条款七:为多态基类声明虚析构函数
为了在多态情况下能完全析构
这个条款比较好理解,我直接给出小结:
小结:
- 带多态性质的基类应该声明一个虚析构函数。如果类带有虚函数,也应该有虚析构函数
- 如果类的设计目的不是作为基类,或不是为了多态性,就不该声明虚析构函数(从体积和可移植性考虑-虚函数表指针等)
条款八:别让异常逃离析构函数
大概理解是逃离后,内存泄漏。
小结:
- 析构函数绝对不要吐出异常(不让异常从析构函数中逃离),如果析构函数调用一个可能抛出异常的函数,析构函数应该捕捉异常,并吞下它们(不传播)或结束程序
- 如果客户需要对异常做出反应,你应该提供一个普通函数让其调用,而不是析构函数