如何在try-catch块中释放内存?

26
我有一个简单的问题,希望能得到解答——当异常发生时,如何释放在try块中分配的内存?请考虑以下代码:
try
 {
  char *heap = new char [50];
        //let exception occur here
  delete[] heap;
 }
 catch (...)
 {
  cout << "Error, leaving function now";
  //delete[] heap; doesn't work of course, heap is unknown to compiler
  return 1;
 }

在堆分配内存后,如果在调用delete[] heap之前发生异常,我该如何释放内存?是否有规则不要在这些try .. catch块中分配堆内存?


8
为什么要使用 new,当你可以使用智能指针和支持 RAII 的容器呢? - Pavel Radzivilovsky
2
@Pavel - 智能指针和RAII容器在复杂使用中仍然存在问题,尤其是在循环引用导致内存泄漏和传递指针时出现奇怪的行为(这就是为什么auto_ptr并不常用)。如果你有一个明确的创建和删除点,并且希望它可以在之间以任何方式使用,那么使用原始指针没有任何问题。 - tloach
8
我完全不同意。C++足够强大,可以将清晰的抽象设计成清晰的类,并将异常友好的行为封装在内部,而不是暴露给用户。 - Pavel Radzivilovsky
7
这是我见过的最糟糕的 C++ 建议之一。代码中几乎不需要使用裸指针。任何裸指针都应该由对象包装和管理。这是因为使用 RAII 可以保证指针的生命周期(当它被包装时,例如 std::vector 永远不会泄漏),即使在存在异常的情况下也可以正确地控制。(使用裸指针在异常情况下实现正确性非常困难)。 - Martin York
2
@tloach:建议不要使用智能指针,因为在某些边缘情况下可能会出错,就像建议在拳击时不戴头盔一样,因为你仍然可能被打到脸上。在这两种情况下,如果你决定使用它们,你会受到的伤害会更少。 - David Rodríguez - dribeas
显示剩余2条评论
9个回答

36
学习RAII(资源获取即初始化)技巧!例如,请参阅Wikipedia关于RAII的文章
RAII只是一般的想法。它被应用于C++标准库的std::unique_ptrstd::shared_ptr模板类中。

RAII习惯用法的简要解释:

基本上,它是C++版本的try..finally块,在其他一些语言中也可以找到。RAII习惯用法可能更灵活。

它的工作原理如下:

  • 你在资源(例如内存)周围编写一个包装器类。析构函数负责释放资源。

  • 你创建一个局部(自动)变量,作为你的包装器类的实例,处于一个范围内。一旦程序执行离开该范围,对象的析构函数将被调用,从而释放资源(例如内存)。

重要的一点是,无论如何退出作用域都没有关系。即使抛出异常,作用域仍然会退出,并且包装器对象的析构函数仍然会被调用。


非常简单的例子:

// BEWARE: this is NOT a good implementation at all, but is supposed to
// give you a general idea of how RAII is supposed to work:
template <typename T>
class wrapper_around
{
  public:
    wrapper_around(T value)
        : _value(value)
    { }
    T operator *()
    {
        return _value;
    }
    virtual ~wrapper_around()
    {
        delete _value;  // <-- NOTE: this is incorrect in this particular case;
                        // if T is an array type, delete[] ought to be used
    }
  private:
    T _value;
};
// ...

{
    wrapper_around<char*> heap( new char[50] );
    // ... do something ...

    // no matter how the { } scope in which heap is defined is exited,
    // if heap has a destructor, it will get called when the scope is left.
    // Therefore, delegate the responsibility of managing your allocated
    // memory to the `wrapper_around` template class.
    // there are already existing implementations that are much better
    // than the above, e.g. `std::unique_ptr` and `std::shared_ptr`!
}

非常感谢您的建议。我对C++还比较陌生,所以暂时我更愿意使用自己的实现方式,而不是盲目地使用已经存在的解决方案,因为我并不知道它们实际上是做什么的。我会先尝试自己实现,然后再了解RAII是什么以及如何正确使用它。再次感谢您。 - Kra
修复你的示例,使其调用delete []。 - Martin York
@Martin York:我添加了一条注释;是的,在这种特定情况下,它应该调用delete[],但在一般情况下不应该这样做。(例如,T也可以只是int。)正如我所写的,我的意图并不是提供一个完美的实现,而仅仅是基本地展示RAII应该如何工作。 - stakx - no longer contributing
2
@Kra:我建议恰恰相反。越早熟悉现有的库,对你越有好处。另外需要注意的是,如果你没有经验,在实现过程中会很容易出错,并且很难调试问题所在。如果你使用经过测试的库,就可以把精力集中到你特定代码的问题上。等到你更有经验之后,再尝试自己实现或者理解库中各种决策的原理。 - David Rodríguez - dribeas
@Martin York,@stakx:这就是为什么Boost有不同的智能指针和智能数组类的原因。 - isekaijin
显示剩余2条评论

9

好的,Java程序员先生:

try
{
    // Exception safe dynamic allocation of a block of memory.
    std::vector<char>  heap(50);

    // DO STUFF

    // Note in C++ we use stack based objects and their constructor/destructor
    // TO give a deterministic cleanup, even in the presence of exceptions.
    //
    // Look up RAII (bad name for a fantastic concept).
}
catch (...)
{
    cout << "Error, leaving function now";
    return 1;  // Though why you want to return when you have not fixed the exception is
               // slightly strange. Did you want to rethrow?
}

不,我不想重新抛出异常。我的函数在一切正常时返回0(还包括try块中处理过的异常),而当发生严重错误(如未处理的异常)时,函数返回1。由调用者在之后处理情况。感谢RAII的建议。 - Kra
2
@Kra:你为什么想要将异常转换为返回值来处理错误呢? - David Rodríguez - dribeas

9
一般而言,使用RAII技术可以解决这个问题。
然而,也可以通过将变量移出try{}语句块的作用域来解决该问题。
char * heap = NULL;
try {
  heap = new char [50];
  ... stuff ...
} catch (...) {
  if (heap) {
    delete[] heap;
    heap = NULL;
  }
  ... however you want to handle the exception: rethrow, return, etc ...
}

请注意,我并不建议这种做法作为良好的实践方式 - 而是一种下策,只有在您真正了解风险并愿意承担它们时才能使用。个人而言,我会使用RAII。
祝好!

2
RAII:资源获取即初始化。RIAA是一个不同的更邪恶的实体。 :) - Zan Lynx
3
我觉得你可能是指RAII。我认为让音乐产业介入不会提高异常安全性 :) - Peter
Lolls - 谢谢大家 - 当我想到邪恶时,RIAA显然会浮现在脑海中!;) - Mordachai
3
if (heap) delete[] heap; 是一种反模式。在空指针上使用 delete[] 是完全安全的,不会有任何影响。 - Ben Voigt
2
你忘记在try块中调用delete[] heap了。这就是为什么RAII模式比使用try/catch手动释放更安全的原因之一。手动释放很容易出错。 - Adrian
是的,原始指针非常有问题,不应该使用 - 最好使用RAII - 最好使用标准库,它提供了许多这样的模式。 - Mordachai

6

要么将 new 放在 try 前面,这样指针仍然在作用域范围内;要么使用智能指针,如 shared_ptrunique_ptr(在紧急情况下使用 auto_ptr,但它存在问题),这些智能指针可以在退出时为您清理内存。异常是智能指针非常重要的原因之一。


2
在这种情况下,auto_ptr<>是完美的选择(如果智能指针是解决方案的话)。但其他方法如动态容器呢? - Martin York
1
@Martin York:你不能使用auto_ptr来处理数组,是吗? - Fred Larson
1
啊,是的,那是正确的。你不应该使用auto_ptr,因为它调用的是delete而不是delete[]。虽然有些编译器可能会让你得逞...但这不是推荐的做法。 - A. Levy
2
@John Dibling:在这种情况下,auto_ptr会导致未定义的行为,因为它使用delete而不是delete[],正如A.Levy所说(我之前暗示过)。 - Fred Larson
1
@Fred Larson:是的,像其他智能指针一样,std::auto_ptr在动态数组上不起作用(除非有帮助)。我的评论是基于在这个答案的上下文中使用它,而不是整个问题。如果您需要类似数组的结构,应该使用某种容器(如std::vector或tr1中提供的新数组)。 - Martin York
显示剩余4条评论

2
“正确”的答案是RAII和shared_ptr,就像上面提到的一样。但为了完整起见,在您的示例中,您可以使用以下进行替换:”
char *heap = new char [50];

使用

char *stack = static_cast<char*>( alloca(50) );

alloca 几乎与 malloc 相同,除了它在栈上分配内存而不是堆上,因此无论函数如何退出(抛出或不抛出异常),内存都会被回收,不需要进行删除或释放。


5
如果你要走这条路,请使用char heap[50];。只有在编译时不知道大小并且对可移植性没有太多关注的情况下,才需要使用alloca - Mike Seymour

1

我必须同意所有那些说RAII的人,不过,我会使用Boost的shared_array而不是auto_ptr。Auto指针调用delete而不是'delete []',这将导致数组泄漏。


0
最简单的方法是在try块之前声明变量,然后只需在块内进行初始化。

0

是的 - 如果你考虑简单性 - 外部指针是你 try 块的解决方案。

敬礼


-4

同意 RAII 和智能指针的答案。

但是,如果你坚持的话,可以这样做:

try { dangerous operations } 
catch { cleanup; throw; }

你如何清理不在作用域内的东西?为什么要重新抛出异常? - John Dibling
(而且你忘记清理,以防所有危险操作都成功。) - stakx - no longer contributing
当然,它应该在范围内。重新抛出异常是为了让我们能够捕获任何异常,而不会丢失错误信息。以上通用模式相当于SEH和其他编程语言中的“finally”。 - Pavel Radzivilovsky
3
原文:So the OP asked "how does one free memory which was allocated in the try block when the exception occurs?" and you responded basically by saying "by freeing memory which was allocated in the try block when the exception occurs." How useless.翻译:所以OP问道:“当异常发生时,如何释放在try块中分配的内存?”你的回答基本上是“通过在异常发生时释放在try块中分配的内存”。多么无用。 - John Dibling

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