下面的代码会在C++中导致内存泄漏吗?

20
class someclass {};

class base
{
    int a;
    int *pint;
    someclass objsomeclass;
    someclass* psomeclass;
public:
    base()
    {
        objsomeclass = someclass();
        psomeclass = new someclass();
        pint = new int(); 
        throw "constructor failed";
        a = 43;
    }
}

int main()
{
    base temp();
}

在上面的代码中,构造函数抛出异常。哪些对象将泄漏,如何避免内存泄漏?

int main()
{
    base *temp = new base();
}

在上面的代码中怎么办?构造函数抛出异常后如何避免内存泄漏?


1
我知道,我有一个可怕的习惯,就是无法抵制吹毛求疵。我控制不了它。我的建议是:语句objsomeclass = someclass(); 是不必要的。在构造函数的主体中,objsomeclass已经默认初始化了。下面的objsomeclass( someclass())也没有意义。 - Maciej Hehl
我同意,但是我认为someclass有一个显式构造函数。我想要关注的是在构造函数中创建对象。 - yesraaj
是的,我知道这只是一个例子。这就是为什么我称之为吹毛求疵。顺便说一句,构造函数base()可以是公共的 :) - Maciej Hehl
7个回答

41

是的,它会泄露内存。当构造函数抛出异常时,没有析构函数被调用(在这种情况下,您没有展示释放动态分配对象的析构函数,但让我们假设您有一个)。这是使用智能指针的主要原因——由于智能指针是完整的对象,它们将在异常的栈展开期间调用析构函数并有机会释放内存。

如果您使用类似于Boost scoped_ptr<> 模板之类的东西,您的类可能看起来更像:

class base{
    int a;
    scoped_ptr<int> pint;
    someclass objsomeclass;
    scoped_ptr<someclass> psomeclass;
    base() : 
       pint( new int),
       objsomeclass( someclass()),
       psomeclass( new someclass())

    {
        throw "constructor failed";
        a = 43;
    }
}

并且您将不会有内存泄漏(默认的析构函数也会清理动态内存分配)。


总结一下(希望这也回答了关于“

”的问题):

base* temp = new base();

构造函数中抛出异常时,需要注意如何正确处理已被分配但未完成构造对象的资源分配问题:

  1. 正在构造的对象的析构函数不会被调用。
  2. 该对象类中包含的成员对象的析构函数将被调用。
  3. 正在构造的对象的内存将被释放。

这意味着,如果您的对象拥有资源,则有两种方法可用于清理在构造函数抛出异常时可能已经获取的这些资源:

  1. 捕获异常、释放资源,然后重新抛出异常。这可能难以正确实现,并可能成为维护问题。
  2. 使用对象来管理资源生命周期(RAII),并将这些对象用作成员。当您对象的构造函数抛出异常时,将调用成员对象的析构函数,并有机会释放它们负责的资源的生命周期。

仅为了获取内存管理而拉取Boost不是很愚蠢吗? - John Millikin
即使我只有一个派生类,基类的析构函数也会被调用吗? 下面这行代码会发生什么情况? base *temp = new base(); - yesraaj
那么为什么要关心释放单个成员的内存呢? - yesraaj
@Loki 你能定义一下“完全构造”吗?除了 a 之外的所有成员不是都在执行 throw 行时被构造了吗? - Nathan
@Nathan: 完全:执行线程已退出构造函数。在这种情况下,是的,所有成员都已完全构造。但是,如果成员在其构造过程中抛出异常(即在初始化列表期间),则可能不会构造所有其他成员。因此,如果您将基类对象用作另一个类的成员,则可能会遇到此问题。 - Martin York
显示剩余4条评论

5

两个消息都会被泄露。

将堆创建的对象的地址分配给命名智能指针,以便在抛出异常时调用智能指针析构函数进行删除 - (RAII)。

class base {
    int a;
    boost::shared_ptr<int> pint;
    someclass objsomeclass;
    boost::shared_ptr<someclass> psomeclass;

    base() :
        objsomeclass( someclass() ),
        boost::shared_ptr<someclass> psomeclass( new someclass() ),
        boost::shared_ptr<int> pint( new int() )
    {
        throw "constructor failed";
        a = 43;
    }
};

现在,在构造函数中抛出异常时,将会调用psomeclasspint的析构函数,并且这些析构函数将会释放已分配的内存。请注意不要删除HTML标记。
int main(){
    base *temp = new base();
}

对于普通的内存分配,使用(非放置)new,在对象构造函数抛出异常时,由操作符new分配的内存会自动释放。对于为什么要释放单个成员(回应Mike B答案中的评论),自动释放只适用于在分配新对象时构造函数抛出异常的情况,而不是其他情况。此外,被释放的内存是为对象成员分配的内存,而不是可能在构造函数中分配的任何内存。即它将释放成员变量a、pint、objsomeclass和psomeclass的内存,但不会释放从“new someclass()”和“new int ()”分配的内存。

如果您拥有对象并永远不会放弃共享所有权,则 shared_ptr<> 是过度设计。使用 std::auto_ptr<> 可以简化代码。 - Martin York
//将问题更改为: base *temp = new base(); //已翻译 - yesraaj
boost::scoped_ptr<> 可能比 auto_ptr<> 更好,后者有它自己的问题。 - Michael Burr
这在智能指针方面是一种(有点)随意的选择作为示例。它的通用性足以让人不必担心在像这样的快速示例中解释何时不应使用它。但是,如果可以使用更简单的智能指针,则应该使用。 - KTC

1

我认为最佳答案是错误的,仍然会泄漏内存。 如果构造函数抛出异常(因为它从未完成初始化,可能有些成员从未达到其构造函数调用),则类成员的析构函数将不会被调用。 它们的析构函数仅在类的析构函数调用期间才会被调用。这是有意义的。

这个简单的程序证明了这一点。

#include <stdio.h>


class A
{
    int x;

public:
    A(int x) : x(x) { printf("A constructor [%d]\n", x); }
    ~A() { printf("A destructor [%d]\n", x); }
};


class B
{
    A a1;
    A a2;

public:
    B()
    :   a1(3),
        a2(5)
    {
        printf("B constructor\n");
        throw "failed";
    }
    ~B() { printf("B destructor\n"); }
};


int main()
{
    B b;

    return 0;
}

使用 g++ 4.5.2 编译器,输出如下:

A constructor [3]
A constructor [5]
B constructor
terminate called after throwing an instance of 'char const*'
Aborted

如果您的构造函数在中途失败,那么处理它就是您的责任。更糟糕的是,异常可能会从您的基类构造函数中抛出! 处理这些情况的方法是使用“函数尝试块”(但即使如此,您也必须仔细编写部分初始化对象的销毁代码)。
解决问题的正确方法应该像这样:
#include <stdio.h>


class A
{
    int x;

public:
    A(int x) : x(x) { printf("A constructor [%d]\n", x); }
    ~A() { printf("A destructor [%d]\n", x); }
};


class B
{
    A * a1;
    A * a2;

public:
    B()
    try  // <--- Notice this change
    :   a1(NULL),
        a2(NULL)
    {
        printf("B constructor\n");
        a1 = new A(3);
        throw "fail";
        a2 = new A(5);
    }
    catch ( ... ) {   // <--- Notice this change
        printf("B Cleanup\n");
        delete a2;  // It's ok if it's NULL.
        delete a1;  // It's ok if it's NULL.
    }

    ~B() { printf("B destructor\n"); }
};


int main()
{
    B b;

    return 0;
}

如果您运行它,您将获得预期的输出,只有分配的对象被销毁和释放。
B constructor
A constructor [3]
B Cleanup
A destructor [3]
terminate called after throwing an instance of 'char const*'
Aborted

如果您愿意,仍然可以使用智能共享指针来解决它,并进行额外的复制。编写类似于以下构造函数:
class C
{
    std::shared_ptr<someclass> a1;
    std::shared_ptr<someclass> a2;

public:
    C()
    {
        std::shared_ptr<someclass> new_a1(new someclass());
        std::shared_ptr<someclass> new_a2(new someclass());

        // You will reach here only if both allocations succeeded. Exception will free them both since they were allocated as automatic variables on the stack.
        a1 = new_a1;
        a2 = new_a2;
    }
}

祝你好运,Tzvi。


2
你第一个例子中的异常没有被捕获,因此不会发生堆栈展开,也不会调用任何析构函数。如果你在 B b; 周围加上 try catch,则可以按预期调用析构函数。 - sourcenouveau

0

如果你在构造函数中抛出异常,那么在抛出异常之前应该清除掉所有的东西。但如果你使用继承或者在析构函数中抛出异常,这其实不是一个好的做法。这种行为非常奇怪(我手头没有我的标准,但可能是未定义的?)。


不确定它是否真正未定义,但肯定非常危险,因为在引发异常事件期间,析构函数会在堆栈展开期间被调用。如果您在已引发异常的同时再次引发异常,则我所知的每个 C++ 运行时都将终止应用程序。 - John Millikin
析构函数中未捕获的异常在异常处理期间引发,导致调用std::terminate(),默认情况下会调用std::abort()。默认行为可以被覆盖。 - KTC
尽管默认行为可以被覆盖,但你的版本仍然无法返回到应用程序,它仍然需要退出。 - Greg Rogers

0

是的,那段代码会泄漏内存。使用“new”分配的内存块在引发异常时不会被释放。这是RAII背后的动机之一。

为避免内存泄漏,请尝试像这样做:

psomeclass = NULL;
pint = NULL;
/* So on for any pointers you allocate */

try {
    objsomeclass = someclass();
    psomeclass = new someclass();
    pint = new int(); 
    throw "constructor failed";
    a = 43;
 }
 catch (...)
 {
     delete psomeclass;
     delete pint;
     throw;
 }


使用对象(智能指针)而不是指针会使事情变得更好吗?因为每当块中抛出异常时,自动对象都会被清除。 - yesraaj
智能指针更好。同时,将“raise”替换为“throw”,以重新抛出当前异常。 - Martin York

-2

所有你 "new" 的东西都需要被删除,否则你会导致内存泄漏。因此,这两行代码:

psomeclass = new someclass();
pint = new int(); 

会导致内存泄漏,因为你需要执行以下操作:

delete pint;
delete psomeclass;

在finally块中以避免它们被泄漏。
另外,这行代码:
base temp = base();

是不必要的。你只需要做:

base temp;

添加“= base()”是不必要的。


1
C++ 中没有“finally”块。 - John Millikin
真的,根据你使用的C++版本,你可能有或没有访问它 - 如果没有,你必须确保无论采取哪种代码路径,分配都会被删除。 - Colen
1
你关于额外初始化的评论是错误的。生成的对象只会被初始化一次,不会被复制。 - John Millikin

-3

你需要删除psomeclass... 清理整数不是必要的...

RWendi


请问Dave Moore能否详细说明一下?这和“不需要清理整数”部分有关吗?原因在于相比类内存指针,Int内存指针的成本较低,这就是为什么我说不需要清理它的原因。 - RWendi
它们都泄漏;成本不是问题。问题是它是否泄漏。如果那段代码执行了数千或数百万次,那么这点成本就会累加起来。即使“成本”相关,也不是指针的大小有所不同,而是指所指向的实体的大小。例如,sizeof(someclass)== sizeof(int)是可能的。而且你不是在删除指针 - 你正在删除所指向的实体。 - Chris Cleeland

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