目录
Know what functions C++ silently writes and calls.
Explicitly disallow the use of compiler-generated functions you do not want.
Declare destructors virtual in polymorphic base classes.
条款08:别让异常逃离析构函数..Prevent exceptions from leaving destructors.
条款05:了解C++默认编写并调用哪些函数
Know what functions C++ silently writes and calls.
在写类时,如果如果自己没声明,编译器就会为它声明一个copy构造函数,一个 copy assignment操作符和一个析构函数,如果没有声明任何构造函数,编译器也会为你声明一个default构造函数,所有这些函数都是public且inline,
因此,如果写下:
class Empty{};
等价于:
class Empty{
public:
Empty(){...}
Empty(const Empty&rhs){...}
~Empty(){...}
Empty& operator=(const Empty&rhs){...}
};
唯有当这些函数被调用,它们才会被编译器创建出来。
下面代码造成上述每一个函数被编译器产出:
Empty e1;//default构造函数
//析构函数
Empty e2(e1);//copy构造函数
e2=e1;//copy assignment 操作符
注意:编译器产出的析构函数是个non-virtual,除非这个class的base class 自身声明有virtual析构函数。copy构造函数和copy assignment操作符,编译器创建的版本只是单纯地将对象的non-static成员变量拷贝到目标对象。
假设有类NameObject定义如下:,其中nameValue是一个string的引用类型,objVal是一个const int 类型,倘若创建对象并进行赋值操作,编译器生成的赋值函数能改动引用吗?
答案是不行,C++拒绝编译那一行赋值动作,如果要在含引用成员的class内支持赋值操作,必须自己定义赋值操作符,同时,更改const 成员也是不合法的。还有一种情况是:基类的赋值操作符是private的,编译器不会为派生类生成赋值操作符,因为无法处理基类的成分。
总结:
编译器可以暗自为类创建默认构造函数,拷贝构造函数,赋值操作符,以及析构函数。
条款06:若不想使用编译器自动生成的函数,就该明确拒绝
Explicitly disallow the use of compiler-generated functions you do not want.
若要实现不能被拷贝的类,如果不声明拷贝构造函数和赋值运算符,编译器会自动生成,于是将拷贝构造函数和赋值运算符放在private,防止被调用,但是成员函数和友元函数仍可以调用,于是将其在private只声明不实现。这样一来,如果客户企图拷贝时,编译就会报错,如果不慎在成员函数或友元函数中调用时,链接器会报错。
将链接期的错误转移到编译期是可能的,而且更好(越早侦测出错误越好),只要将拷贝构造函数和赋值运算符在一个专门阻止拷贝的基类中声明为private就可以办到。为了防止类被拷贝,只要让类继承这个基类就可以了。这样一来,不管是成员函数,友元函数,或任何人尝试拷贝对象,编译器就会报错。
例如(基类代码):
class Uncopyable{
protected:
Uncopyable(){}
~Uncopyable(){}
private:
Uncopyable(const Uncopyable&);
Uncopyable& operator=(const Uncopyable&);
};
总结:
为驳回编译器自动提供的机能,将相应的成员函数声明为private并且不予实现。或者继承Uncopyable这样的基类也是一种做法。
条款07:为多态基类声明virtual析构函数
Declare destructors virtual in polymorphic base classes.
当基类指针指向派生类对象并delete指针的时候,并且基类的析构函数不是虚函数,所以实际发生的析构过程中,只调用了基类的析构函数,析构的派生类对象中的成分可能没被销毁干净,就会造成局部销毁,形成资源泄露。比如下面代码:
class TimeKeeper {
public:
TimeKeeper();
~TimeKeeper();
};
class Atomicclock : public TimeKeeper {
//...
};//原子钟
int main()
{
TimeKeeper* ptr = new Atomicclock;
//...
delete ptr;//资源泄漏
return 0;
}
为了解决这个问题,应当给基类的析构函数实现成虚函数。但是如果一个类不作为基类的话,就不应该让其析构函数作为虚函数,因为那样会增加其对象的体积,因为带有虚函数的类生成的对象中包含了所有的虚函数指针。正确代码:
class TimeKeeper {
public:
TimeKeeper();
virtual ~TimeKeeper();
};
class Atomicclock : public TimeKeeper {
//...
};//原子钟
int main()
{
TimeKeeper* ptr = new Atomicclock;
//...
delete ptr;//资源泄漏
return 0;
}
标准string不带任何virtual函数,如果你这么做:
class Specialstring :public std::string
{
//...
};
int main()
{
Specialstring* pss = new Specialstring("Impending Doom");
std:: string * ps;
//...
ps = pss;//基类指针指向子类对象
//...
delete ps;
return 0;
}
就会造成同样的资源泄漏,包括所有STL容器和不带虚析构函数的类,不要继承它们!
一种方便的做法,让类带有纯虚析构函数,但需要为这个函数提供一份定义,因为析构派生类时,最深层的析构函数先被调用,如果没有定义会发生链接错误。
class Abstract{
public:
virtual ~Abstract()=0;
};
Abstract::~Abstract(){}//编译器需要调用它
总结:
带多态性质的基类应该声明一个virtual析构函数。如果class带有任何virtual函数,它就应该拥有一个virtual析构函数。
类的设计目的如果不是作为基类使用,或不是为了具备多态性,就不该声明virtual析构函数。不要继承STL的所有容器和没有虚析构函数的类。
条款08:别让异常逃离析构函数..
Prevent exceptions from leaving destructors.
C++可以在析构函数中抛异常,但是不推荐这么做,因为这样有可能导致资源泄露和未定义行为。
比如:
Widget类的析构函数可能会抛异常,当v析构的时候,析构到第一个元素抛了异常,剩下9个元素应该被析构,否则资源泄漏,如果析构到第二个元素又抛异常,在C++中两个异常会导致不是程序终止就是未定义行为。所以C++不喜欢在析构函数中抛异常。
另一个场景:
实现一个负责数据库连接的类:
实现一个管理DBConnection资源的类:
当DBConn类析构的时候,close抛异常会跳出析构函数出现问题,有两种方法处理,让异常不离开析构函数。
方法一:抛异常就abort结束程序
方法二: 把这个异常吞掉
一种更好的处理办法是DBConn自己提供一个close函数,给使用者一个机会处理该close函数的异常,并在最后析构函数再次确保close调用成功了,如果析构函数还抛出异常,就继续吞掉异常或终止程序:
总结:
析构函数中不要抛异常,如果无法避免,就吞掉异常(不让异常传播)或终止程序。
如果客户需要对某个操作函数的异常做反应,类中应该提供普通函数(非析构)执行。