• 【原创】Performanced C++ 经验规则 第三条:你不知道的构造函数(下)


    第三条:你不知道的构造函数(下)

    前面两篇,我们已经讨论了C++构造函数中诸多细枝末节,但百密一疏,还有一些地方我们没有考虑到。这一篇将对这些问题进行完结。

    7、构造函数中的异常

    当你在构造函数中写代码的时候,你有没有想过,如果构造函数中出现异常(别告诉我,你不抛异常。“必要”时系统会替你抛的),那会出现怎样的情况?

    对象还能构建完成吗?构造函数中已经执行的代码产生的负面效应(如动态分配内存)如何解决?对象退出其作用域时,其析构函数能被调用吗?

    上述这些问题,正是构造函数中产生异常要面临的问题。让我们先看结论,再分析过程:尽可能不要在构造函数中产生(抛出)异常,否则,一定会产生问题

    我们先看一段代码:

     1 #include <iostream>
     2 #include <exception>
     3 #include <stdexcept>
     4 using namespace  std;
     5 
     6 
     7 class ConWithException
     8 {
     9 public:
    10     ConWithException() : _pBuf(NULL)
    11     {
    12         _pBuf = new int[100];
    13         throw std::runtime_error("Exception in Constructor!");
    14     }
    15 
    16     ~ConWithException()
    17     {
    18         cout << "Destructor!" << endl;
    19         if( _pBuf != NULL )
    20         {
    21             cout <<  "Delete buffer..." << endl;;
    22             delete[] _pBuf;
    23             _pBuf = NULL;
    24         }
    25     }
    26 
    27 private:
    28     int* _pBuf;
    29 };
    30 
    31 int main(int argc, char** argv)
    32 {
    33     ConWithException* cwe = NULL;
    34     try
    35     {
    36         cwe = new ConWithException;
    37     }
    38     catch( std::runtime_error& e )
    39     {
    40         cout<< e.what() << endl;
    41     }
    42 
    43     delete cwe;
    44 
    45     return 0;
    46 }
    47                                                         

    这段代码运行结果是什么呢?

    输出

    1 Exception in Constructor!

    输出“Exception in Constructor!"说明,我们抛出的异常已经成功被捕获,但有没有发现什么问题呢?有一个很致命的问题,那就是,对象的析构函数没有被调用!也就是说,delete cwe这一句代码没有起任何作用,相当于对delete NULL指针。再往上推,我们知道cwe值还是初始化的NULL,说明对象没有成功的构建出来,因为在构造函数中抛出了异常,终止了构造函数的正确执行,没有返回对象。即使我们把cwe = new ConWithException换成在栈中分配(ConWithException cwe;),仍是相同的结果,但cwe退出其作用域时,其析构函数也不会被调用,因为cwe根本不是一个正确的对象!继续看,在这个构造函数中,为成员指针_pBuf动态申请了内存,并计划在析构函数中释放这一块内存。然而,由于构造函数抛出异常,没有返回对象,析构函数也没有被调用,_pBuf指向的内存就发生了泄露!每调用一次这个构造函数,就泄露一块内存,产生严重的问题。现在,你知道了,为什么不能在构造函数中抛出异常,即使没有_pBuf这样需要动态申请内存的指针成员存在。

    然而很多时候,异常并不是由你主动抛出的,也就是说,将上述构造函数改造成这样:

       ConWithException() : _pBuf(NULL)
       {
            _pBuf = new int[100];
       }

    这是我们十分熟悉的格式吧?没错,但是,这样的写法仍然可能产生异常,因为这取决于编译器的实现。当动态内存分配失败时,编译器可能返回一个NULL指针(这也是惯用方式),OK,那没有问题。但是,有些编译器也有可能引发bad_alloc异常,如果对异常进行捕获(通常也不会这样做),结果将同上述例子所示。而如果未对异常进行捕获,结果更加糟糕,这将产生Uncaught exception,通常将导致程序终止。并且,此类问题是运行阶段可能出现的问题,这将更难发现和处理。

    说了半天,就是认为上述写法,还不够好,不OK,接下来讲述解决方案。

    解决方案一:使用智能指针shared_ptr(c++0x后STL提供,c++0x以前可采用boost),注意,在此处不能使用auto_ptr(因为要申请100个int,而即使申请的是单个对象,也不建议使用auto_ptr,关于智能指针,本系列后面的规则会有讲述);

    解决方案二:就是前面多次提到的,采用"工厂模式"替换公有构造函数,从而尽可能使构造函数“轻量级“

    class ConWithException //为和前面比对,类名没改,糟糕的类名
    {
    public:
        ConWithException* factory(some parameter...)
        {
            ConWithException* cwe = new ConWithException;
            if(cwe)
            {
                cwe->_pBuf = new int[100];
                //other initialization...
            }
            return cwe;
        }
        
        ~ConWithException()
        {
            if(cwe->_pBuf)
            {
                delete[] cwe->_pBuf;
                _pBuf = NULL;
            }
            //other destory process...
        }
    private:
        ConWithException() : _pBuf(NULL) {} //如果有非静态const成员还需要在初始化列表中进行初始化,否则什么也不做
        int* _pBuf;
    };

    使用“工厂模式”的好处是显而易见的,上述构造函数中异常的问题可以得到完美解决?why?因为构造函数十分轻量级,可轻松的完成对象的构建,“重量级”的工作都交由“工厂”(factory)方法完成,这是一个公有的普通成员函数,如果在这个函数中产生任何异常,因为对象已经正确构建,可以完美的进行异常处理,也能保证对象的析构函数被正确地调用,杜绝memory leak。构造函数被声明为私有,以保证从工厂“安全”地产生对象,使用“工厂模式”,还可以禁止从栈上分配对象(其实Java、Objective-C都是这么做的),在必要的时候,这会很有帮助。

    8、构造函数不能被继承:虽然子类对象中包含了基类对象,但并不能代表构造函数被继承,即,除了在子类构造函数的初始化列表里,你可以显式地调用基类的构造函数,在子类的其它地方调用父类的构造函数都是非法的。

    9、当类中有需要动态分配内存的成员指针时,需要使用“深拷贝“重写拷贝构造函数和赋值操作符,杜绝编译器“用心良苦”的产生自动生成版本,以防资源申请、释放不正确。

    10、除非必要,否则最好在构造函数前添加explicit关键字,杜绝隐式使构造函数用作自动类型转换。

    终于写完了,这三篇有关构造函数的“经验”之谈,其实,这些问题,也是老生常谈了。经过这三篇的学习,为敲开C++的壁垒,我们又添加了一把强有力的斧头。

    iCC Develop Center
  • 相关阅读:
    Mysql:mysqlslap:基于5.7.36的修改版:不自动创建测试schma、不删除测试schema、默认生成初始化记录数为1
    Mysql:mysqlslap:自带的简单压力测试工具:使用、bug等:续(为了方便阅读)
    《微服务设计》
    E: Unable to locate package vim
    'dependencies.dependency.version' for org.testng:testng:jar is either LATEST or RELEASE (both of them are being deprecated)
    Linux
    硬盘IO性能估算入门
    [转载]在 Linux 中使用 fsck 命令修复文件系统
    阿里巴巴 P8、P9 及以上到底是什么水平?
    课程表-python
  • 原文地址:https://www.cnblogs.com/ccdev/p/2830976.html
Copyright © 2020-2023  润新知