寻求明确的解释:throw()和堆栈展开

时间:2011-03-10 23:32:02

标签: c++

我不是一个程序员,但是他学到了很多东西。我正在编写包装类,以通过我正在使用的真正技术API来简化操作。它的例程返回错误代码,我有一个函数将它们转换为字符串:

static const char* LibErrString(int errno);

为了统一,我决定让我的类成员在遇到错误时抛出异常。我创建了一个类:

struct MyExcept : public std::exception {
   const char* errstr_;
   const char* what() const throw() {return errstr_;}
   MyExcept(const char* errstr) : errstr_(errstr) {}
};

然后,在我的一个课程中:

class Foo {
public:
   void bar() {
   int err = SomeAPIRoutine(...);
   if (err != SUCCESS) throw MyExcept(LibErrString(err));
   // otherwise...
   }
};

整个过程完美无缺:如果SomeAPIRoutine返回错误,则Foo::bar调用周围的try-catch块会在what()中使用正确的错误字符串捕获标准异常。

然后我希望该成员提供更多信息:

void Foo::bar() {
   char adieu[128];
   int err = SomeAPIRoutine(...);
   if (err != SUCCESS) {
      std::strcpy(adieu,"In Foo::bar... ");
      std::strcat(adieu,LibErrString(err));
      throw MyExcept((const char*)adieu);
   }
   // otherwise...
}

但是,当SomeAPIRoutine返回错误时,异常返回的what()字符串仅包含垃圾。在我看来,问题可能是由于adieu在调用throw后超出范围。我通过将adieu移出成员定义并使其成为类Foo的属性来更改代码。在此之后,整个过程完美无缺:围绕对Foo::bar的调用捕获异常的try-call块在what()中具有正确的(扩展的)字符串。

最后,我的问题是:当堆栈“展开”时,if-block中抛出异常时,究竟是从堆栈中弹出的(按顺序)是什么?正如我上面提到的,我是一名数学家,而不是程序员。当这个C ++转换成运行的机器代码时,我可以使用一个非常清晰的解释堆栈(按顺序)的内容。

3 个答案:

答案 0 :(得分:4)

你是对的:异常构造函数将指针带到字符串,它不存储字符串内容的副本。该内容存储在本地变量

   char adieu[128];

在退出Foo :: bar方法时超出范围。

堆栈包含当前正在执行的函数的“激活记录”(也称为“堆栈帧”)。每个函数调用为该函数中声明的所有局部变量分配该堆栈上的内存(在机器级别,这可以实现为'push'或任何其他使堆栈指针前进的命令)。每次从函数返回 - 无论是通过抛出异常是正常的返回还是退出都没关系 - 释放进入函数时分配的内存(在机器级别,这被实现为'pop'或'restore stack pointer to输入函数时的值。)。

因此,当堆栈被展开时,抛出异常的函数和捕获异常的函数之间的“函数调用链”或“堆栈”中的所有“激活记录”都被释放。

答案 1 :(得分:1)

你有一个更简单的解决方案:

struct MyExcept : public std::exception {
   std::string errstr;
   const char* what() const throw() {return errstr_.c_str();}
   MyExcept(int errno, std::string prefix = "") : errstr (prefix + LibErrString(errno)) {}
};
...
throw MyExcept(err, "In Foo::bar... ");

使用C字符串,您必须更加关注范围和手动记忆管理。你正确地注意到你退出了几个范围(弹出变量和函数离开堆栈),所以C字符串更糟糕。另一方面,C ++字符串的行为更像整数。内存管理是一项集成功能。

出于同样的原因,我在您的异常类中移动了LibErrString调用。错误处理代码自然适合异常类,不应混淆业务代码。


对于实用的代码来说,回到理论上。抛出并捕获异常会发生什么? C ++首先确定 将捕获异常。必须有一个封闭的try{ }范围,但可能还有更多的范围:函数范围,范围,if范围或只是普通块范围。

然后从内到外退出这些范围。退出每个范围时,将销毁该块的本地变量。当退出函数作用域时,下一个要考虑的作用域当然是调用者;对于其他范围,下一个范围是周围范围。

您会看到变量被销毁,并且函数以LIFO顺序退出。这意味着堆栈结构很自然。您还可以使用两个堆栈,将返回地址和变量分开。或者三,避免堆栈上的大缓冲区。由于有许多合理的实现,C ++实际上并没有描述精确的实现,只是行为。

答案 2 :(得分:0)

首先,您的异常类应如下所示

class MyExcept : public std::runtime_error {
    MyExcept(const std::string & message) : std::runtime_error(message) {}
};

这就是所有需要的。如果更适合您的需要,您可以将基类更改为std::logic_error(有关区别的说明,请参阅here,有关其他预定义异常类的列表,请参阅here)。然后,您可以使用字符串连接构建错误消息:

void Foo::bar() {
    int err = SomeAPIRoutine(...);
    if (err != SUCCESS) {
       throw MyExcept("Error: " + LibErrString(err));
   }
   // otherwise...
}

This FAQ包含有关异常处理的更多有用信息,尤其是如何正确捕获异常:

try {
    // fail here
}
catch (std::exception & e) { // NOTE: catch by reference!!!
}

关于堆栈问题,当前堆栈帧已展开,其中的所有对象都按其构造的相反顺序销毁。执行此操作直到捕获到异常。这就是RAII有效的原因。