• 二、构造,析构,赋值运算--条款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应该提供一个普通函数(而非在析构函数中)执行该操作。

  • 相关阅读:
    阴影及定位
    选择器高级、样式及布局
    css的导入与基础选择器
    html知识
    ORM
    python实现进度条
    MySQL单表查询
    一、HTTP
    mysql4
    练习——MySQL
  • 原文地址:https://www.cnblogs.com/love-jelly-pig/p/9627745.html
Copyright © 2020-2023  润新知