【c++面向对象编程】第35篇:构造函数与异常:如何避免资源泄露?

目录

一、构造函数可以抛异常吗?

二、构造函数抛异常时,哪些资源会自动释放?

三、裸指针的陷阱:资源泄露

四、解决方案1:用智能指针管理动态资源

[五、解决方案2:函数级 try-catch](#五、解决方案2:函数级 try-catch)

六、完整例子:对比三种方案

七、构造函数异常与继承

八、最佳实践总结

九、这一篇的收获


一、构造函数可以抛异常吗?

可以,而且有时候必须抛异常。

构造函数没有返回值,无法像普通函数那样返回错误码。如果构造过程中发生错误(比如分配内存失败、参数无效),唯一的选择就是抛出异常。

cpp

复制代码
class Buffer {
private:
    int* data;
    size_t size;
public:
    Buffer(size_t n) : size(n) {
        if (n == 0) {
            throw std::invalid_argument("大小不能为0");
        }
        data = new int[n];  // 可能抛出 bad_alloc
    }
    ~Buffer() { delete[] data; }
};

二、构造函数抛异常时,哪些资源会自动释放?

C++ 保证:已完成构造的成员对象会自动析构

cpp

复制代码
#include <iostream>
#include <stdexcept>
using namespace std;

class Member {
    string name;
public:
    Member(const string& n) : name(n) {
        cout << "Member 构造: " << name << endl;
    }
    ~Member() {
        cout << "Member 析构: " << name << endl;
    }
};

class Container {
    Member m1;
    Member m2;
    Member m3;
public:
    Container() : m1("m1"), m2("m2"), m3("m3") {
        cout << "Container 构造函数体开始" << endl;
        throw runtime_error("构造失败");
        cout << "Container 构造函数体结束" << endl;
    }
    ~Container() {
        cout << "Container 析构" << endl;
    }
};

int main() {
    try {
        Container c;
    }
    catch (const exception& e) {
        cout << "捕获异常: " << e.what() << endl;
    }
    return 0;
}

输出:

text

复制代码
Member 构造: m1
Member 构造: m2
Member 构造: m3
Container 构造函数体开始
Member 析构: m3
Member 析构: m2
Member 析构: m1
捕获异常: 构造失败

关键观察

  • 先构造成员对象(m1m2m3

  • 构造函数体抛异常

  • 成员对象按构造逆序自动析构m3m2m1

  • Container 的析构函数不会被调用(因为对象本身未完成构造)


三、裸指针的陷阱:资源泄露

上面的规则有一个重要的例外:裸指针指向的动态内存不会自动释放

cpp

复制代码
class Leaky {
private:
    int* p1;
    int* p2;
public:
    Leaky() {
        p1 = new int[1000];      // 第一步
        p2 = new int[1000];      // 第二步:如果这里抛出异常
    }
    ~Leaky() {
        delete[] p1;
        delete[] p2;
    }
};

如果 new int[1000] 抛出 bad_alloc

  1. p1 已经指向一块内存

  2. 异常被抛出,Leaky 对象构造失败

  3. ~Leaky() 不会被调用(因为对象未完成构造)

  4. p1 指向的内存永远不会被释放 → 内存泄露

这就是构造函数异常中最隐蔽的内存泄露来源。


四、解决方案1:用智能指针管理动态资源

cpp

复制代码
#include <memory>
using namespace std;

class Safe {
private:
    unique_ptr<int[]> p1;
    unique_ptr<int[]> p2;
public:
    Safe(size_t n1, size_t n2) 
        : p1(make_unique<int[]>(n1)), 
          p2(make_unique<int[]>(n2)) {
        // 即使 p2 的构造抛异常,p1 也会被 unique_ptr 自动释放
        // 因为 p1 作为成员对象,已经完成构造,会被自动析构
    }
    // 不需要手动写析构函数!
};

原理unique_ptr 是成员对象,C++ 保证已构造的成员对象会被自动析构。即使构造函数体还没执行,初始化列表中的成员构造失败也会正确清理。


五、解决方案2:函数级 try-catch

如果必须使用裸指针,可以在构造函数中使用 function-try-block

cpp

复制代码
class Safe {
private:
    int* p1;
    int* p2;
public:
    Safe(size_t n1, size_t n2) 
    try : p1(new int[n1]), p2(new int[n2]) {
        // 构造函数体
    }
    catch (...) {
        // 注意:这里必须清理已分配的资源
        delete[] p1;   // p2 分配失败时,p1 已被分配,需要手动清理
        delete[] p2;
        throw;  // 重新抛出异常
    }
    
    ~Safe() {
        delete[] p1;
        delete[] p2;
    }
};

但这种写法比智能指针繁琐且易错,不推荐。


六、完整例子:对比三种方案

cpp

复制代码
#include <iostream>
#include <memory>
#include <stdexcept>
using namespace std;

// ========== 方案1:裸指针(危险)==========
class LeakyPointer {
    int* big1;
    int* big2;
public:
    LeakyPointer() {
        big1 = new int[1000000];
        cout << "已分配 big1" << endl;
        // 模拟第二个分配失败
        throw bad_alloc();
        big2 = new int[1000000];
    }
    ~LeakyPointer() {
        delete[] big1;
        delete[] big2;
        cout << "LeakyPointer 析构" << endl;
    }
};

// ========== 方案2:智能指针(安全)==========
class SafePointer {
    unique_ptr<int[]> big1;
    unique_ptr<int[]> big2;
public:
    SafePointer() 
        : big1(make_unique<int[]>(1000000)),
          big2(make_unique<int[]>(1000000)) {  // 如果这里抛异常
        cout << "SafePointer 构造完成" << endl;
    }
    // 不需要析构函数
};

// ========== 方案3:成员对象管理(安全)==========
class BigArray {
    unique_ptr<int[]> data;
public:
    BigArray(size_t n) : data(make_unique<int[]>(n)) {
        cout << "BigArray 构造 " << n << " 个元素" << endl;
    }
};

class Composite {
    BigArray a1;
    BigArray a2;
public:
    Composite() : a1(1000000), a2(1000000) {
        cout << "Composite 构造完成" << endl;
    }
    // 不需要析构函数
};

int main() {
    cout << "=== 测试智能指针方案(安全)===" << endl;
    try {
        SafePointer sp;
    }
    catch (const bad_alloc& e) {
        cout << "捕获异常: " << e.what() << endl;
    }
    
    cout << "\n=== 测试成员对象方案(安全)===" << endl;
    try {
        Composite c;
    }
    catch (const bad_alloc& e) {
        cout << "捕获异常: " << e.what() << endl;
    }
    
    cout << "\n=== 测试裸指针方案(危险)===" << endl;
    try {
        LeakyPointer lp;   // 内存泄露!
    }
    catch (const bad_alloc& e) {
        cout << "捕获异常: " << e.what() << endl;
        cout << "注意:big1 的内存已经泄露" << endl;
    }
    
    return 0;
}

输出(示意):

text

复制代码
=== 测试智能指针方案(安全)===
SafePointer 构造完成

=== 测试成员对象方案(安全)===
BigArray 构造 1000000 个元素
BigArray 构造 1000000 个元素
Composite 构造完成

=== 测试裸指针方案(危险)===
已分配 big1
捕获异常: std::bad_alloc
注意:big1 的内存已经泄露

七、构造函数异常与继承

派生类构造函数抛异常时,基类子对象已经被构造,会被自动析构。

cpp

复制代码
class Base {
    int* data;
public:
    Base() : data(new int[1000]) {
        cout << "Base 构造" << endl;
    }
    ~Base() {
        delete[] data;
        cout << "Base 析构" << endl;
    }
};

class Derived : public Base {
    int* more_data;
public:
    Derived() : Base(), more_data(new int[1000]) {
        cout << "Derived 构造" << endl;
        throw runtime_error("构造失败");
    }
    ~Derived() {
        delete[] more_data;
        cout << "Derived 析构" << endl;
    }
};

int main() {
    try {
        Derived d;
    }
    catch (...) {
        cout << "捕获异常" << endl;
    }
    return 0;
}

输出:

text

复制代码
Base 构造
Base 析构
捕获异常

Derived 的析构函数没有被调用(因为对象未完成构造),但 Base 子对象已经被析构,data 被正确释放。


八、最佳实践总结

规则 说明
用智能指针代替裸指针 unique_ptrshared_ptr 自动管理资源
遵循 RAII 资源应在构造函数中获取,析构函数中释放
使用成员对象管理资源 vectorstring、智能指针等 RAII 类持有资源
避免在构造函数体中分配资源 优先在初始化列表中分配
如果必须用裸指针 使用 function-try-block 并手动清理
不要在构造函数中抛异常后留下裸指针资源 这是内存泄露的常见来源

九、这一篇的收获

你现在应该理解:

  • 构造函数可以抛异常,这是报告构造失败的唯一方式

  • 已完成构造的成员对象会自动析构(包括智能指针)

  • 裸指针指向的动态内存不会自动释放,即使它是成员变量

  • 用智能指针unique_ptrshared_ptr)代替裸指针,自动解决这个问题

  • 派生类构造失败时,基类子对象会被正确析构

💡 小作业:写一个 FileHandler 类,构造函数接收文件名并打开文件(用 fopen)。如果文件打开失败,抛出异常。用 unique_ptr<FILE, CustomDeleter> 管理文件句柄,确保构造失败时不会泄露。写测试代码验证异常安全。


下一篇预告 :第36篇《析构函数应永远不抛出异常------原因与最佳实践》------为什么析构函数抛异常是"死刑"?栈展开过程中抛异常会导致 terminate。如何写"不抛异常"的析构函数?catch 所有异常并记录日志是最佳实践。

相关推荐
玖釉-4 小时前
C++ 中的 buckets 详解:从哈希桶到 unordered_map 底层原理
算法·哈希算法·散列表
桀人4 小时前
类和对象——下
开发语言·c++
一只大袋鼠4 小时前
Git (三):Tag 标签管理、图形工具、IDEA 集成与 GitLab 私有化部署
开发语言·git·gitlab
z200509304 小时前
今日算法(二叉树剪枝)
数据结构·c++·算法·剪枝
雪度娃娃4 小时前
Asio异步读写——简单服务器和客户端异步通信
运维·服务器·网络·c++·php
froginwe114 小时前
Matplotlib 中文显示
开发语言
IronMurphy4 小时前
【算法四十八】416. 分割等和子集
算法
2601_953660374 小时前
File类
linux·开发语言·python
GIOTTO情4 小时前
Infoseek 媒介投放 API 实战:基于 Python 的全流程自动化方案摘要
开发语言·python·自动化