1

假设我有类似的东西:

class obj001
{
public:
    obj001() {
        std::cout << "ctor == obj001" << std::endl;
    }

    ~obj001() {
        std::cout << "dtor == obj001" << std::endl;
    }
};

class obj002
{
public:
    obj002() {
        std::cout << "ctor == obj002" << std::endl;
    }

    ~obj002() {
        std::cout << "dtor == obj002" << std::endl;
    }
};

class packet001
{
public:
    packet001(): p01(NULL), p02(NULL) {
        /*p01 = new obj001;
        p02 = new obj002;
        throw "hahaha";*/

        std::cout << "CTOR == PACKET01" << std::endl;
    }

    ~packet001() {
        delete p01;
        delete p02;

        std::cout << "DTOR == PACKET01" << std::endl;
    }

    void init() {
        p01 = new obj001;
        p02 = new obj002;
        throw "hahaha";
    }

    obj001* p01;
    obj002* p02;
};

如果我这样做:

try
{       
    packet001 superpack;
    superpack.init();
}
catch(char* type)
{

}

然后init()失败了,Dtorsuperpack就会被调用。

但是如果我把内存分配放在 的 Ctor 里面superpack
(当然不要执行init()
那么在 Ctor 失败后,Dtor 就不会被调用,所以p01p02被泄露。

那么,最好使用类似的东西init()

谢谢!

4

4 回答 4

4

使用两阶段构造,普通构造后跟对init函数的外部调用,这意味着在构造之后你还不知道你手头是否有一个有效的对象。这意味着在任何获取此类对象作为参数的函数中,您都不知道该对象是否有效。这意味着很多额外的检查和不确定性,这反过来又意味着错误和额外的工作,因此,构造函数应该建立一个功能齐全、有效的对象。

进入“功能,有效”概念的一组假设称为类不变量

所以换句话说,更学术的措辞,构造函数的工作是建立类不变量,以便知道它在构造后成立。

然后在每个外部可用操作中保持对象有效,意味着它将继续保证有效。因此不需要进一步的有效性检查。该方案并非完全 100% 适用于所有对象(反例是表示文件的对象,其中任何操作都可能导致对象变得有效无效),但大多数情况下它是一个好主意并且效果很好,而且它在哪里不能直接工作,它适用于零件。

因此,在您的构造函数中,您应该通过以下方式之一确保清理:

  • 使用标准库容器(或第 3 方容器),而不是直接处理原始数组和动态分配。

  • 或者使用每个只管理一个资源的子对象。子对象可以是数据成员或基类。如果是数据成员,则可以是智能指针

  • 或者在最坏的情况下,使用try-catch进行直接清理。

在技​​术上也可以使用检查返回值的 C 思想在必要时调用直接清理。但上面的列表是按易用性和安全性递减的顺序排列的。C 风格的编码超出了该列表的底部。


C++ 语言创建者 Bjarne Stroustrup在他的第 3The C++ Programming Language的附录附录 E:标准库异常安全附录中写了一些关于这个主题的文章。只需下载 PDF,然后在您的 PDF 阅读器中搜索“init(”。您应该直接进入第 §E3.5 节,关于构造函数和不变量;请至少阅读关于使用的第 §E.3.5.1 节初始化()函数

正如 Bjarne 在那里列出的那样,……

[...] 有一个单独的init()函数是一个机会
[1] 忘记调用init()(§10.2.3),
[2] 忘记测试 的成功init()
[3]init()多次调用,
[4] 忘记init()可能会抛出一个例外,并且
[5] 在调用init().

我认为 Bjarne 的讨论非常适合初学者,整本书也是如此。

但是,请注意,根本没有提到两阶段构造的常见原因,即支持特定于派生类的初始化,这不是 Bjarne 的图片的一部分。这就是在许多 GUI 框架中进行两阶段初始化的原因。但是,确实存在一些具有 OK 单阶段初始化的 C++ GUI 框架,这证明这主要是一个教育问题——那些早期的 C++ 程序员根本不知道,或者不能假设他们的库用户会理解 C++ RAII。

于 2013-03-26T04:34:33.477 回答
1

The best thing is to avoid these kinds of allocations altogether. You can put instances directly within a class for many things. If you really need a pointer, you can use unique_ptr and shared_ptr for automatic memory management.

In your example, this would be fine:

struct packet001
{
    obj001 p01;
    obj002 p02;
};

If you need them to be pointers:

struct packet001
{
    packet001()
      : p01(new obj001),
        p02(new obj002)
    {
    }

    std::unique_ptr<obj001> p01;
    std::unique_ptr<obj002> p02;
};

The memory will automatically be freed in the destructor, and deallocations will happen properly if an exception occurs during construction.

于 2013-03-26T04:28:50.413 回答
0

Shouldn't you catch all exceptions in Ctor and clean up properly if exceptions arrive inside Ctor?

于 2013-03-26T04:30:23.523 回答
0

在构造函数可能多次失败并且我希望程序在失败后继续运行的情况下,我使用了两阶段构造或在构造函数清理中指出的各种其他方法;例如,试图构造一个读取文件的对象,其中文件名由用户提供。那里的构造函数可能会失败很多次,例如错误的用户输入。

但是 bad_alloc - 对于一个设计良好的程序来说应该很少见。如果内存分配失败,你到底要做什么?你的 C++ 程序很可能在那个时候注定要失败。为什么要在那个时候担心内存泄漏?现在您可以指出一些反例,即使在遇到错误分配后程序也可以继续运行,或者程序使用花哨的技术来避免错误分配,但是您的程序是其中之一吗?

于 2013-03-26T04:40:47.033 回答