重生之我在10天内速成C++ - DAY 8
🚀 重生之我在10天内卷赢C++ - DAY 8
导师寄语:嘿,我的“代码建筑师”!你已经能盖起宏伟的“软件大厦”了,有继承的框架,有多态的电梯,功能强大。但再宏伟的大厦,也怕“意外事故”——比如用户输入了错误的数据,或者内存申请失败了。如果处理不好,大厦随时可能“崩塌”(程序崩溃)。今天,我们要为我们的代码装上“安全网”和“消防系统”,学习如何优雅地处理运行时错误——异常处理 (Exception Handling)。然后,我们将拿到一把“万能钥匙”,彻底告别
delete
的烦恼,它就是现代C++的超级神器——智能指针 (Smart Pointers)!
🎯 今日目标
- 理解异常是什么,以及为什么需要它。
- 掌握
try...catch...throw
语法,学会捕获和处理异常。 - 学会使用C++标准库提供的标准异常。
- 学会自定义异常类型,让错误信息更明确。
- 【核心】 掌握RAII思想,并精通三种智能指针:
unique_ptr
,shared_ptr
, 和weak_ptr
。
1. 异常处理:优雅地面对错误
想象一下,你写了一个除法函数 divide(a, b)
。如果用户输入的 b
是 0
,会发生什么?程序会因为“除以零”这个致命错误而直接崩溃。
传统的错误处理方式是返回一个特殊值:
double divide(double a, double b) {
if (b == 0) {
return -1; // -1 是个错误码
}
return a / b;
}
// 调用方
double result = divide(10, 0);
if (result == -1) { // 每次调用都要检查错误码,很麻烦
// ...处理错误
}
这种方式很麻烦,而且如果 -1
本身是一个合法的计算结果呢?
异常处理机制提供了一种更强大、更清晰的方式:它将错误处理代码与正常业务逻辑代码分离开。
try...catch...throw
三部曲
这就像一个“安全生产”流程:
try
(尝试):把可能出问题的代码块放进try
的{}
里。这是我们的“重点监控区域”。throw
(抛出):当在try
块里发现错误时,用throw
关键字“扔”出一个异常对象。这个异常对象可以是任何类型(int
,string
, 类对象等)。throw
一旦执行,后面的代码就不再执行了,程序会立即跳转去寻找能接住这个异常的东西。catch
(捕获):紧跟在try
块后面的catch
块,像一个“安全网”。它会声明自己能接住什么类型的异常。如果throw
出来的异常类型和catch
声明的匹配,这个catch
块就会被执行。
例子:安全的除法
#include <iostream>
#include <string>
using namespace std;
double safe_divide(double a, double b) {
if (b == 0) {
// 抛出一个 string 类型的异常对象
throw string("错误:除数不能为零!");
}
return a / b;
}
int main() {
try {
cout << "--- 准备进行计算 ---" << endl;
double result1 = safe_divide(10, 2);
cout << "10 / 2 = " << result1 << endl;
// 这次会出问题
double result2 = safe_divide(10, 0);
// 上一行抛出异常后,这一行及try块中后续代码不会被执行
cout << "10 / 0 = " << result2 << endl;
cout << "--- 计算结束 ---" << endl;
}
// 捕获 string 类型的异常
catch (const string& error_message) {
cout << "!!! 捕获到异常 !!!" << endl;
cout << "错误详情: " << error_message << endl;
}
cout << "\n程序继续平稳运行,没有崩溃。" << endl;
return 0;
}
2. 使用标准异常和自定义异常
每次都 throw string
不够专业。C++标准库在 <stdexcept>
头文件中定义了一系列标准的异常类,它们都继承自 std::exception
。
标准异常类 | 用途 |
---|---|
std::invalid_argument |
无效的参数 |
std::out_of_range |
访问数组或vector越界 |
std::runtime_error |
其他运行时错误 |
优点:代码更规范,并且 catch
可以捕获基类 std::exception
来处理所有标准异常。
例子:使用标准异常
#include <stdexcept> // 需要这个头文件
double safe_divide_pro(double a, double b) {
if (b == 0) {
throw invalid_argument("除数不能为零");
}
return a / b;
}
try {
safe_divide_pro(10, 0);
}
catch (const invalid_argument& e) { // 更精确地捕获
cout << "捕获到无效参数错误: " << e.what() << endl; // .what()返回错误信息
}
catch (const std::exception& e) { // 兜底,捕获所有其他标准异常
cout << "捕获到其他标准异常: " << e.what() << endl;
}
自定义异常:你也可以创建自己的异常类,通常继承自 std::exception
,这样可以携带更丰富的错误信息。
3. RAII 与智能指针:告别手动 delete
异常机制有一个潜在的“坑”。看下面的代码:
void process_data() {
MyResource* ptr = new MyResource(); // 1. 申请资源
// ... 做一些可能会抛出异常的操作 ...
risky_operation(); // 如果这里抛了异常
delete ptr; // 2. 释放资源。这一行根本没机会执行!
}
如果 risky_operation()
抛出异常,程序会直接跳出 process_data
函数,delete ptr;
这行代码被完美地跳过了,导致内存泄漏!
为了解决这个问题,C++社区提出了一种绝妙的设计哲学——RAII (Resource Acquisition Is Initialization),中文意思是“资源获取即初始化”。
RAII核心思想:用一个栈对象的生命周期来管理堆上的资源(或其他需要释放的资源)。
- 在对象的构造函数中获取资源(比如
new
)。 - 在对象的析构函数中释放资源(比如
delete
)。
由于栈对象的析构函数在函数结束时(无论正常结束还是因异常跳出)保证会被调用,所以资源也保证会被释放!
智能指针 (Smart Pointers) 就是RAII思想最经典的实现。它们是行为像指针的类对象,但能自动管理内存。
std::unique_ptr
:专属的“房产证”
- 特点:独占所有权。同一时间,只能有一个
unique_ptr
指向一个给定的对象。它就像一份唯一的房产证,不能复制。 - 优点:轻量、高效,开销和裸指针几乎一样。
- 适用场景:当你明确知道一个对象只有一个“主人”时。
#include <memory> // 需要这个头文件
void unique_ptr_demo() {
// 创建一个unique_ptr,它现在拥有这个新的Car对象
unique_ptr<Car> u_ptr(new Car("独占的宝马"));
// 像普通指针一样使用
u_ptr->drive();
// unique_ptr<Car> u_ptr2 = u_ptr; // 错误!不能复制,房产证不能复印
// 可以转移所有权
unique_ptr<Car> u_ptr3 = std::move(u_ptr); // u_ptr现在变空了,u_ptr3成了新的主人
} // 函数结束时,u_ptr3(或u_ptr,如果没转移)会自动调用析构函数,delete掉Car对象
std::shared_ptr
:共享的“共享单车”
- 特点:共享所有权。多个
shared_ptr
可以指向同一个对象。它内部有一个引用计数器,记录有多少个“主人”。 - 工作原理:
- 每当一个新的
shared_ptr
指向该对象(通过拷贝构造或赋值),引用计数+1
。 - 每当一个
shared_ptr
被销毁(或指向别处),引用计数-1
。 - 当引用计数降为0时,最后一个
shared_ptr
会负责delete
掉它指向的对象。
- 每当一个新的
- 适用场景:当你需要多个地方共同管理同一个对象的生命周期时。
void shared_ptr_demo() {
shared_ptr<Car> sh_ptr1; // 空的
{
shared_ptr<Car> sh_ptr2(new Car("共享的奔驰"));
cout << "引用计数: " << sh_ptr2.use_count() << endl; // 输出1
sh_ptr1 = sh_ptr2; // 拷贝,sh_ptr1也指向了奔驰
cout << "引用计数: " << sh_ptr1.use_count() << endl; // 输出2
} // sh_ptr2在这里被销毁,引用计数-1,变为1
cout << "sh_ptr2销毁后,引用计数: " << sh_ptr1.use_count() << endl; // 输出1
} // sh_ptr1在这里被销毁,引用计数-1,变为0,奔驰对象被delete
std::weak_ptr
:“暗中观察”的观察员
- 问题:
shared_ptr
有一个致命问题——循环引用。如果对象A有一个shared_ptr
指向B,对象B也有一个shared_ptr
指向A,那么它们的引用计数永远不会降为0,导致两个对象都无法被释放。 weak_ptr
的作用:它是一个“弱引用”,可以指向一个由shared_ptr
管理的对象,但不会增加引用计数。它只是一个观察员。- 特点:
- 不能直接操作对象,必须先通过
lock()
方法“升级”成一个临时的shared_ptr
。 - 如果对象已经被释放,
lock()
会返回一个空指针。
- 不能直接操作对象,必须先通过
- 适用场景:打破
shared_ptr
的循环引用。
🧠 知识点大总结:编写“坚不可摧”的代码
今天我们学习了如何让我们的程序从“能跑就行”进化到“稳如老狗”。核心就是两大法宝:异常处理和智能指针。
表一:传统错误处理 vs. 异常处理
特性 | 传统方式 (返回错误码) | 异常处理 (try-catch ) |
---|---|---|
代码结构 | 业务逻辑与错误处理代码混杂在一起 | 业务逻辑与错误处理代码分离,try 管业务, catch 管错误 |
错误传递 | 需要层层返回错误码,非常繁琐 | 异常会自动“穿透”函数调用栈,直到被catch 捕获 |
错误信息 | 通常是一个简单的整数,信息量少 | 可以抛出任何类型的对象,携带丰富、具体的错误信息 |
构造函数 | 构造函数没有返回值,无法用此方式报告错误 | 构造函数可以抛出异常,是报告构造失败的唯一正确方式 |
适用场景 | 性能极其敏感、不希望有额外开销的底层代码 | 大多数现代C++应用,能显著提高代码的可读性和健壮性 |
表二:裸指针 vs. 智能指针
这张表告诉你,为什么现代C++(C++11及以后)强烈推荐使用智能指针。
特性 | 裸指针 (Car* ptr ) |
std::unique_ptr |
std::shared_ptr |
---|---|---|---|
所有权模型 | 不明确,只是一个地址 | 独占所有权 | 共享所有权 |
内存管理 | 手动 (new /delete ) |
自动 (RAII) | 自动 (RAII + 引用计数) |
主要风险 | 内存泄漏、悬挂指针 | 安全 (除非release() 后忘记delete ) |
循环引用 |
能否复制 | ✅ 可以随便复制 | ❌ 不能 (只能std::move ) |
✅ 可以 |
性能开销 | 极低 | 几乎为零,和裸指针一样 | 略高 (有引用计数的开销) |
“一句话”总结 | “自己动手,丰衣足食,也容易饿死” | “我的东西,不许碰!” | “大家的东西,一起用,最后一个走的人关灯” |
weak_ptr
的特别说明
weak_ptr
不在上面的对比表中,因为它不拥有资源,它是一个“观察员”。
- 它的使命:打破
shared_ptr
的循环引用。 - 它的特点:不增加引用计数,想用的时候必须先
lock()
一下,看看它观察的对象还在不在。
编程黄金法则:
优先使用
std::unique_ptr
,因为它的开销最小,所有权模型最清晰。
当你确实需要共享所有权时,才使用std::shared_ptr
。
当你发现可能会有shared_ptr
循环引用时,用std::weak_ptr
来打破循环。
几乎永远不要再直接使用new
和delete
来管理对象的生命周期(除非你在写自己的底层容器或智能指针)。
✍️ DAY 8 作业
-
带异常检查的vector访问器:
- 编写一个函数
get_element(const vector<int>& vec, int index)
。 - 这个函数接收一个
int
的vector
和一个索引index
。 - 如果
index
超出了vector
的有效范围(0
到vec.size() - 1
),函数应该抛出一个std::out_of_range
异常。 - 如果索引有效,函数返回该位置的元素。
- 在
main
函数中,用try...catch
块来调用这个函数,并分别测试有效索引和无效索引的情况。
- 编写一个函数
-
智能指针管理工厂:
- 有一个
Toy
类,在构造函数和析构函数中打印信息,让我们能看到它的生与死。 - 编写一个函数
make_toy()
,它在堆上创建一个Toy
对象,并返回一个管理这个对象的std::unique_ptr<Toy>
。 - 编写另一个函数
share_toy(std::shared_ptr<Toy> sp)
,它接收一个shared_ptr
,并打印出当前玩具的共享数量。 - 在
main
函数中:- 调用
make_toy()
获得一个玩具的unique_ptr
。 - 将这个
unique_ptr
的所有权转移给一个shared_ptr
。 - 创建几个其他的
shared_ptr
来共享这个玩具。 - 调用
share_toy()
函数,观察引用计数的变化。 - 让所有
shared_ptr
离开作用域,观察玩具是何时被销毁的。
- 调用
- 有一个
💡 作业答案与解析
1. vector访问器
#include <iostream>
#include <vector>
#include <stdexcept>
using namespace std;
int get_element(const vector<int>& vec, int index) {
if (index < 0 || index >= vec.size()) {
throw out_of_range("索引越界!有效范围是 0 到 " + to_string(vec.size() - 1));
}
return vec[index];
}
int main() {
vector<int> my_vec = {10, 20, 30};
// 测试有效索引
try {
cout << "尝试访问索引 1..." << endl;
int val = get_element(my_vec, 1);
cout << "my_vec[1] = " << val << endl;
} catch (const out_of_range& e) {
cout << "捕获到异常: " << e.what() << endl;
}
// 测试无效索引
try {
cout << "\n尝试访问索引 5..." << endl;
int val = get_element(my_vec, 5);
cout << "my_vec[5] = " << val << endl;
} catch (const out_of_range& e) {
cout << "捕获到异常: " << e.what() << endl;
}
return 0;
}
2. 智能指针管理工厂
#include <iostream>
#include <memory>
#include <string>
using namespace std;
class Toy {
private:
string name;
public:
Toy(string n) : name(n) { cout << "玩具 '" << name << "' 被制造出来了。" << endl; }
~Toy() { cout << "玩具 '" << name << "' 被销毁了。" << endl; }
void play() { cout << "正在玩 '" << name << "'..." << endl; }
};
// 工厂函数,返回一个 unique_ptr
unique_ptr<Toy> make_toy(string name) {
cout << "\n--- 进入玩具工厂 ---" << endl;
return make_unique<Toy>(name); // C++14 推荐用法,更安全
// 等价于 return unique_ptr<Toy>(new Toy(name));
}
// 共享函数
void share_toy(shared_ptr<Toy> sp) {
cout << " (进入共享函数) '" << sp.get()->play() <<"' 的共享者数量: " << sp.use_count() << endl;
}
int main() {
// 1. 从工厂获得一个独占的玩具
unique_ptr<Toy> my_unique_toy = make_toy("变形金刚");
my_unique_toy->play();
// 2. 将所有权转移给 shared_ptr
cout << "\n--- 将独占所有权转为共享 ---" << endl;
shared_ptr<Toy> shared_toy1 = std::move(my_unique_toy);
// my_unique_toy 现在为空了
cout << "转移后,共享者数量: " << shared_toy1.use_count() << endl;
// 3. 创建更多共享者
{
shared_ptr<Toy> shared_toy2 = shared_toy1;
cout << "创建新共享者后,数量: " << shared_toy1.use_count() << endl;
// 4. 调用共享函数
share_toy(shared_toy1);
cout << "准备离开作用域..." << endl;
} // shared_toy2 在这里销毁,引用计数-1
cout << "离开作用域后,数量: " << shared_toy1.use_count() << endl;
cout << "\n--- main函数即将结束 ---" << endl;
return 0; // shared_toy1 在这里销毁,引用计数变为0,玩具被销毁
}
点个赞和关注,更多知识包你进步,谢谢!!!你的支持就是我更新的最大动力
更多推荐
所有评论(0)