来自C++入门选手的理解,希望有大佬看到可以指导补充
首先我们需要先讨论一下assert宏,它仅用来捕捉不应该发生的逻辑错误,而且他也不能对错误进行处理,而是直接终止程序执行。
随后我们来介绍C++的异常处理机制。它允许程序中独立开发的部分能够在运行时就出现的问题进行处理。
整个行文的逻辑我想先说明 try … catch…块,也就是我们如何抛出异常和捕获异常,然后我们再细致讨论这两个概念。
文章目录
assert宏
在 C++ 中,assert 是用于在调试时检查程序中某些条件是否为真的宏。当条件为假(即断言失败)时,程序会输出错误信息并终止执行。assert 通常用于捕捉不应该发生的逻辑错误,在发布版本中可以禁用这些检查以提高性能。
使用方法
- 包含头文件:
- 编写断言
- 实际示例
禁用断言
在发布版本中,可以通过定义 NDEBUG 来禁用 assert 检查。可以在编译时使用 -DNDEBUG 选项:
g++ -DNDEBUG -o myprogram myprogram.cpp
在 CMake文件中禁用断言,通常是在 CMakeLists.txt 文件中使用 add_definitions
或 target_compile_definitions
指令来设置编译选项。
cmake_minimum_required(VERSION 3.10)
project(MyProject)
set(CMAKE_CXX_STANDARD 14)
# 添加源文件
add_executable(MyProject main.cpp)
# 禁用所有目标的断言
add_definitions(-DNDEBUG)
# 为特定目标禁用断言
# target_compile_definitions(MyProject PRIVATE NDEBUG)
# 链接库
target_link_libraries(MyProject PRIVATE moduo_base)
异常处理机制
前面我们提到assert
仅用于在调试期间的错误检测和诊断,如果断言为假,程序会直接终止执行。
而我们的 try...catch
块用于处理程序运行期间可能发生的异常,能够应对运行时的错误。它允许程序捕获并处理异常,从而防止程序崩溃并提供有意义的错误处理和恢复机制。
这里是一个简单的例子:
#include <iostream>
#include <stdexcept>
double divide(double numerator, double denominator) {
if (denominator == 0) {
throw std::invalid_argument("Denominator cannot be zero");
}
return numerator / denominator;
}
int main () {
double num = 10.0;
double den = 0.0;
try {
double result = divide(num, den);
std::cout << "Result: " << result << std::endl;
} catch (const std::invalid_argument &e) }
std::cerr << "Error: " << e.what() << std::endl;
}
return 0;
}
在这个例子中,如果分母为零,函数 divide 将抛出一个 std::invalid_argument 异常。try…catch 块捕获这个异常并输出错误信息,而不是让程序崩溃。
也就是说即使我们的被除数为设置为了0,程序也只会捕获这个异常信息,并且为我们提供了处理异常的机会,它并不会让程序崩溃。
那么,我们现在来了解一下它的整个运行机制吧!
throw、try、catch
作用
-
throw
- 用于抛出异常。可以在函数中通过 throw 关键字抛出一个异常,异常可以是基本类型或任何类型的对象。
- 例如:throw 42; 或 throw std::runtime_error(“An error occurred”);
-
try
- 用于包围可能抛出异常的代码块。当异常被抛出时,控制权转移到与之关联的 catch 块。
- 例如:
try { // 可能抛出异常的代码 }
-
catch
- 用于捕获异常。catch 块会捕获由 try 块抛出的异常,并对其进行处理。
- 例如:
catch (const std::exception& e) { // 处理异常 }
- 值得关注的是,我们的 catch 也能够使用 throw ,这叫重新抛出,也就是说有时候一个单独的 catch 语句不能完整地处理某个异常,在执行了某些矫正操作之后,当前的 catch 可能会决定由调用链更上一层的函数接着处理异常。一条 catch 语句通过重新抛出的操作将一场传递给另外一个 catch 语句。这里的重新抛出仍然是一条 throw 语句,只不过不包含任何表达式。
关系
当一个异常被抛出时,程序会跳转到最近的匹配的 catch 块进行处理。如果找不到匹配的 catch 块,程序会调用 std::terminate 并终止。
也就是说,如果一个异常没有被捕获,则它将终止当前的程序
那么这个异常是如何被捕获的呢 ?这里就不得不提到一个名词“栈展开”
当抛出一个异常后,程序暂停当前函数的执行过程并立即开始寻找与异常匹配的 catch 子句。当 throw 出现在一个 try 语句块內时,检查与该 try 块关联的 catch 子句。如果找到了匹配的 catch,就使用该 catch 处理异常。如果这一步没找到匹配的 catch 且该 try 语句嵌套在其他 try 块中,则继续检查与外层 try 匹配的 catch 子句。如果还是找不到匹配的 catch ,则退出当前这个主调函数,继续在调用了刚刚推出的这个函数的外层函数中寻找,以此类推。
这个过程就叫做栈展开,栈展开过程沿着嵌套函数的调用链不断寻找,直到找到与之匹配的 catch 子句为止,如果找不到匹配的 catch 子句,程序就会调用标准库函数 terminate,他负责终止程序的执行过程。
好了,我们已经能够基本使用C++的异常处理机制了,但是我们还需要弄明白以下内容:
⭐️栈展开过程中对象会被自动销毁
在栈展开的过程中,位于调用链上的语句块可能会提前退出。通常情况下,程序在这些块中可能已经创建了一些局部对象。我们已经知道,块退出后它的局部对象也将随之销毁,这条规则对于栈展开过程同样适用。
如果在栈展开过程中推出某个块,编译器将负责确保这个块中创建的对象能被正确地销毁。如果某个局部对象的类型是类类型,则该对象的析构函数将被自动调用。与往常一样,编译器在销毁内置类型的对象时不需要做任何事情。
⭐️异常可能会发生在构造函数中,我们应该怎么处理
如果异常发生在构造函数总,则当前的对象可能只构造了一部分。
有的成员已经初始化了,而另外一些成员在异常发生前也许还没有初始化。即使某个对象只构造了一部分,我们也要确保已经构造的成员能够被正确销毁。
如果成员是栈上的变量,那么这里的“我们”指的就是编译器,根本不需要操心。
如果成员是堆上的动态资源,那么这里的“我们”指的就是程序员了,程序员应该确保已构造的成员能够被正常销毁,这里是一个例子:展示如何在堆上分配成员变量以及在构造函数抛出异常时正确释放它们。
#include <iostream>
#include <stdexcept>
class Member {
public:
Member(const std::string& name) : name(name) {
std::cout << "Constructing Member: " << name << std::endl;
if (name == "bad") {
throw std::runtime_error("Failed to construct Member: " + name);
}
}
~Member() {
std::cout << "Destructing Member: " << name << std::endl;
}
private:
std::string name;
};
class MyClass {
public:
MyClass(const std::string& name1, const std::string& name2, const std::string& name3)
: member1(nullptr), member2(nullptr), member3(nullptr) {
try {
member1 = new Member(name1);
member2 = new Member(name2);
member3 = new Member(name3);
std::cout << "Constructing MyClass" << std::endl;
} catch (...) {
// 释放已成功分配的资源
delete member1;
delete member2;
delete member3;
std::cout << "Exception caught in MyClass constructor" << std::endl;
throw; // 重新抛出异常
}
}
~MyClass() {
delete member1;
delete member2;
delete member3;
std::cout << "Destructing MyClass" << std::endl;
}
private:
Member* member1;
Member* member2;
Member* member3;
};
int main() {
try {
MyClass myObject("good1", "bad", "good3");
} catch (const std::exception& e) {
std::cout << "Exception: " << e.what() << std::endl;
}
return 0;
}
输出分析
运行上述代码,输出结果如下:
Constructing Member: good1
Constructing Member: bad
Exception caught in MyClass constructor
Destructing Member: good1
Exception: Failed to construct Member: bad
-
构造对象:
- Member member1 被成功构造,输出 “Constructing Member: good1”。
- Member member2 的构造函数抛出异常,输出 “Constructing Member: bad” 并抛出异常 std::runtime_error(“Failed to construct Member: bad”)。
-
捕获异常:
- MyClass 的构造函数捕获到异常,进入 catch 块。
- 在 catch 块中,删除已分配的成员变量 member1,防止内存泄漏。
-
栈展开:
在栈展开过程中,编译器不会自动处理堆上分配的成员变量的清理,因此需要程序员在 catch 块中手动释放这些资源。
-
异常处理:
- 异常被传播到 main 函数的 catch 块,输出 “Exception: Failed to construct Member: bad”。
更好的办法是使用智能指针
为了简化内存管理并防止内存泄漏,可以使用智能指针(如 std::unique_ptr)。智能指针可以自动管理资源的生命周期,即使在异常情况下也能确保资源被正确释放。
#include <iostream>
#include <stdexcept>
#include <memory>
class Member {
public:
Member(const std::string& name) : name(name) {
std::cout << "Constructing Member: " << name << std::endl;
if (name == "bad") {
throw std::runtime_error("Failed to construct Member: " + name);
}
}
~Member() {
std::cout << "Destructing Member: " << name << std::endl;
}
private:
std::string name;
};
class MyClass {
public:
MyClass(const std::string& name1, const std::string& name2, const std::string& name3) {
member1 = std::make_unique<Member>(name1);
member2 = std::make_unique<Member>(name2);
member3 = std::make_unique<Member>(name3);
std::cout << "Constructing MyClass" << std::endl;
}
~MyClass() {
std::cout << "Destructing MyClass" << std::endl;
}
private:
std::unique_ptr<Member> member1;
std::unique_ptr<Member> member2;
std::unique_ptr<Member> member3;
};
int main() {
try {
MyClass myObject("good1", "bad", "good3");
} catch (const std::exception& e) {
std::cout << "Exception: " << e.what() << std::endl;
}
return 0;
}
输出分析:
Constructing Member: good1
Constructing Member: bad
Exception: Failed to construct Member: bad
Destructing Member: good1
解释:
-
构造对象:
- Member member1 被成功构造,输出 “Constructing Member: good1”。
- Member member2 的构造函数抛出异常,输出 “Constructing Member: bad” 并抛出异常 std::runtime_error(“Failed to construct Member: bad”)。
-
智能指针管理资源:
- 当异常抛出时,智能指针 member1 和 member2 会自动销毁已经构造的对象,防止内存泄漏。
-
异常处理:
- 异常被传播到 main 函数的 catch 块,输出 “Exception: Failed to construct Member: bad”。
⭐️析构函数与异常
C++在语法层面上保证了,在栈展开的过程中,运行类类型的局部对象的析构函数时,由于这些析构函数是自动执行的,所以他们不应该抛出异常。一旦在栈展开的过程中析构函数抛出了异常,并且析构函数自身没能捕获到该异常,则程序直接终止。