堆栈上分配的异常如何超出其范围?
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
操作数。
【讨论】:
以上是关于堆栈上分配的异常如何超出其范围?的主要内容,如果未能解决你的问题,请参考以下文章
分配对局部变量的引用,如果局部变量超出范围,它会超出范围吗?