🚀 重生之我在10天内卷赢C++ - DAY 8

导师寄语:嘿,我的“代码建筑师”!你已经能盖起宏伟的“软件大厦”了,有继承的框架,有多态的电梯,功能强大。但再宏伟的大厦,也怕“意外事故”——比如用户输入了错误的数据,或者内存申请失败了。如果处理不好,大厦随时可能“崩塌”(程序崩溃)。今天,我们要为我们的代码装上“安全网”和“消防系统”,学习如何优雅地处理运行时错误——异常处理 (Exception Handling)。然后,我们将拿到一把“万能钥匙”,彻底告别 delete 的烦恼,它就是现代C++的超级神器——智能指针 (Smart Pointers)

🎯 今日目标

  1. 理解异常是什么,以及为什么需要它。
  2. 掌握 try...catch...throw 语法,学会捕获和处理异常。
  3. 学会使用C++标准库提供的标准异常
  4. 学会自定义异常类型,让错误信息更明确。
  5. 【核心】 掌握RAII思想,并精通三种智能指针unique_ptr, shared_ptr, 和 weak_ptr

1. 异常处理:优雅地面对错误

想象一下,你写了一个除法函数 divide(a, b)。如果用户输入的 b0,会发生什么?程序会因为“除以零”这个致命错误而直接崩溃。

传统的错误处理方式是返回一个特殊值:

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 三部曲

这就像一个“安全生产”流程:

  1. try (尝试):把可能出问题的代码块放进 try{} 里。这是我们的“重点监控区域”。
  2. throw (抛出):当在 try 块里发现错误时,用 throw 关键字“扔”出一个异常对象。这个异常对象可以是任何类型(int, string, 类对象等)。throw 一旦执行,后面的代码就不再执行了,程序会立即跳转去寻找能接住这个异常的东西。
  3. 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 来打破循环。
几乎永远不要再直接使用 newdelete 来管理对象的生命周期(除非你在写自己的底层容器或智能指针)。


✍️ DAY 8 作业

  1. 带异常检查的vector访问器

    • 编写一个函数 get_element(const vector<int>& vec, int index)
    • 这个函数接收一个 intvector 和一个索引 index
    • 如果 index 超出了 vector 的有效范围(0vec.size() - 1),函数应该抛出一个 std::out_of_range 异常。
    • 如果索引有效,函数返回该位置的元素。
    • main函数中,用 try...catch 块来调用这个函数,并分别测试有效索引和无效索引的情况。
  2. 智能指针管理工厂

    • 有一个 Toy 类,在构造函数和析构函数中打印信息,让我们能看到它的生与死。
    • 编写一个函数 make_toy(),它在堆上创建一个 Toy 对象,并返回一个管理这个对象的 std::unique_ptr<Toy>
    • 编写另一个函数 share_toy(std::shared_ptr<Toy> sp),它接收一个 shared_ptr,并打印出当前玩具的共享数量。
    • main 函数中:
      1. 调用 make_toy() 获得一个玩具的 unique_ptr
      2. 将这个 unique_ptr 的所有权转移给一个 shared_ptr
      3. 创建几个其他的 shared_ptr 来共享这个玩具。
      4. 调用 share_toy() 函数,观察引用计数的变化。
      5. 让所有 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,玩具被销毁
}

点个赞和关注,更多知识包你进步,谢谢!!!你的支持就是我更新的最大动力

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐