二、构造,析构,赋值运算--条款05-08

条款05:了解C++默默编写并调用哪些函数

直入正题:4个函数。

  1. default构造函数。
  2. copy构造函数。
  3. copy assignment操作符。(operator=)
  4. 析构函数。

特点:

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应该提供一个普通函数(而非在析构函数中)执行该操作。

原文地址:https://www.cnblogs.com/love-jelly-pig/p/9627745.html