条款05:了解C++默默编写并调用哪些函数
直入正题:4个函数。
- default构造函数。
- copy构造函数。
- copy assignment操作符。(operator=)
- 析构函数。
特点:
1. 它们都是public且inline的。
2. 它们只有在被需要(被调用)时才会创建出来。
3. 编译器为我们创建的是一个non-virtual版本。
注意事项:
编译器为我们产生的都是最简单的函数,考虑以下有引用变量的场景:
template<class T>
class NameObject
{
public:
NameObject(string &name,const T value);
...
private:
string &nameValue; // 引用变量
const T objectVale;
}
NameObject类中有一reference,此时我们只声明了一个构造函数,并未声明拷贝构造函数,拷贝赋值运算符,析构函数,由编译器负责生成。
执行以下语句:
NameObject<int> p("newDog",2);
NameObject<int> s("oldDog",36);
p = s; // 编译器拒绝执行!!!
上述代码中,将s拷贝给p,一开始p中的引用变量nameValue已经绑定了一个变量,如果这个语句成功执行,那么引用所绑定的对象将会被更改,这是不合法的。所以编译器会拒绝执行这一行语句!
作者总结:
编译器可以暗自为class创建default构造函数,copy构造函数,copy assignment运算符以及析构函数。
个人总结:
熟记这几个编译器默认会给出的函数(倘若我们自己编写了,编译器将不再提供)。但是他们仅仅只是简单的几个函数:
- 比如默认构造函数和析构函数都是没有函数体的空函数。
- 拷贝构造函数只是仅仅做了拷贝每一个bit,但是对于上述有引用的情况是不行的。
条款06:若不想使用编译器自动生成的函数,就该明确拒绝。
这个条款的适用背景在于:
某个类的对象,你并不希望它能够被复制(复制到另外一个对象之中),你希望它是独一无二的。
(就我目前接触来说,还不知道什么情形会有这样的独一无二的做法,当然我觉得这个和单例模式并不同。)
现在我们希望做到不被复制,首先我们想到不去声明这个函数即可,但是问题在于,拷贝构造函数和拷贝赋值运算符是编译器会帮我们生成的,这就形成了一个矛盾的现象。
问题就变成了:如何做到这copy构造函数和copy assignment运算符不被调用?
回顾条款5,默认生成的拷贝构造函数和拷贝赋值运算符是public的,如果我们不想要被调用,只需要自己声明一个private类型即可, 可以为空。如果需要被内部调用的话,可以写成真正的函数。但是友元函数还是可以调用,所以编程的时候需要注意不被友元函数调用或者不要有友元函数。
编写一个base class来负责拒绝被复制
class Uncopyable
{
protected:
Uncopyable();
~Uncopyable();
private:
Uncopyable(const Uncopyable &);
Uncopyable &operator=(const Uncopyable &);
}
注意:
- 拷贝构造函数的参数要使用常量类引用。
- 拷贝赋值运算符的返回值需要是一个当前类的引用,才能连锁赋值。
我们需要的不能被复制的类,只需要继承Uncopyable类就可以不让编译器实现,又可以不被外部调用。
作者总结
为驳回编译器自动提供的机能,可将相应的成员函数声明为private并且不予实现。使用像Uncopyable的base class也是一种做法。
条款07:为多态基类声明virtual析构函数
简单的概述这样做的原因:
class BaseClass
{
...
~BaseClass(){ ... }
}
class DriveClass : public BaseClass
{
...
~DriveClass(){ ... }
}
BaseClass *p = new DriveClass;
delete p;
上述代码中,基类的析构函数不是一个虚函数,当我们delete p的时候,真正被delete的是BaseClass部分。而调用者的真正意图在于析构掉DriveClass部分。
这可是形成资源泄漏,败坏之数据结构,在调试器上浪费许多时间的绝佳途径。
故:析构函数尽可能并推荐被声明成一个virtual函数。如果并不是一个virtual函数,那么它可能并不打算被继承。
虚函数在《Effective C++》中的介绍:
- 有一个vptr指针指向一个由函数指针构成的数组。称为vtbl(虚表)。
- 每一个带有虚函数的class都有一个虚表。
- 当对象调用某个虚函数,编译器在虚表中寻找适当的函数指针进行调用。
- 如果一个类中含有虚函数,那么就多了一个虚指针,指向一个虚表。所以类的大小就会多出一个指针的大小,32位机器上为4个字节,64为机器上为8个字节。
另外:标准string类的析构函数是一个non-virtual析构函数。
假设析构函数是一个pure virtual函数,需要注意?
析构的顺序是由下而上的:最底层的子类先析构,然后析构上一层基类,直到首层的Base Class。当我们的最原始的基类是一个纯虚函数的时候:
virtual ~ALOW() = 0;
析构的最后是执行此基类的析构函数,故此析构函数必须又定义。所以我们需要给它一个函数体,让它执行。又由于是一个抽象基类,所以我们只要给它一个空函数体即可。
ALOW::~ALOW()
{
}
作者总结
polymorphic(带多态性质的)base classes应该声明一个virtual析构函数。如果class带有任何的virtual函数,它就应该拥有一个virtual函数。
Classes的设计目的如果不是作为base classes使用,或不是为了具备多态性,就不该声明virtual析构函数。
个人总结:
对于作者的第一条总结:
(1) 因为virtual函数是作为一个实现多态的机制,如果我们声明了,就应该有子类去继承这个类,并重写虚函数以实现多态。这才是我们的目的。
(2) 当我们继承这个类了,就应该声明其析构函数为virtual函数,否则析构子类的时候可能会造成基类被析构,而子类却并不被析构。
条款08:别让异常逃离析构函数
1.1 众所周知,析构函数是用来进行“善后”,释放内存等。如果在析构函数中发生了异常,那么久会造成内存并没有被释放,从而导致内存泄漏。
1.2
在两个异常同时存在的情况下,程序若不是结束执行就是导致不明确的行为。
2.1 数据库连接的例子。
class DBCon
{
public:
...
~DBCon()
{
db.close();
}
private:
DBConnection db;
}
这份代码是十分合乎常理的,为了防止内存泄漏。如果让异常逃离了析构函数,那么这块没有被释放的内存将没有任何东西去管控,很容易造成内存泄漏。
我们的解决方案是:
(1) 给客户端提供一个close函数。客户端通过这个接口,可以自己去关闭数据库的连接。
(2) 再捕捉异常。如果客户端调用并没有正确关闭数据库。我们在析构函数中再次选择将它关闭。如果析构函数中还是抛出了异常,要记录此次异常,根据实际情况结束程序或者是吞下这个异常。
示例代码如下:
class DBCon
{
public:
...
close()
{
db.close();
bIsClose = true;
}
~DBCon()
{
if(!bIsClose)
{
try{
db.close();
}
catch(...){
//记录下调用失败信息
}
}
db.close();
}
private:
DBConnection db;
bool bIsClose;
}
如果某个操作可能在失败时抛出异常,且又存在某种需要必须处理该异常,那么这个异常必须来自析构函数以外的某个函数。因为析构函数吐出异常,那么就有风险造成过早结束程序或者发生不明确的行为。
作者总结
析构函数绝不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下他们或者结束程序。
如果客户端需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数(而非在析构函数中)执行该操作。