C++ 智能指针 --- 草履虫也能轻松看懂

一、为什么需要智能指针?

智能指针基于RAII(资源获取即初始化)机制,自动管理堆内存,核心解决裸指针的内存泄漏问题:把堆内存的生命周期绑定到栈上的智能指针对象,当智能指针对象超出作用域时,析构函数会自动释放绑定的堆内存,无需手动delete

C++ 中的裸指针(int* p = new int;)需要手动管理内存:

  • 忘记delete会导致内存泄漏
  • 多次delete会导致未定义行为
  • 异常场景下(比如delete前抛出异常),内存也无法释放。

二、C++ 智能指针的分类(C++11 及以后)

C++ 标准库在<memory>头文件中提供了三种核心智能指针:unique_ptrshared_ptrweak_ptr,其中auto_ptr已被废弃(设计缺陷)。

  1. unique_ptr是独占式智能指针(轻量、高效),shared_ptr是共享式(引用计数),weak_ptr辅助shared_ptr解决循环引用;
  2. 使用智能指针的核心原则:优先选unique_ptr,避免裸指针与智能指针混用,警惕shared_ptr的循环引用。

1. std::unique_ptr(独占式智能指针)

核心特点:独占对堆内存的所有权,同一时刻只能有一个unique_ptr指向同一个资源,不支持拷贝(copy),仅支持移动(move)。

基本用法(代码示例)
#include <iostream>
#include <memory> // 必须包含的头文件

// 自定义类,用于演示
class MyClass {
public:
    MyClass(int val) : value(val) {
        std::cout << "MyClass 构造:" << value << std::endl;
    }
    ~MyClass() {
        std::cout << "MyClass 析构:" << value << std::endl;
    }
    void show() {
        std::cout << "值:" << value << std::endl;
    }
private:
    int value;
};

int main() {
    // 1. 创建unique_ptr,管理堆内存
    // unique_ptr<指针类型> 指针变量名(需要管理的堆内存);
    std::unique_ptr<MyClass> ptr1(new MyClass(10)); 
    // 推荐写法(C++14+):make_unique,更安全(避免异常安全问题)
    auto ptr2 = std::make_unique<MyClass>(20);

    // 2. 使用智能指针(和裸指针用法类似)
    ptr1->show(); // 输出:值:10
    (*ptr2).show(); // 输出:值:20

    // 3. 移动语义(转移所有权)
    std::unique_ptr<MyClass> ptr3 = std::move(ptr1); // ptr1变为空,ptr3拥有资源
    if (ptr1 == nullptr) {
        std::cout << "ptr1 已为空" << std::endl; // 会执行这行
    }
    ptr3->show(); // 输出:值:10

    // 4. 手动释放资源(一般不需要,析构会自动释放)
    ptr3.reset(); // 释放资源,ptr3变为空

    // 5. 管理数组(unique_ptr专门支持数组)
    std::unique_ptr<int[]> arr_ptr(new int[5]{1,2,3,4,5});
    arr_ptr[0] = 100;
    std::cout << arr_ptr[0] << std::endl; // 输出:100

    // 6. 作为函数返回值(自动移动,无需手动std::move)
    auto get_ptr() {
        return std::make_unique<MyClass>(30);
    }
    auto ptr4 = get_ptr();
    ptr4->show(); // 输出:值:30

    return 0; // 函数结束,所有智能指针析构,自动释放堆内存
}
关键说明
  • std::make_unique是 C++14 引入的,比直接new更安全(避免 “内存泄漏 + 异常” 的场景);
  • unique_ptr不支持拷贝(std::unique_ptr<MyClass> ptr4 = ptr3; 编译报错),只能通过std::move转移所有权;
  • unique_ptr是轻量级的(大小和裸指针一样),性能几乎和裸指针无差别,是首选的智能指针(除非需要共享所有权)。

2. std::shared_ptr(共享式智能指针)

核心特点:允许多个shared_ptr共享同一个资源的所有权,底层通过引用计数实现:

  • 每新增一个shared_ptr指向该资源,引用计数 + 1;
  • 每销毁一个shared_ptr,引用计数 - 1;
  • 当引用计数变为 0 时,自动释放资源。
基本用法(代码示例)
#include <iostream>
#include <memory>

class MyClass {
public:
    MyClass(int val) : value(val) {
        std::cout << "MyClass 构造:" << value << std::endl;
    }
    ~MyClass() {
        std::cout << "MyClass 析构:" << value << std::endl;
    }
    void show() {
        std::cout << "值:" << value << std::endl;
    }
private:
    int value;
};

int main() {
    // 1. 创建shared_ptr(推荐用make_shared,更高效)
    std::shared_ptr<MyClass> ptr1 = std::make_shared<MyClass>(10);
    std::cout << "引用计数:" << ptr1.use_count() << std::endl; // 输出:1

    // 2. 拷贝,引用计数+1
    std::shared_ptr<MyClass> ptr2 = ptr1;
    std::cout << "引用计数:" << ptr1.use_count() << std::endl; // 输出:2
    std::cout << "引用计数:" << ptr2.use_count() << std::endl; // 输出:2

    // 3. 多个指针共享资源
    ptr1->show(); // 输出:值:10
    ptr2->show(); // 输出:值:10

    // 4. 重置指针,引用计数-1
    ptr1.reset();
    std::cout << "引用计数:" << ptr2.use_count() << std::endl; // 输出:1

    // 5. 管理数组(C++17+支持make_shared数组,C++11/14需手动new)
    std::shared_ptr<int[]> arr_ptr(new int[5]{1,2,3,4,5});
    arr_ptr[1] = 200;
    std::cout << arr_ptr[1] << std::endl; // 输出:200

    return 0; // ptr2析构,引用计数变为0,资源释放
}
关键问题:循环引用

shared_ptr的最大陷阱是循环引用:两个shared_ptr互相指向对方,导致引用计数永远无法变为 0,最终内存泄漏。

循环引用示例(错误)

#include <iostream>
#include <memory>

class B; // 前向声明

class A {
public:
    std::shared_ptr<B> b_ptr;
    ~A() { std::cout << "A 析构" << std::endl; }
};

class B {
public:
    std::shared_ptr<A> a_ptr;
    ~B() { std::cout << "B 析构" << std::endl; }
};

int main() {
    std::shared_ptr<A> a = std::make_shared<A>();
    std::shared_ptr<B> b = std::make_shared<B>();
    a->b_ptr = b; // A引用B
    b->a_ptr = a; // B引用A
    // 循环引用:a和b的引用计数都是2,析构时各减1,变为1,永远不会释放
    return 0; // 不会输出"A 析构"和"B 析构",内存泄漏
}

3. std::weak_ptr(弱引用智能指针)

核心特点:专门解决shared_ptr的循环引用问题,是shared_ptr的 “辅助指针”:

  • 不拥有资源的所有权,不会增加引用计数;
  • 可以观察shared_ptr管理的资源是否还存在;
  • 必须通过lock()方法转换成shared_ptr才能访问资源(避免访问已释放的资源)。
解决循环引用的示例(正确):
#include <iostream>
#include <memory>

class B;

class A {
public:
    std::weak_ptr<B> b_ptr; // 改为weak_ptr
    ~A() { std::cout << "A 析构" << std::endl; }
};

class B {
public:
    std::weak_ptr<A> a_ptr; // 改为weak_ptr
    ~B() { std::cout << "B 析构" << std::endl; }
};

int main() {
    std::shared_ptr<A> a = std::make_shared<A>();
    std::shared_ptr<B> b = std::make_shared<B>();
    a->b_ptr = b; // weak_ptr不增加引用计数
    b->a_ptr = a; // weak_ptr不增加引用计数

    // 检查资源是否存在
    if (auto temp = a->b_ptr.lock()) { // lock()返回shared_ptr,若资源存在则有效
        std::cout << "B资源存在" << std::endl;
    }

    return 0; // a和b析构,引用计数变为0,资源释放(输出"A 析构"和"B 析构")
}
关键说明
  • weak_ptr不能直接访问资源(没有->*运算符),必须通过lock()获取shared_ptr后才能访问;
  • expired()方法可以判断weak_ptr指向的资源是否已释放(返回true表示已释放);
  • weak_ptr的大小和shared_ptr相同(因为要存储引用计数的指针)。

三、智能指针的使用原则

  1. 优先使用unique_ptr(高效、无额外开销),仅在需要共享所有权时使用shared_ptr
  2. 避免用同一个裸指针创建多个智能指针(会导致重复释放);
  3. 不要手动delete智能指针管理的裸指针(智能指针析构时会再次delete);
  4. shared_ptr的循环引用必须用weak_ptr解决;
  5. 优先使用make_unique/make_shared创建智能指针(异常安全、更高效)。

四、内存泄漏案例以及改良方案

内存泄漏的核心本质是:堆内存被分配后,失去了对它的所有引用,导致程序无法再释放这块内存,直到程序退出(系统会回收,但长期运行的程序如服务器会持续占用内存)

常见的内存泄漏场景

1. 最基础:忘记释放手动分配的内存

这是新手最易犯的错误 —— 用new/malloc分配堆内存后,未调用delete/free释放,尤其是在分支、循环等复杂逻辑中更容易遗漏。

示例代码(错误)

#include <iostream>
using namespace std;

void func() {
    // 分配堆内存
    int* p = new int(10);
    string name = "test";
    // 分支逻辑导致忘记释放
    if (name == "test") {
        cout << "分支返回,遗漏delete" << endl;
        return; // 直接返回,p指向的内存永远无法释放
    }
    // 只有走else才会释放(本例不会执行)
    delete p;
}

int main() {
    func(); // 执行后内存泄漏(4字节int)
    return 0;
}

避免方法

  • 优先使用智能指针(unique_ptr/shared_ptr)替代裸指针;
  • 若必须用裸指针,遵循 “分配即规划释放” 原则,在分配内存时就确定释放的位置。

修复后的完整代码

#include <iostream>
#include <memory>  // 必须包含智能指针的头文件
using namespace std;

void func() {
    // 用unique_ptr替代裸指针,make_unique是创建unique_ptr的推荐方式
    unique_ptr<int> p = make_unique<int>(10);
    string name = "test";
    
    if (name == "test") {
        cout << "分支返回,智能指针自动释放内存" << endl;
        return; // 即使提前返回,p也会析构并释放内存
    }
    
    // 无需手动delete!智能指针超出作用域时会自动释放
    // 原来的delete p 可以完全删除
}

int main() {
    func(); // 执行后无内存泄漏
    return 0;
}

2. 异常导致的内存泄漏

new分配内存后,delete执行前抛出异常,导致delete语句无法执行,进而泄漏内存。这是比 “忘记释放” 更隐蔽的问题。

示例代码(错误)

#include <iostream>
#include <stdexcept>
using namespace std;

void riskyFunc() {
    throw runtime_error("突发异常"); // 抛出异常
}

void func() {
    int* p = new int(20); // 分配内存
    riskyFunc(); // 抛出异常,后续代码全部跳过
    delete p; // 永远执行不到,内存泄漏
}

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

避免方法

  • 核心方案:使用智能指针(RAII 机制),即使抛出异常,智能指针对象析构时仍会自动释放内存;
  • 兜底方案:用try-catch包裹,但代码冗余且易遗漏,不如智能指针可靠。

修复后的代码

#include <iostream>
#include <stdexcept>
#include <memory> // 智能指针头文件
using namespace std;

void riskyFunc() {
    throw runtime_error("突发异常");
}

void func() {
    unique_ptr<int> p = make_unique<int>(20); // 智能指针
    riskyFunc(); // 抛异常也不影响,p析构时自动释放
}

int main() {
    try {
        func();
    } catch (const exception& e) {
        cout << "捕获异常:" << e.what() << endl;
    }
    return 0; // 无内存泄漏
}

3. shared_ptr 的循环引用(进阶陷阱)

这是使用智能指针时的高频错误 —— 两个或多个shared_ptr互相持有对方的引用,导致引用计数永远无法归 0,内存无法释放。

示例代码(错误)

#include <iostream>
#include <memory>
using namespace std;

class B; // 前向声明

class A {
public:
    shared_ptr<B> b_ptr; // A持有B的shared_ptr
    ~A() { cout << "A 析构" << endl; } // 不会执行
};

class B {
public:
    shared_ptr<A> a_ptr; // B持有A的shared_ptr
    ~B() { cout << "B 析构" << endl; } // 不会执行
};

int main() {
    shared_ptr<A> a = make_shared<A>();
    shared_ptr<B> b = make_shared<B>();
    a->b_ptr = b; // 循环引用开始
    b->a_ptr = a;
    // a和b的引用计数都是2,析构时各减1变为1,永远不会释放
    return 0; // 无析构输出,内存泄漏
}

避免方法

  • 将循环引用中的一方或双方的shared_ptr替换为weak_ptr(弱引用,不增加引用计数);
  • 修复后的代码可参考上一轮讲解智能指针时的weak_ptr示例。

4. 容器存储裸指针未清理

vector/list/map等容器存储裸指针时,清空容器(如clear())仅会删除指针本身(容器内的元素),但不会释放指针指向的堆内存。

示例代码(错误)

#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int*> vec;
    // 向容器添加堆内存指针
    vec.push_back(new int(1));
    vec.push_back(new int(2));
    vec.push_back(new int(3));

    vec.clear(); // 仅清空容器,3个int的堆内存未释放,泄漏
    return 0;
}

避免方法

  • 容器中存储智能指针(如vector<unique_ptr<int>>),清空时自动释放内存;
  • 若必须存裸指针,清空容器前遍历 delete 每个元素。

修复后的代码

#include <iostream>
#include <vector>
#include <memory>
using namespace std;

int main() {
    vector<unique_ptr<int>> vec;
    vec.push_back(make_unique<int>(1));
    vec.push_back(make_unique<int>(2));
    vec.push_back(make_unique<int>(3));

    vec.clear(); // 自动释放所有堆内存,无泄漏
    return 0;
}

5. 动态数组释放错误(delete vs delete [])

new[]分配的数组,若误用delete(而非delete[])释放:

  • 对于类对象数组:仅调用第一个元素的析构函数,其余元素的析构函数不执行,导致内存泄漏;
  • 对于内置类型数组(int/char 等):看似无泄漏,但属于 “未定义行为”,可能引发其他问题。

示例代码(错误)

#include <iostream>
using namespace std;

class MyClass {
public:
    MyClass() { cout << "MyClass 构造" << endl; }
    ~MyClass() { cout << "MyClass 析构" << endl; }
};

int main() {
    // 分配对象数组
    MyClass* arr = new MyClass[3]; // 输出3次构造
    delete arr; // 错误!仅调用第一个对象的析构,后2个泄漏
    // 正确写法:delete[] arr;
    return 0;
}

避免方法

  • 严格遵循 “newdeletenew[]delete[]” 的规则;
  • 优先使用vectorunique_ptr<T[]>管理动态数组(无需手动释放)。

6. 全局 / 静态指针的内存泄漏

全局或静态指针指向堆内存时,若程序结束前未释放:

  • 虽然程序退出后操作系统会回收内存,但长期运行的程序(如服务器、后台服务)会持续占用内存,最终导致内存耗尽;
  • 不符合 “资源用完即释放” 的编程规范。

示例代码(错误)

#include <iostream>
using namespace std;

// 全局指针
int* g_ptr = new int(100);

int main() {
    // 程序运行期间未释放g_ptr,直到退出才被系统回收
    cout << *g_ptr << endl;
    // 遗漏:delete g_ptr;
    return 0;
}

避免方法

  • 用全局智能指针(如static unique_ptr<int> g_ptr = make_unique<int>(100));
  • 在程序退出前(如 main 结束前)显式释放全局 / 静态裸指针。

7. 第三方库资源未释放

使用第三方库的 API 分配资源(如自定义句柄、内存、句柄)时,未调用库提供的 “释放函数”,导致泄漏(这类泄漏常被忽略)。

示例场景(伪代码)

// 第三方库API示例
void* create_obj(); // 分配资源,返回指针
void destroy_obj(void* p); // 释放资源

int main() {
    void* obj = create_obj(); // 分配资源
    // 业务逻辑...
    destroy_obj(obj); // 忘记调用,资源泄漏
    return 0;
}

避免方法

  • 封装成 RAII 类,析构函数中调用释放函数;
  • 记录所有 “分配 - 释放” API 对,确保成对调用。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值