当某段代码抛出一个异常时,会在堆栈中寻找catch处理程序,当发现一个catch的时候堆栈会释放所有中间堆栈帧,并直接回到定义catch处理程序的堆栈层。堆栈释放(stack unwinding)意味着所有具有局部作用域的名称的析构函数都会被调用,然而当堆栈释放的时候,并不释放指针变量,也不会执行其它清理。
示例1:
#include <iostream>
#include <fstream>
#include <stdexcept>
#include <string>
namespace test_exception {
auto func2() -> void;
auto func1() -> void {
std::string str1;
std::string *str2 = new std::string();
func2();
delete str2;
}
auto func2() -> void {
std::ifstream in_file("test.txt");
throw std::exception();
in_file.close();
}
auto main() -> int {
std::cout << "testing exception..." << std::endl;
try {
func1();
}
catch(const std::exception& e) {
std::cerr << "Line " << __LINE__ << ", " << e.what() << std::endl;
return 1;
}
std::cout << "------------------------------" << std::endl;
return 0;
}
}
示例1输出:
__cplusplus: 201703
testing exception...
Line 35, std::exception
The end.
当func2()
抛出一个异常时,最近的异常处理程序在main()中,控制立刻从func2()
的这一行:throw std::exception();
跳转到main()
的这一行:std::cerr << "Line " << __LINE__ << ", " << e.what() << std::endl;
。
在func2()
中控制依然在抛出异常的那一行,后面的行永远不会有机会运行:in_file.close();
。然而幸运的是,因为in_file
是堆栈中的局部变量,因此会调用ifstream
析构函数,ifstream
析构函数会自动关闭文件,因此在此不会泄漏资源。如果动态分配了in_file
那么这个指针不会被销毁,文件也不会被关闭。
在func1()
中,控制在func2()
的调用中,因此后面的行永远不会有机会执行:delete str2;
,在此情况下确实会发生内存泄漏,堆栈释放不会自动删除str2
,然而str1
会被正确地销毁,因为str1
是堆栈中的局部变量,堆栈会正确地销毁所有局部变量。
备注: 可结合return
语句理解。
示例2:
#include <iostream>
#include <fstream>
#include <stdexcept>
#include <string>
namespace test_exception {
auto func() -> void {
throw std::runtime_error("exception in func()");
}
auto testThrow1() -> auto {
std::ifstream in_file;
try {
std::string filename("testThrow.txt");
in_file.open(filename);
if(in_file.fail()) { // 本示例假设文件打开成功,即不会走该if分支的异常
throw std::runtime_error(filename.c_str());
}
func();
}
catch(const std::runtime_error& e) {
std::cout << "Line " << __LINE__ << ", " << e.what() << std::endl;
}
catch(...) {
std::cout << "Line " << __LINE__ << ", some exception occurs!" << std::endl;
}
// 异常发生时执行上面catch语句块,此句后面不会被执行,但因为in_file是局部变量,
// 因此会调用ifstream析构函数,ifstream析构函数会自动关闭文件,因此不会泄漏资源
// 如果in_file是动态分配的那么这个指针不会被销毁,文件也不会被关闭
in_file.close();
}
auto testThrow2() -> auto {
int *p;
try {
p = new int;
func();
}
catch(...) {
std::cout << "Line " << __LINE__ << ", some exception occurs!" << std::endl;
}
// 异常发生时执行上面catch语句块,此句后面不会被执行,因此会产生内存泄漏
delete p;
}
auto main() -> int {
std::cout << "testing exception..." << std::endl;
testThrow1();
testThrow2();
std::cout << "------------------------------" << std::endl;
return 0;
}
}
示例2输出:
__cplusplus: 201703
testing exception...
Line 24, testThrow.txt
Line 44, some exception occurs!
------------------------------
The end.
以上示例表明粗心的异常处理会导致内存以及资源的泄漏。
在C++中可以通过使用智能指针
或者捕获、清理并重新抛出
两种技术来处理这种情况。
智能指针对象在堆栈中分配,无论什么时候销毁智能指针对象,都会释放底层的资源。
示例1中的func1()
函数可改写为:
#include <memory>
auto func1() -> void {
std::string str1;
std::unique_ptr<std::string> str2(new std::string("Hello"));
func2();
}
当从func1()
返回或者抛出异常时,将自动删除std::string*
类型的str2
指针。
使用智能指针时,永远不必考虑释放底层的资源:智能指针的析构函数会自动完成这一操作,无论是正常退出函数还是抛出异常退出函数都是如此。
避免内存以及资源泄漏的另一种技术是使每个函数捕获可能抛出的所有异常,执行必要的清理并且重新抛出异常供堆栈中更高层的函数处理。
示例1中的func1()
可改为:
auto func1() -> void {
std::string str1;
std::string *str2 = new std::string();
try {
func2();
}
catch(...) {
delete str2;
throw; // rethrow the exception
}
delete str2;
}
该函数用异常处理程序封装了func2()
的调用,处理程序执行清理(删除了str2
)并重新抛出异常。(本方案可以运行良好,但是繁琐,需要两行完全相同的代码删除str2
,一行用来处理异常,另一行在函数正常退出的情况下执行)。
关键字throw
本身会重新抛出最近捕获的任何异常。
使用智能指针
是比捕获、清理和重新抛出
技术更好的解决方案。
1.Marc Gregoire, Nicholas A. Solter, Scott J. Kleper. C++高级编程(第2版). 清华大学出版社,2012.(P300)