堆栈上分配的异常如何超出其范围?

Posted

技术标签:

【中文标题】堆栈上分配的异常如何超出其范围?【英文标题】:How are exceptions allocated on the stack caught beyond their scope? 【发布时间】:2011-01-25 03:31:17 【问题描述】:

在下面的代码中,基于堆栈的变量“ex”被抛出并被捕获到超出 ex 声明范围的函数中。这对我来说似乎有点奇怪,因为(AFAIK)基于堆栈的变量不能在声明它们的范围之外使用(堆栈已展开)。

void f() 
    SomeKindOfException ex(...);
    throw ex;


void g() 
    try 
        f();
     catch (SomeKindOfException& ex) 
        //Handling code...
    

我在 SomeKindOfException 的析构函数中添加了一个 print 语句,它表明 ex 一旦超出 f() 的范围就会被破坏,但是它会在 g() 中被捕获并在它超出范围时再次被破坏.

有什么帮助吗?

【问题讨论】:

这里使用引用是否正确? catch (SomeKindOfException &ex) 我认为这很危险,因为它不调用复制构造函数,并且您可以访问属于 f() 的释放堆栈的内存区域!我想这应该是正确的:catch (SomeKindOfException ex) 通过引用捕获是正确的(甚至更好 - 参见parashift.com/c++-faq-lite/exceptions.html 17.7 节)。正如我对问题的回答所说,被捕获的异常不是抛出的基于堆栈的对象,而是它的副本,它驻留在不同的地方,可以在堆栈展开后幸存下来,因此不存在这样的风险。 嗯,我昨天晚上做了一些实验,是的,最好使用参考。看这个:pastebin.com/8YQuNAux 如果你执行它,你会注意到异常在没有引用的情况下在每次捕获时被动态分配(在new 的意义上):如果你使用引用,它只会被分配一次并被破坏范围终止时自动执行。另外我认为这种行为完全取决于编译器。 这里详细解释了异常如何工作的一个很好的概述:monoinfinito.wordpress.com/series/exception-handling-in-c 【参考方案1】:

异常对象被复制到一个特殊的位置,以在堆栈展开后继续存在。您看到两个破坏的原因是,当您退出 f() 时,原始异常被破坏,而当您退出 g() 时,副本被破坏。

【讨论】:

哦,这是我直觉上的预期,我很高兴它符合事实。还应该调用复制构造函数,那么您如何正确处理呢?假设您的异常类有一个包含错误消息的实例变量:在随后的每个throw 中,您都有该变量的新副本分配!说到最佳实践,您如何看待确保仅针对所有异常的浅拷贝?【参考方案2】:

对象被复制到一个异常对象中,该对象在堆栈展开后仍然存在。该对象的内存来自何处未指定。对于大对象,它可能是malloc'ed,对于较小的对象,实现可能有一个预分配的缓冲区(我可以想象这可以用于bad_alloc 异常)。

引用ex 然后绑定到那个异常对象,这是一个临时的(它没有名字)。

【讨论】:

感谢您澄清异常的内存位置,我已经编辑了上面的评论以删除该问题。【参考方案3】:

C++ 标准 15.1/4:

被抛出异常的临时副本的内存以未指定的方式分配, 3.7.3.1 中所述的除外。只要有一个正在执行的处理程序,临时的就会持续存在 例外。特别是,如果处理程序通过执行 throw 退出;语句,将控制权交给另一个 相同异常的处理程序,因此临时保留。当最后一个处理程序被执行时 异常以 throw 以外的方式退出;临时对象被销毁并执行 可以为临时对象释放内存;任何此类解除分配都是以未指定的方式完成的。 在异常声明中声明的对象销毁后立即发生销毁 在处理程序中。

没什么好说的了。

【讨论】:

【参考方案4】:

当你抛出 ex 时,它会被复制到用于抛出异常对象的特殊内存位置。这种复制是由普通的复制构造函数执行的。

您可以从这个示例中轻松看出这一点:

#include <iostream>

void ThrowIt();

class TestException

  public:
    TestException()
    
        std::cerr<<this<<" - inside default constructor"<<std::endl;
    

    TestException(const TestException & Right)
    
        (void)Right;
        std::cerr<<this<<" - inside copy constructor"<<std::endl;
    

    ~TestException()
    
        std::cerr<<this<<" - inside destructor"<<std::endl;    
    
;

int main()

    try
    
        ThrowIt();
    
    catch(TestException & ex)
    
        std::cout<<"Caught exception ("<<&ex<<")"<<std::endl;
    
    return 0;


void ThrowIt()

    TestException ex;
    throw ex;

样本输出:

matteo@teolapubuntu:~/cpp/test$ g++ -O3 -Wall -Wextra -ansi -pedantic ExceptionStack.cpp -o ExceptionStack.x
matteo@teolapubuntu:~/cpp/test$ ./ExceptionStack.x 
0xbf8e202f - inside default constructor
0x9ec0068 - inside copy constructor
0xbf8e202f - inside destructor
Caught exception (0x9ec0068)
0x9ec0068 - inside destructor

顺便说一句,你可以在这里看到,用于抛出对象的内存位置(0x09ec0068)肯定远离原始对象之一(0xbf8e202f):堆栈像往常一样具有高地址,而用于抛出对象的内存在虚拟地址空间中相当低。尽管如此,这是一个实现细节,因为正如其他答案所指出的那样,标准没有说明抛出对象的内存应该在哪里以及应该如何分配。

【讨论】:

使用VC++,被抛出的对象的内存位置非常接近原始对象的内存位置(即在堆栈上)。这确实证明了 Johannes 和 Kirill 的观点,即未指定的内存位置。 你说得对,这确实是一个实现细节。我会澄清答案。【参考方案5】:

除了标准在 15.1/4 中所说的(“异常处理/抛出异常”) - 用于抛出异常的临时副本的内存以未指定的方式分配 - 其他一些琐事关于异常对象的分配方式有:

标准的 3.7.3.1/4(“分配函数”)表明异常对象不能通过 new 表达式或对“全局分配函数”的调用(即, operator new() 替换)。请注意,malloc() 不是标准定义的“全局分配函数”,因此malloc() 绝对是分配异常对象的一个​​选项。

“当抛出异常时,会创建异常对象并将其放置在某种异常数据堆栈上”(Stanley Lippman,“C++ 对象模型内部”- 7.2 异常处理)

来自 Stroustrup 的“C++ 编程语言,第 3 版”:“C++ 实现需要有足够的空闲内存才能在内存耗尽的情况下抛出 bad_alloc。但是,抛出其他一些异常会导致内存耗尽。”(14.4.5 资源耗尽);并且,“该实现可以应用多种策略来存储和传输异常。但是,可以保证有足够的内存允许new 抛出标准的内存不足异常bad_alloc”( 14.3 捕获异常)。

请注意,来自 Stroustrup 的报价是预先标准的。我觉得有趣的是,该标准似乎没有保证 Stroustrup 认为重要到足以提及两次。

【讨论】:

【参考方案6】:

因为规范明确指出,会创建一个临时对象来代替 throw 操作数。

【讨论】:

以上是关于堆栈上分配的异常如何超出其范围?的主要内容,如果未能解决你的问题,请参考以下文章

缓冲区溢出及堆栈/堆操纵

分配对局部变量的引用,如果局部变量超出范围,它会超出范围吗?

如何在此自定义堆栈实现中正确分配更多内存?

通过map文件了解堆栈分配(STM32MDK5)--避免堆栈溢出

计算数组时出现“列表分配索引超出范围”错误

Python构建二维数组获取列表分配索引超出范围