我不是一个程序员,但是他学到了很多东西。我正在编写包装类,以通过我正在使用的真正技术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 ++转换成运行的机器代码时,我可以使用一个非常清晰的解释堆栈(按顺序)的内容。
答案 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有效的原因。