【C++】智能指针
目录
1. 为什么需要智能指针?
在传统的 C++ 中,我们使用 new 和 delete 来手动管理堆内存。
voidproblem(){ MyClass* ptr =newMyClass();// 分配内存// ... 一些业务逻辑 ...if(some_condition){throw std::runtime_error("Oops!");// 如果这里抛出异常delete ptr;// 这行代码不会被执行!return;}// ... 更多业务逻辑 ...delete ptr;// 正常情况下的释放}如果在 delete ptr 之前,由于条件判断、异常、或者程序员疏忽而提前返回,那么 ptr 所指向的内存就永远无法被释放,导致内存泄漏。
2. 核心思想:RAII
它的基本原理非常简单,却极其强大:
- 在构造函数中获取资源或者其管理权(例如:分配内存、打开文件、加锁)。
- 在析构函数中释放资源(例如:释放内存、关闭文件、解锁)。
由于 C++ 保证,当一个对象离开其作用域时(无论是正常离开还是因为异常),它的析构函数一定会被自动调用。因此,通过 RAII,我们就能确保资源一定会被释放。
智能指针就是一个将 RAII 思想应用于动态内存管理的完美范例。 它将一个裸指针(raw pointer)包装成一个类对象,并在其析构函数中自动执行 delete。
3. C++ 中的主要智能指针
C++ 11 引入了三种主要的智能指针,位于 memory 头文件中。
3.1 auto_ptr
- auto_ptr 是什么?
auto_ptr是C++98标准中引入的第一个智能指针,它的主要目的是实现自动内存管理(RAII机制)。 - auto_ptr 的核心特性:管理权转移
这是auto_ptr最核心也最受诟病的特性。
什么是管理权转移?
当拷贝一个auto_ptr时,资源的所有权会从源对象转移到目标对象,源对象会变成空指针。
#include<iostream>#include<memory>usingnamespace std;voidownership_transfer(){ auto_ptr<int>p1(newint(100)); cout <<"p1 points to: "<< p1.get()<< endl;// 有地址 auto_ptr<int>p2(p1);// 拷贝构造:管理权转移 cout <<"p1 after copy: "<< p1.get()<< endl;// 变成nullptr! cout <<"p2 points to: "<< p2.get()<< endl;// p2获得地址 cout <<*p2 << endl;// 正常:100// cout << *p1 << endl; // 崩溃!p1已经是空指针}- auto_ptr 的模拟实现
template<classT>classauto_ptr{public:auto_ptr(T* ptr):_ptr(ptr){}auto_ptr(auto_ptr<T>& ap):_ptr(ap._ptr){ ap._ptr =nullptr;}~auto_ptr(){ cout <<"delete: "<< _ptr << endl;delete _ptr;} T&operator*(){return*_ptr;} T*operator->(){return _ptr;}private: T* _ptr;};- 为什么auto_ptr被弃用?
主要问题:
- 违反直觉:拷贝操作不应该改变原对象
- 不适用于STL容器:STL算法和容器要求元素可以正常拷贝
- 容易导致隐藏的bug:空指针问题在编译期无法发现
重要建议:在新项目中绝对不要使用auto_ptr,始终使用C++11引入的现代智能指针。
3.2 unique_ptr
基本特性:
- 独占所有权:一个资源只能被一个unique_ptr拥有
- 禁止拷贝:不能拷贝构造,不能拷贝赋值
- 支持移动:可以通过std::move转移所有权
- 零开销:与裸指针相比几乎没有性能损失,相当于裸指针,因为没有开辟多余空间
模拟实现
template<classT>classunique_ptr{public:unique_ptr(T* ptr):_ptr(ptr){}unique_ptr(unique_ptr<T>& ap)=delete; unique_ptr<T>&operator=(unique_ptr<T>& ap)=delete;~unique_ptr(){ cout <<"delete: "<< _ptr << endl;delete _ptr;} T&operator*(){return*_ptr;} T*operator->(){return _ptr;}private: T* _ptr;};创建 unique_ptr:
#include<memory>#include<iostream>usingnamespace std;voidbasic_usage(){// 方式1:直接构造 unique_ptr<int>p1(newint(42));// 方式2:推荐使用make_unique (C++14)auto p2 = make_unique<int>(100);auto p3 = make_unique<string>("Hello");// 方式3:创建数组auto p4 = make_unique<int[]>(10);// 10个int的数组 cout <<*p2 << endl;// 解引用:100 cout << p3->length()<< endl;// 箭头操作符:5}3.3 shared_ptr
3.3.1 shared_ptr的原理和特性
核心特性:
- 共享所有权。多个 shared_ptr 可以共同拥有同一个对象。
- 内部使用引用计数来跟踪有多少个 shared_ptr 指向同一个对象。
- 当最后一个指向该对象的 shared_ptr 被销毁或重置时,对象才会被删除。
- 由于需要维护引用计数,它有微小的内存和性能开销。
引用计数就是记录有多少个 shared_ptr 指向同一个对象:
- 当新的
shared_ptr指向对象时,计数+1 - 当
shared_ptr被销毁时,计数-1 - 当计数变为0时,自动删除对象
模拟实现
template<classT>classshared_ptr{public:shared_ptr(T* ptr =nullptr):_ptr(ptr),_pcount(newint(1))//当直接构造时,开辟了一个空间,放对象对应的引用计数{} T&operator*(){return*_ptr;} T*operator->(){return _ptr;}~shared_ptr(){//如果*_pcount == 0的话,就释放所有资源if(--(*_pcount)==0){ cout <<"delete"<< _ptr << endl;delete _ptr;delete _pcount;}}//拷贝构造shared_ptr(const shared_ptr<T>& sp){//共享所有权 (*_pcount)++; _pcount = sp._pcount; _ptr = sp._ptr;(*_pcount)++;}//赋值 shared_ptr<T>&operator=(const shared_ptr<T>& sp){// 管理对象的改变,两者管理同一个对象if(_ptr == sp._ptr){return*this;}if(--(*_pcount)==0){delete _pcount;delete _ptr;} _pcount = sp._pcount; _ptr = sp._ptr;++(*_pcount);return*this;}intuse_count()const{return*_pcount;} T*get()const{return _ptr;}private: T* _ptr;int* _pcount;};最佳实践和注意事项
- 优先使用 std::make_shared
// 好:一次内存分配,效率更高auto ptr = std::make_shared<MyClass>();// 不好:两次内存分配 std::shared_ptr<MyClass>ptr(newMyClass());- 不要混用原始指针和shared_ptr
MyClass* raw_ptr =newMyClass(); std::shared_ptr<MyClass>ptr1(raw_ptr);// std::shared_ptr<MyClass> ptr2(raw_ptr); // 错误!会导致重复释放- 小心循环引用
classNode{public: std::shared_ptr<Node> next;// std::shared_ptr<Node> prev; // 这会导致循环引用! std::weak_ptr<Node> prev;// 应该用weak_ptr};- 不要get()之后手动delete
std::shared_ptr<MyClass> ptr = std::make_shared<MyClass>(); MyClass* raw = ptr.get();// delete raw; // 绝对不要这样做!————————————————————————————————————————————
共享所有权时要注意的情况:
- 共享同一对象,有一个计数系统(正确):
// 先创建一个对象 std::shared_ptr<MyClass>ptr1(newMyClass());// 对象A// 然后让其他shared_ptr指向同一个对象 std::shared_ptr<MyClass> ptr2 = ptr1;// 也指向对象A std::shared_ptr<MyClass> ptr3 = ptr1;// 也指向对象A- 内存中有3个完全独立的MyClass对象,每个shared_ptr管理一个不同的对象,有三个记数系统(错误):
std::shared_ptr<MyClass>ptr1(newMyClass());// 对象A std::shared_ptr<MyClass>ptr2(newMyClass());// 对象B std::shared_ptr<MyClass>ptr3(newMyClass());// 对象C- 虽然是指向了同一对象,但是是三个不同的计数系统,每个系统计数为1,当ptr1销毁时,计数为0,也销毁raw_ptr指向的对象,而ptr2销毁时候,也会销毁一次raw_ptr指向的对象,造成不存在的对象销毁的情况(错误):
MyClass* raw_ptr =newMyClass();// 原始指针 std::shared_ptr<MyClass>ptr1(raw_ptr); std::shared_ptr<MyClass>ptr2(raw_ptr); std::shared_ptr<MyClass>ptr3(raw_ptr);共享所有权的关键是:利用已有的shared_ptr创建新的shared_ptr,而不是各自用new创建。
3.3.2 shared_ptr 定制删除器
(1) 为什么需要定制删除器?
在 C++ 中,我们通常使用 new 和 delete 来管理动态内存。但实际开发中,资源的获取和释放方式多种多样:
- new[] / delete[] - 数组内存管理
- malloc() / free() - C 风格内存管理
- fopen() / fclose() - 文件资源管理
- 其他第三方库的资源管理函数
如果对这些不同类型的资源都使用标准的 delete,会导致资源泄漏或未定义行为。所以下面写的析构函数其实是错的。
~shared_ptr(){//如果*_pcount == 0的话,就释放所有资源if(--(*_pcount)==0){ cout <<"delete"<< _ptr << endl;delete _ptr;delete _pcount;}}(2) 定制删除器的本质
定制删除器就是一个可调用对象(函数指针、仿函数、lambda 表达式),它知道如何正确释放特定类型的资源。
(3) 代码实现解析
template<classT>classshared_ptr{private: T* _ptr;// 管理的指针int* _pcount;// 引用计数 function<void(T*)> _del =[](T* t){// 类内初始化默认删除器delete t;};public:// 构造函数shared_ptr(T* ptr =nullptr):_ptr(ptr),_pcount(newint(1)){}// 支持删除器的构造函数template<classD>shared_ptr(T* ptr, D del):_ptr(ptr),_pcount(newint(1)),_del(del)// 使用传入的删除器{}// 使用删除器的析构函数~shared_ptr(){if(--(*_pcount)==0){ cout <<"delete"<< _ptr << endl;_del(_ptr);// 使用定制删除器释放资源delete _pcount;}}};(4) 使用实例
场景1:管理动态数组(new[])
// 仿函数作为删除器template<classT>structDeleteArray{voidoperator()(T* ptr){delete[] ptr;}};structA{int _a;A(int a =0):_a(a){ cout <<"A(int a = 0)"<< endl;}~A(){ cout <<"~A()"<< endl;}};intmain(){ wzx::shared_ptr<A>sp1(new A[10], DeleteArray<A>());return0;}场景2:管理 malloc 内存
// 函数指针作为删除器voidFreeInt(int* a){ cout <<"free()"<< endl;free(a);}intmain(){ wzx::shared_ptr<int>sp2((int*)malloc(sizeof(int)), FreeInt);return0;}场景3:管理文件资源
// lambda表达式作为删除器intmain(){ wzx::shared_ptr<FILE>sp3(fopen("Test.cpp","r"),[](FILE* f){ cout <<"fclose()"<< endl;fclose(f);});return0;}场景4:默认情况(普通 new)
intmain(){// 使用默认的delete删除器 wzx::shared_ptr<A>sp4(newA(1));return0;}(5) 补充知识点
1. std::function 的使用:
function<void(T*)> _del;- function 可以包装任何可调用对象
- _del 就是 void(T*) 函数指针
2. using 别名:
// 传统的 typedef 写法typedefvoid(*FuncPtr)(int);typedef std::map<std::string, std::vector<int>> ComplexMap;// 现代的 using 写法(更清晰)using FuncPtr =void(*)(int);using ComplexMap = std::map<std::string, std::vector<int>>;//using + function using FuncPtr = function<void(*)(int)>;3.3.3 循环引用
- 什么是循环引用?
循环引用指的是两个或多个对象通过shared_ptr相互引用,形成环状依赖,导致引用计数永远无法降为0,从而内存泄漏。 - 简单的循环引用示例:
#include<iostream>#include<memory>usingnamespace std;classB;// 前向声明classA{public: shared_ptr<B> b_ptr;//一个shared_ptr<B>类型的智能指针,注意:还没有确定该智能指针指向哪个对象A(){ cout <<"A constructed"<< endl;}~A(){ cout <<"A destroyed"<< endl;}};classB{public: shared_ptr<A> a_ptr;//一个shared_ptr<A>类型的智能指针,注意:还没有确定该智能指针指向哪个对象B(){ cout <<"B constructed"<< endl;}~B(){ cout <<"B destroyed"<< endl;}};voidcircular_reference_demo(){ cout <<"=== 循环引用演示 ==="<< endl;auto a = make_shared<A>();//一个A匿名对象被创建,并且a管理它auto b = make_shared<B>();//一个B匿名对象被创建,并且b管理它//A,B对象都是new出来的,不在栈上,需要手动释放。其实就是只有全部智能指针释放,它才会被释放。//我们不会手动去释放,因为交给了智能指针管理 cout <<"初始引用计数:"<< endl; cout <<"a use_count: "<< a.use_count()<< endl;// 1 cout <<"b use_count: "<< b.use_count()<< endl;// 1// 创建循环引用//智能指针的赋值函数:使得两个智能指针管理同一个对象 a->b_ptr = b;// a->b_ptr也管理了B对象,B对象 的引用计数变成 2 b->a_ptr = a;// b->a_ptr也管理了A对象,A对象 的引用计数变成 2 cout <<"循环引用后:"<< endl; cout <<"a use_count: "<< a.use_count()<< endl;// 2 cout <<"b use_count: "<< b.use_count()<< endl;// 2}intmain(){circular_reference_demo(); cout <<"函数结束,但A和B都没有被销毁!"<< endl;return0;}输出:
=== 循环引用演示 === A constructed B constructed 初始引用计数: a use_count:1 b use_count:1 循环引用后: a use_count:2 b use_count:2 函数结束,但A和B都没有被销毁! 
循环引用的核心问题:
- shared_ptr 相互引用导致引用计数永远 ≥ 1
- 对象无法被析构,内存泄漏
3.4 weak_ptr
- weak_ptr同样也是c++11的智能指针
- 它的出现仅仅是为了解决shared_ptr的循环引用的问题
- weak_ptr有可以像指针一样使用的性质,但是不支持RAII
- 可以RAII的智能指针,一定都有删除器,只有weak_ptr没有删除器
- weak_ptr就相当于一个裸指针
template<classT>classweak_ptr{public:weak_ptr():_ptr(nullptr){}// 从 shared_ptr 构造 weak_ptrweak_ptr(const shared_ptr<T>& sp):_ptr(sp.get()){} T&operator*(){return*_ptr;} T*operator->(){return _ptr;}private: T* _ptr;};4. make_unique、make_shared 工厂函数
make_unique - C++14 引入的工厂函数,用于创建 std::unique_ptr 智能指针。
make_shared - C++11 引入的工厂函数,用于创建 std::shared_ptr 智能指针。
(1) make_unique
我们先来讲讲make_unique 的使用方法:
- 单个对象:
make_unique<类型>(构造参数) - 数组:
make_unique<类型[]>(元素个数) - make_unique 放回值都是unique_ptr 智能指针
- 数组版本只能调用默认构造函数
- 数组元素需要后续手动初始化
创建单个对象:
// make_unique - 创建 unique_ptrauto p1 = make_unique<int>(42);auto p2 = make_unique<string>("Hello");创建数组:
// make_unique - 创建数组auto arr1 = make_unique<int[]>(5);// 5个int的数组具体实例:
#include<iostream>#include<memory>classMyClass{public:MyClass(int val,const std::string& name):value(val),name(name){ std::cout <<"构造: "<< name <<" = "<< value <<"\n";}~MyClass(){ std::cout <<"析构: "<< name <<"\n";}voidprint()const{ std::cout << name <<": "<< value <<"\n";}private:int value; std::string name;};intmain(){// 使用 make_unique 创建对象auto obj1 = make_unique<MyClass>(100,"obj1"); obj1->print();// 创建数组auto arr = make_unique<int[]>(3); arr[0]=10; arr[1]=20; arr[2]=30;return0;}(2) make_shared
make_shared 和 make_unique 的使用方法一模一样:
- 单个对象:
make_shared<类型>(构造参数) - 数组:
make_shared<类型[]>(元素个数) - make_shared 放回值都是shared_ptr 智能指针
需要额外注意的点:
make_shared 是一次内存分配,更加高效。
// 低效写法:两次内存分配 shared_ptr<MyClass>p1(newMyClass(42,"test"));// 1. 分配 MyClass 对象// 2. 分配控制块(引用计数等)// 高效写法:一次内存分配auto p2 = make_shared<MyClass>(42,"test");// 1. 一次性分配对象+控制块5. 智能指针总结
| 特性 | unique_ptr | shared_ptr | weak_ptr |
|---|---|---|---|
| 所有权 | 独占 | 共享 | 不拥有 |
| 拷贝 | 禁止 | 允许(增加计数) | 允许(不增加计数) |
| 移动 | 允许(转移所有权) | 允许(转移所有权) | 允许 |
| 性能 | 零开销(同裸指针) | 有开销(引用计数) | 有开销 |
| 适用场景 | 单一明确所有者 | 多个不确定的所有者 | 打破循环引用、缓存 |
最佳实践:
- 首选 unique_ptr:除非你需要共享所有权,否则默认使用 unique_ptr。它最安全、最高效。
- 使用 make_shared 和 make_unique:
- auto ptr = std::make_unique(args…);
- auto ptr = std::make_shared(args…);
- 优点:更安全(防止异常导致的内存泄漏)、更高效(对于 shared_ptr,可以将引用计数和对象放在同一块内存分配)。
- 避免使用裸指针进行内存管理:将所有的 new 和 delete 替换为智能指针。
- 不要混用智能指针和裸指针:不要用同一个裸指针初始化多个智能指针,这会导致重复 delete。
- 明确 weak_ptr 的用途:只在需要打破循环引用或作为临时可失效的观察者时使用。
6. 内存泄漏
(1) 基本概念
内存泄漏是指程序在动态分配内存后,由于设计错误或疏忽未能正确释放已经不再使用的内存。这不是物理内存的消失,而是程序失去了对已分配内存的控制权,导致这部分内存无法被重新利用。
(2) 内存泄漏示例
#include<iostream>#include<stdexcept>usingnamespace std;voidriskyFunction(){throwruntime_error("发生异常!");}// 示例1:普通内存泄漏voidsimpleMemoryLeak(){int* ptr =newint(10);// 申请内存// 忘记调用 delete ptr; cout <<"值: "<<*ptr << endl;}// 示例2:异常导致的内存泄漏voidexceptionMemoryLeak(){int* ptr1 =newint(20);int* ptr2 =newint(30);riskyFunction();// 如果这里抛出异常delete ptr1;// 这行不会执行delete ptr2;// 这行也不会执行}// 示例3:循环引用导致的内存泄漏(智能指针场景)structNode{ shared_ptr<Node> next; shared_ptr<Node> prev;int data;Node(int val):data(val){ cout <<"创建节点: "<< data << endl;}~Node(){ cout <<"销毁节点: "<< data << endl;}};(3) 内存泄漏分类
- 堆内存泄漏 (Heap Leak)
最常见的类型,发生在动态内存管理中:
voidheapLeakExamples(){// 1. 直接忘记释放char* buffer =newchar[1024];// 使用buffer...// 忘记: delete[] buffer;// 2. 提前返回导致泄漏int* data =newint[100];if(someCondition){return;// 直接返回,内存泄漏!}delete[] data;// 3. 异常抛出导致泄漏int* resource =newint[50];someRiskyOperation();// 可能抛出异常delete[] resource;// 异常时不会执行}- 系统资源泄漏
#include<fstream>#include<cstdio>voidresourceLeakExamples(){// 文件句柄泄漏 FILE* file =fopen("data.txt","r");if(file){// 处理文件...// 忘记: fclose(file);}// 动态库句柄泄漏// void* handle = dlopen("library.so", RTLD_LAZY);// 忘记: dlclose(handle);}(4) 总结
内存泄漏是C++开发中的常见问题,但通过以下方法可以有效避免:
- 优先使用智能指针而不是裸指针
- 遵循RAII原则,在构造函数中获取资源,在析构函数中释放
- 编写异常安全的代码,确保异常发生时资源正确释放
- 使用标准库容器而不是手动管理数组