【问题标题】:Will the below code cause memory leak in c++下面的代码会导致c ++中的内存泄漏吗
【发布时间】:2010-09-13 22:30:25
【问题描述】:
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();
}

上面的代码怎么样?构造函数抛出后如何避免内存泄漏?

【问题讨论】:

  • 我知道,我有一个可怕的本性,我无法抗拒吹毛求疵。我没办法。我的 2 美分:声明 objsomeclass= someclass();是不必要的。在构造函数的主体中,objsomeclass 已经默认初始化。下面的 objsomeclass( someclass()) 也没有意义。
  • 我同意,但认为 someclass 有一个明确的构造函数。我想专注于对象是在构造函数中创建的
  • 是的,我知道这只是一个例子。这就是为什么我称之为挑剔。顺便说一句,构造函数 base() 可能是公共的 :)

标签: c++ exception memory-leaks constructor


【解决方案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;
    }
}

祝你好运, 茨维。

【讨论】:

  • 第一个示例中的异常未被捕获,因此不会发生堆栈展开,也不会调用析构函数。如果您将 B b; 包装在 try catch 中,则析构函数将按预期调用。
【解决方案2】:

两个新的都将被泄露。

将堆创建对象的地址分配给命名智能指针,以便在抛出异常时调用的智能指针析构函数中将其删除 - (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 析构函数将在构造函数中抛出异常时堆栈展开时被调用,并且这些析构函数将释放分配的内存。

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

对于使用 (non-plcaement) new 的普通内存分配,如果构造函数抛出异常,则由 operator new 分配的内存会自动释放。至于为什么要释放单个成员(响应 cmets 对 Mike B 的回答),自动释放仅适用于在新分配的对象的构造函数中引发异常时,而不是在其他情况下。此外,释放的内存是为对象成员分配的内存,而不是您可能在构造函数内部分配的任何内存。 ie 它会释放成员变量 apintobjsomeclasspsomeclass 的内存,但不会释放从 new someclass()new int() 分配的内存。

【讨论】:

  • shared_ptr 如果您拥有该对象并且永远不会放弃共享所有权,那就太过分了。使用 std::auto_ptr 进行简化
  • //把问题改成base *temp = new base();
  • 而且 boost::scoped_ptr 可能比 auto_ptr 更好,它有自己的蠕虫罐。
  • 以智能指针为例,这是一个(排序)随机选择。它足够通用,人们不必担心解释什么时候不应该在像这样的快速示例中使用它。但是是的,如果可以使用更简单的智能指针,那就去做吧。
【解决方案3】:

是的,它会泄漏内存。当构造函数抛出时,不会调用析构函数(在这种情况下,您不会显示释放动态分配对象的析构函数,但假设您有一个)。

这是使用智能指针的主要原因 - 由于智能指针是成熟的对象,它们将在异常堆栈展开期间调用析构函数并有机会释放内存。

如果你使用类似 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;
    }
}

而且您不会有内存泄漏(默认 dtor 也会清理动态内存分配)。


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

base* temp = new base();

声明):

当在构造函数中抛出异常时,在正确处理对象的中止构造中可能发生的资源分配方面,您应该注意几件事:

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

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

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

【讨论】:

  • 在 Boost 中引入内存管理不是很愚蠢吗?
  • 也许吧,但是 scoped_ptr 在 TR1 中并且将在 C++09 中,所以无论如何都应该学习它。 Boost 中具有 scoped_ptr 的部分只是一堆标题。最后,对于这个简单的示例,您可以使用 auto_ptr 代替,但 auto_ptr 可能是应该避免的。
  • 基类的dtor会被调用一次吗?下面行 base *temp = new base(); 会发生什么
  • “base* temp = new base;”不会发生任何事情行 - 异常将导致尝试分配新基础对象的内存被释放(即使不会调用 dtor)。
  • 那么你应该关心释放单个成员的内存吗?
【解决方案4】:

是的,该代码会泄漏内存。引发异常时,不会释放使用“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;
 }

【讨论】:

  • 而不是使用指针使用对象(智能指针)会让事情变得更好?因为当块中抛出异常时,自动清除对象。
  • 智能指针更好。也替换“raise”;用'扔;'重新抛出当前异常。
【解决方案5】:

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

RWendi

【讨论】:

  • 你能详细说明一下戴夫摩尔吗?是关于“不需要清理整数”部分吗?这背后的原因是,与类内存指针相比,Int 内存指针的成本并不高,这就是为什么我说它没有必要清理它。
  • 它们都泄漏了;成本不是问题。问题是它是否泄漏。如果那段代码执行数千或数百万次,那么这笔小成本就会增加。即使“成本”是相关的,也不是 指针的 大小产生影响,而是指向实体的大小。例如,sizeof(someclass) == sizeof(int) 是可能的。而且你不是在删除指针——你是在删除指向的实体。
【解决方案6】:

您“新建”的所有内容都需要删除,否则会导致内存泄漏。所以这两行:

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

会导致内存泄漏,因为你需要这样做:

delete pint;
delete psomeclass;

在 finally 块中以避免它们被泄露。

另外,这一行:

base temp = base();

没有必要。你只需要这样做:

base temp;

不需要添加“= base()”。

【讨论】:

  • C++ 中没有“finally”块这样的东西
  • 没错,您可能有权访问它,也可能无法访问它,具体取决于您的 C++ 风格 - 如果没有,无论采用何种代码路径,您都必须确保删除分配。
  • 您关于额外初始化的评论是错误的。结果对象只会被初始化一次,不会被复制。
【解决方案7】:

如果你在构造函数中抛出,你应该清理在调用 throw 之前的所有内容。如果您使用继承或抛出析构函数,您真的不应该这样做。这种行为很奇怪(手边没有我的标准,但它可能是未定义的?)。

【讨论】:

  • 不确定它是否实际上是未定义的,但肯定是非常危险的,因为在引发异常的情况下,在堆栈展开期间会调用析构函数。如果您提出一个异常同时另一个已经引发,我知道的每个 C++ 运行时都会终止应用程序。
  • 在异常处理期间引发的析构函数中未捕获的异常导致调用 std::terminate(),默认情况下调用 std::abort()。可以覆盖默认行为。
  • 即使可以覆盖默认行为,您的版本仍然无法返回到应用程序,它仍然必须退出。
猜你喜欢
  • 1970-01-01
  • 2018-08-17
  • 1970-01-01
  • 1970-01-01
  • 2012-01-18
  • 1970-01-01
  • 1970-01-01
  • 2020-01-16
  • 1970-01-01
相关资源
最近更新 更多