如果构造函数抛出异常,如何删除对象?

9

所以我们有一个构造函数,根据传递给它的参数可能会抛出异常,但我们不知道如何在这种情况下删除对象。 代码的重要部分:

try
{
    GameBase *gameptr = GameBase::getGame(argc, argv);
    if (gameptr == 0)
    {
        std::cout << "Correct usage: " << argv[PROGRAM_NAME] << " " << "TicTacToe" << std::endl;
        return NO_GAME;
    }
    else
    {
        gameptr->play();
    }
    delete gameptr;
}
catch (error e)
{
    if (e == INVALID_DIMENSION)
    {
        std::cout << "Win condition is larger than the length of the board." << std::endl;
        return e;
    }
}
catch (...)
{
    std::cout << "An exception was caught (probably bad_alloc from new operator)" << std::endl;
    return GENERIC_ERROR;
}

在第三行中,GameBase::getGame()调用了一个由GameBase派生出来的游戏的构造函数,并返回指向该游戏的指针,这些构造函数可能会抛出异常。问题是,如果发生这种情况,我们如何删除gameptr所指向的(部分?)对象?如果抛出异常,我们将退出gameptr的作用域,因为我们离开了try块,无法调用delete gameptr

4
如果构造函数抛出异常,那么对象就没有被构造。因此你不需要 delete 它。唯一需要担心的是如果在抛出异常之前已经分配了资源:析构函数将不会运行。 - smiling_nameless
3个回答

14

要评估异常安全性,需要在GameBase::getGame中提供对象构造的更多细节。

规则很简单,如果构造函数抛出异常,则不会创建对象,因此析构函数不会被调用。相关的内存分配也会被释放(即对象本身的内存)。

问题变成了,最初如何分配内存?如果是使用new GameBase(...),则无需释放或删除指针 - 运行时会自动释放内存。


为了澄清已经构造的成员变量的行动,它们将在“父”对象异常时被销毁。请考虑示例代码

#include <iostream>
using namespace std;
struct M {
    M() { cout << "M ctor" << endl; }
    ~M() { cout << "M dtor" << endl; }
};
struct C {
    M m_;
    C() { cout << "C ctor" << endl; throw exception(); }
    ~C() { cout << "C dtor" << endl; }
};
auto main() -> int {
    try {
        C c;
    }
    catch (exception& e) {
        cout << e.what() << endl;
    }
}

输出结果为:

M ctor
C ctor
M dtor
std::exception
如果要动态分配 M m_ 成员,建议使用 unique_ptrshared_ptr 而不是裸指针,并允许智能指针为您管理对象;如下所示;
#include <iostream>
#include <memory>
using namespace std;
struct M {
    M() { cout << "M ctor" << endl; }
    ~M() { cout << "M dtor" << endl; }
};
struct C {
    unique_ptr<M> m_;
    C() : m_(new M()) { cout << "C ctor" << endl; throw exception(); }
    ~C() { cout << "C dtor" << endl; }
};

此处的输出与上面的输出相同。


2
正确。任何成员变量都是独立的对象,一旦构造完成,如果父对象抛出异常,则会被销毁。 - Niall
@TartanLlama,刚刚看到我的回答。 - hr0m
2
如果您在单个语句中使用new创建多个对象,则可能会导致内存泄漏等问题。通常不建议这样做。如果构造函数有多个需要使用new创建的参数,则最好将它们创建并分配给资源管理器(例如std::unique_ptr),然后将它们“移动”到对象中(移动或分离)。如果出现任何异常,RAII机制将清理资源。 - Niall
@Niall 如果在结构体C中,不是有一个M对象,而是一个M指针,且在C构造函数中出现了m_ = new M(); 抛出错误之前,那么你是否需要自己释放这块内存?另外,非常感谢你的帮助。 - Kerry
是的,它不会被释放。通过将 M 指针设置为 std::unique_ptr 这样的智能指针来解决这个问题。这将使其自动化。 - Unimportant
显示剩余4条评论

5
当你写下Foo* result = new Foo()时,编译器将其转换为以下代码的等效形式:
void* temp = operator new(sizeof(Foo)); // allocate raw memory
try {
  Foo* temp2 = new (temp) Foo(); // call constructor
  result = temp2;
} catch (...) {
  operator delete(temp); // constructor threw, deallocate memory
  throw;
}

因此,如果构造函数抛出异常,您无需担心分配的内存。但请注意,这不适用于在构造函数内分配的额外内存。析构函数仅针对构造函数完成的对象调用,因此您应该立即将所有分配放入小包装对象(智能指针)中。

当你说分配时,你是指使用 new 分配的内存,对吗? - Kerry
实际上,我指的是任何资源分配。使用 "new"、 "malloc"、 "VirtualAlloc" 分配的内存、文件句柄、套接字、数据库连接等等。这只是 RAII 原则的重新表述。 - Sebastian Redl
如果构造函数抛出异常,would result指向nullptr吗? - Matias Chara
@MatiasChara result 不会受到影响。它的值与新表达式之前相同。 - Sebastian Redl

-1
如果你在构造函数中抛出异常,那么对象将不会被构造,因此你需要负责释放已分配的资源。这甚至更进一步!考虑以下代码。
int a = function(new A, new A);

在编译器中,A的分配和构造顺序由编译器决定。如果A的构造函数可能会抛出异常,那么你可能会遇到内存泄漏的问题!

编辑: 请使用以下代码:

try{
auto first = std::make_unique<A>();
auto second = std::make_unique<A>();
int a = function(*first, *second);
...

所以我的想法是,如果我在构造函数中使用“new”分配资源,那么我将不得不在构造函数内的catch块中删除它们?如果实例变量没有使用new分配,而是堆栈变量呢? - Kerry
栈变量没问题。成员变量也应该没问题。你只需要在构造函数中捕获并释放,然后重新抛出异常,这样上层代码也可以做出反应。你可以查看fstream中的代码,或者任何使用RAII原则的类的代码。 - hr0m
@K.Li,这里的问题在于实现可能选择为两个'A'分配内存,然后调用其中一个构造函数。如果这个过程中出现异常,那么抛出异常的'A'的内存将被回收,但另一个'A'的内存则不会。 - TartanLlama
1
@hr0m 更好的解决方案是将中间的 A 存储在 std::unique_ptr 中。这样,如果 second 构造函数抛出异常,则 first 将被销毁并回收其内存。 - TartanLlama
2
你的代码如何解决内存泄漏问题?如果第二个抛出异常,第一个仍然会泄漏。将分配移动到单独的行不足以解决问题。 - Benjamin Lindley

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接