跳到主要内容 C++ 内存管理与智能指针详解:RAII 及 shared_ptr 实现 | 极客日志
C++
C++ 内存管理与智能指针详解:RAII 及 shared_ptr 实现 C++ 中的 RAII 编程范式及其在资源管理中的应用。详细讲解了智能指针 auto_ptr、unique_ptr、shared_ptr 和 weak_ptr 的特性与区别。重点阐述了 unique_ptr 的独占所有权机制及定制删除器用法,shared_ptr 的引用计数原理及循环引用问题解决方案。通过代码示例展示了智能指针的模拟实现过程,强调了异常安全性和内存泄漏预防的重要性。
魔尊 发布于 2026/3/24 更新于 2026/4/16 8.2K 浏览RAII
RAII(Resource Acquisition Is Initialization)是一种广泛应用于 C++ 等编程语言中的编程范式,它的核心思想是:资源的获取和释放与对象的生命周期绑定 。在 RAII 中,资源(如内存、文件句柄、网络连接等)的获取通常发生在对象的构造函数中,而资源的释放则发生在对象的析构函数中。
这种设计模式确保了资源在不再需要时自动释放 ,从而避免了手动管理资源的复杂性和潜在的错误 (如内存泄漏和资源泄露)。
核心思想
资源获取:当一个对象被创建时,它会立即获取某个资源。例如,分配内存、打开文件或创建数据库连接等。
资源释放:当该对象超出作用域或被销毁时,它的析构函数会自动释放相应的资源。这意味着开发者不需要显式地释放资源,降低了出错的概率。
实现方式
构造函数:在对象创建时,负责分配所需的资源。例如,在构造函数中打开一个文件或分配一块内存。
析构函数:在对象销毁时,负责释放该对象占用的资源。当对象的生命周期结束时,析构函数会自动执行,释放资源。
RAII 的优势
自动资源管理:RAII 自动处理资源的释放,不需要显式调用资源释放代码,减少了出错的可能性(如忘记释放资源)。
异常安全:RAII 能够保证即使程序中发生异常,资源也会被正确释放。例如,在 try 块中的对象被销毁时,析构函数会自动释放资源,从而避免资源泄漏。
简洁性和易维护性:使用 RAII 模式可以使资源管理代码更加简洁和模块化,减少了繁琐的手动管理。
防止内存泄漏:通过将资源与对象的生命周期绑定,可以有效防止内存泄漏、悬挂指针等问题。
RAII 的缺点
不能自由控制资源释放的时机:在 RAII 模式中,资源的释放依赖于对象的生命周期,无法显式控制资源的释放时机。如果需要在对象销毁之前释放资源,RAII 可能不适用。
资源生命周期绑定问题:RAII 通过对象生命周期管理资源,这对于某些类型的资源可能不适用。例如,某些外部资源(如数据库连接)可能需要在特定时刻关闭,而不仅仅是在对象销毁时。
RAII 的应用场景
内存管理:例如,unique_ptr 和 shared_ptr 是 C++ 中的智能指针,它们的实现就是基于 RAII 模式,自动管理内存资源。
文件操作:如上文所示,RAII 可以用于文件的打开和关闭,确保即使发生异常,文件资源也会被自动释放。
数据库连接:RAII 可用于数据库连接的管理,确保连接在对象生命周期结束时被自动关闭。
线程锁管理:通过 RAII 模式,锁的获取和释放可以自动管理,避免忘记释放锁导致死锁。
智能指针
智能指针(Smart Pointer)是现代 C++ 中用于自动管理动态内存的一种工具,它通过封装原始指针 ,提供对内存资源的自动管理,帮助避免常见的内存管理错误,如内存泄漏和悬挂指针。
智能指针实际上是一个类,它重载了指针操作符(如 * 和 ->),使得使用智能指针的代码和普通指针一样简便,但它能自动处理资源的释放。
C++标准库中的智能指针都在 <memory> 这个头文件下,智能指针主要有 auto_ptr、unique_ptr、shared_ptr 和 weak_ptr 等。
auto_ptr
auto_ptr 是 C++98 标准中引入的一个智能指针类型,通过自动释放资源来避免内存泄漏和悬挂指针的问题 。
微信扫一扫,关注极客日志 微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具 Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
HTML转Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
JSON 压缩 通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
JSON美化和格式化 将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online
1. auto_ptr 的缺陷
auto_ptr 的设计存在巨大缺陷,在涉及资源所有权转移时(拷贝或者赋值时) ,原 auto_ptr 不再拥有资源,资源的所有权转移给 目标 auto_ptr,这导致了 原 auto_ptr 变成一个悬挂指针(类似于空指针)。
struct Date {
int _year;
int _month;
int _day;
Date (int year = 2000 , int month = 1 , int day = 1 ) : _year(year), _month(month), _day(day) {}
~Date () { cout << "~Date" << endl; }
};
int main () {
auto_ptr<Date> ap1 (new Date()) ;
auto_ptr<Date> ap2 (ap1) ;
auto_ptr<Date> ap3;
ap3 = ap2;
return 0 ;
}
auto_ptr 的设计存在缺陷,在涉及资源所有权转移时,其行为会造成意外的错误,auto_ptr 在 C++11 中被废弃,不推荐使用!
2. auto_ptr 的模拟实现 auto_ptr 的模拟实现比较简单,在涉及资源的转移时,将原指针置空即可。
template <class T >
class auto_ptr {
public :
auto_ptr (T* ptr = nullptr ) : _ptr(ptr) {}
auto_ptr (auto_ptr<T>& ap) : _ptr(ap._ptr) { ap._ptr = nullptr ; }
auto_ptr<T>& operator =(auto_ptr<T>& ap) {
if (_ptr != ap._ptr) {
if (_ptr) delete _ptr;
_ptr = ap._ptr;
ap._ptr = nullptr ;
}
return *this ;
}
~auto_ptr () {
if (_ptr) delete _ptr;
_ptr = nullptr ;
}
T& operator *() { return *_ptr; }
T* operator ->() { return _ptr; }
private :
T* _ptr;
};
unique_ptr unique_ptr 是独占式的智能指针,表示指向一个动态分配的对象的唯一所有者。该指针不支持拷贝和赋值,但支持移动构造或者赋值。
当一个资源只能有一个拥有者时,使用 unique_ptr 是最合适的选择。
unique_ptr<Date> up1 (new Date()) ;
unique_ptr<Date> up2 (move(up1)) ;
unique_ptr<Date> up3;
up3 = move (up2);
1. make_unique make_unique 是 C++11/14 标准库中引入的一个函数模板,用于创建动态分配的对象并返回一个 unique_ptr,从而安全高效地管理对象的生命周期。
template <typename T, typename ... Args>
std::unique_ptr<T> make_unique (Args&&... args) ;
避免手动调用 new 和 delete :使用 make_unique 能够简化动态内存分配,避免使用裸指针容易产生的内存泄漏或未定义行为。
性能优化 :它能够一次性分配对象和控制块所需的内存,减少额外开销。
强异常安全性 :使用 make_unique 时,不会因为对象构造和分配的中间异常而泄漏内存。
auto up1 = make_unique <int >(20 );
unique_ptr<int > up2 (new int (10 )) ;
2. 定制删除器 unique_ptr 在释放资源时,默认是 delete _ptr,如果指向的资源是 new type[num] 而来的,默认释放资源的方式就不适合了,需要 delete[] 的方式是释放资源,这时我们需要定制删除器。
new [] 的方式经常使用,库里已经有了特化版本,而对于定制删除器,仿函数、函数指针、lambda 表达式 都可作为删除器。
不过要注意的是传定制删除器给 unique_ptr,是传给模板参数,其构造参数也要传。
unique_ptr<Date[]> up1 (new Date[5 ]) ;
unique_ptr<Date, DeleteArray<Date>> up2 (new Date[5 ]);
unique_ptr<Date, void (*) (Date*) > up3 (new Date[5 ], DeleteArrayFunc<Date>) ;
auto delArr = [](Date* ptr){ delete [] ptr; };
unique_ptr<Date, decltype (delArr) > up4 (new Date[5 ], delArr) ;
简单说一下定制删除器的底层,将定制删除器的类型传过去 ,利用其类型创建删除器对象并用传给构造参数的具体定制删除器对象来初始化 ,这样底层就有了外层传进来的定制删除器 ,然后利用删除器释放资源 。
3. unique_ptr 的模拟实现 unique_ptr 的模拟实现也比较简单,将其构造函数 和赋值重载函数 delete 即可。
template <class T >
class unique_ptr {
public :
explicit unique_ptr (T* ptr) : _ptr(ptr) { }
~unique_ptr () {
if (_ptr) delete _ptr;
_ptr = nullptr ;
}
unique_ptr (const unique_ptr<T>& up) = delete ;
unique_ptr<T>& operator =(const unique_ptr<T>& up) = delete ;
unique_ptr (unique_ptr<T>&& up) : _ptr(up._ptr) { up._ptr = nullptr ; }
unique_ptr<T>& operator =(unique_ptr<T>&& up) {
delete _ptr;
_ptr = up._ptr;
up._ptr = nullptr ;
return *this ;
}
private :
T* _ptr;
};
关键字 explicit 的作用是修饰构造函数,防止隐式类型转换 。
Date* ptr = new Date ();
unique_ptr<Date> up1 (ptr) ;
使用 explicit 修饰单参数构造函数可以提高代码的可读性,减少维护负担。
shared_ptr shared_ptr 是 C++11 标准引入的一种智能指针,用于管理动态分配的对象,并允许多个 shared_ptr 实例共享同一对象的所有权。shared_ptr 使用引用计数来追踪有多少个 shared_ptr 对象共享资源,并在最后一个 shared_ptr 被销毁时自动释放资源。这种机制确保了内存管理的安全性,避免了内存泄漏,同时允许多个对象共享相同的资源。
shared_ptr 是一种 共享所有权 的智能指针,而非独占所有权(像 unique_ptr)。多个 shared_ptr 对象可以共同管理一个动态分配的对象,而不必担心资源的重复释放或遗漏释放。
shared_ptr<Date> sp1 (new Date()) ;
shared_ptr<Date> sp2 (sp1) ;
cout << "Reference count: " << sp1. use_count () << endl;
shared_ptr<Date> sp1(new Date());
sp1 是一个 shared_ptr,它管理一个动态分配的 Date 对象。此时引用计数为 1。
shared_ptr<Date> sp2(sp1);
sp2 是 sp1 的副本,意味着它也指向同一个 Date 对象,引用计数增加到 2。
sp1.use_count() 返回当前有多少个 shared_ptr 管理相同的对象。此时返回 2。
当 sp1 和 sp2 超出作用域时,它们的引用计数都会减少。当引用计数降到 0 时,Date 对象会自动销毁。
make_shared 也是一个函数模板,用于创建共享指针,可以接受任何类型的参数,并返回一个指向该类型对象的共享指针。
template <class T, class ... Args>
shared_ptr<T> make_shared (Args&&... args) ;
make_shared 与直接使用 shared_ptr 的对比
特性 make_shared直接用 shared_ptr 语法简洁性 更简洁 需要手动调用 new 内存分配次数 1 次 2 次(对象和引用计数分别分配) 异常安全性 更安全 容易出现内存泄漏
模拟实现 对于 shared_ptr 的模拟实现,我们首先要考虑的就是引用计数的设计。
因为静态成员变量是整个类共有的,每当指向一个资源,无论是不同的资源还是相同的资源,静态成员变量都会增加,不能做到对于不同的资源都有独立的一份引用计数 。
比如 sp1 和 sp2 指向着资源 1,引用计数是 2,在创建一个 sp3 指向资源 2,由于引用计数是静态成员变量,引用计数就变成 3 了,这显然是错误的,sp3 的引用计数应该是 1.
引用计数的设计应该采用动态开辟的方式,做到每一个不同的资源都有一份独立的引用计数。
template <class T >
class shared_ptr {
public :
explicit shared_ptr (T* ptr = nullptr ) : _ptr(ptr), _pcount(new int(1 )) { }
~shared_ptr () {
if (--(*_pcount) == 0 ) {
delete _ptr;
delete _pcount;
}
}
shared_ptr (const shared_ptr<T>& sp) : _ptr(sp._ptr), _pcount(sp._pcount) {
++(*_pcount);
}
shared_ptr<T>& operator =(const shared_ptr<T>& sp) {
if (_ptr != sp._ptr) {
if (--(*_pcount) == 0 ) {
delete _ptr;
delete _pcount;
}
_ptr = sp._ptr;
_pcount = sp._pcount;
++(*_pcount);
}
return *this ;
}
T& operator *() { return *_ptr; }
T* operator ->() { return _ptr; }
T* get () const { return _ptr; }
int use_count () const { return *_pcount; }
private :
T* _ptr;
int * _pcount;
};
shared_ptr 的成员函数的实现都比较简单,但是赋值重载函数有比较多细节要注意:
赋值操作要保证不是一个指针自己给自己赋值 ,this != &sp 不能完全处理所有情况,因为不同的 shared_ptr 对象的 _ptr 可能是一样的,得用 _ptr != sp._ptr 才可以完全覆盖所有情况。
被赋值的指针的引用计数要先要减 1 ,判断该指针是否是最后一个指向对应资源的指针 ,若是则要释放原来的资源 。
进行赋值操作,完成后引用计数要 +1 ,最后返回 *this。
定制删除器 shared_ptr 也可以传定制删除器,不过相比 unique_ptr 的方式,shared_ptr 传递删除器的方式只需传到构造函数的参数即可 。
template <class U, class D>
shared_ptr (U* p, D del) ;
template <class D>
shared_ptr (nullptr_t p, D del) ;
template <class T >
struct DeleteArray {
void operator () (T* ptr) { delete [] ptr; }
};
template <class T >
void DeleteArrayFunc (T* ptr) { delete [] ptr; }
shared_ptr<int []> sp1 (new int [10 ]) ;
shared_ptr<Date> sp2 (new Date[10 ], DeleteArray<Date>()) ;
shared_ptr<Date> sp3 (new Date[10 ], DeleteArrayFunc<Date>) ;
auto delArr = [](Date* ptr){ delete [] ptr; };
shared_ptr<Date> sp4 (new Date[10 ], delArr) ;
template <class T >
class shared_ptr {
public :
explicit shared_ptr (T* ptr = nullptr ) : _ptr(ptr), _pcount(new int(1 )) { }
template <class D >
shared_ptr (T* ptr, D del) : _ptr(ptr), _pcount(new int (1 )), _del(del) {}
~shared_ptr () {
if (--(*_pcount) == 0 ) {
_del(_ptr);
delete _pcount;
}
}
shared_ptr (const shared_ptr<T>& sp) : _ptr(sp._ptr), _pcount(sp._pcount), _del(sp._del) {
++(*_pcount);
}
shared_ptr<T>& operator =(const shared_ptr<T>& sp) {
if (_ptr != sp._ptr) {
if (--(*_pcount) == 0 ) {
_del(_ptr);
delete _pcount;
}
_ptr = sp._ptr;
_pcount = sp._pcount;
_del = sp._del;
++(*_pcount);
}
return *this ;
}
T& operator *() { return *_ptr; }
T* operator ->() { return _ptr; }
private :
T* _ptr;
int * _pcount;
function<void (T*)> _del = [](T* ptr){ delete ptr; };
};
写多一个构造函数并套一层模板,当传递删除器的时候,调用此函数。
template <class D >
shared_ptr (T* ptr, D del) : _ptr(ptr), _pcount(new int (1 )), _del(del) {}
删除器可以是仿函数、函数指针和 lambda 表达式等 ,我们是没有办法用具体的某个类型去创建 _del 变量 ,但是 C++11 中有一个类模板 function ,它是通用的函数包装器,可以包装仿函数、函数指针和 lambda 表达式 ,而删除器的函数签名都是 void(T* ptr)(返回类型和参数类型)。我们就可以用 function 来创建 _del 变量,并给上 lambda 缺省值 [](T* ptr) {delete ptr; } 。
function<void (T*)> _del = [](T* ptr){ delete ptr; };
循环引用 和 weak_ptr 智能指针 是用来管理动态分配的内存,以避免内存泄漏的问题。然而,如果使用不当,智能指针也会引入一些新的问题,例如循环引用 。
循环引用(Cyclic Reference)是指两个或多个对象互相持有对方的引用 ,形成一个环 ,导致它们无法被释放 ,即使它们已经不再被其他部分使用。
class Node {
public :
shared_ptr<Node> next;
~Node () { cout << "Node destroyed" << endl; }
};
int main () {
auto node1 = make_shared <Node>();
auto node2 = make_shared <Node>();
node1->next = node2;
node2->next = node1;
return 0 ;
}
shared_ptr 的原理:
shared_ptr 通过引用计数来管理对象的生命周期。
当引用计数变为 0 时,shared_ptr 会自动释放内存。
循环引用的本质:
在上述例子中,node1 和 node2 互相持有对方的 shared_ptr,node1 需要 node2 的 shared_ptr 析构时释放,而 node2 的 shared_ptr 是 node2 的成员变量,需要让 node2 释放才会析构,node2 需要 node1 的 shared_ptr 析构时释放,node1 的 shared_ptr 需要让 node1 释放才会析构。这样就形成一个环了,两个节点的 shared_ptr 的引用计数始终不为 0;
即使它们超出了作用域,也不会被销毁,从而引发内存泄漏 。
可以通过将其中一个 shared_ptr 替换为 weak_ptr 来打破循环引用。
class Node {
public :
weak_ptr<Node> next;
~Node () { cout << "Node destroyed" << endl; }
};
int main () {
auto node1 = make_shared <Node>();
auto node2 = make_shared <Node>();
node1->next = node2;
node2->next = node1;
return 0 ;
}
程序结束时,先析构 node2,引用计数减到 0,释放 node2 ,而不会像循环引用那般由于 node2 和 node1->next 指向同一个对象,析构 node2 时其引用计数从 2 减到 1,导致引用计数永远不为 0 导致 node2 无法释放。node2 释放后 node1 也能正常释放了 。
weak_ptr 是一种辅助智能指针,它与 shared_ptr 配合使用,用于解决循环引用问题 或实现对象的非强拥有关系 。
weak_ptr 是一种不参与引用计数的智能指针。
它不会改变所指向对象的生命周期,仅仅是一个'弱引用'。
常用于观察由 shared_ptr 管理的对象,而不会影响其销毁时机。
weak_ptr 必须从 shared_ptr 初始化,不能直接管理动态分配的内存。
通过 weak_ptr 无法直接访问对象,需要调用 lock() 方法将其转换为 shared_ptr。
lock() 方法返回一个指向相同对象的 shared_ptr,如果对象已被释放,则返回一个空指针。
特性 shared_ptrweak_ptr是否参与引用计数 是 否 是否影响生命周期 是 否 访问对象方式 直接使用 * 或 -> 需调用 lock() 转换为 shared_ptr 应用场景 强拥有关系,负责对象生命周期 弱引用,避免循环引用或临时访问
weak_ptr 是一种轻量级的智能指针,用于观察对象,不参与对象生命周期管理。
在设计需要临时引用或防止循环引用的场景中,weak_ptr 是一个非常重要的工具。
配合 shared_ptr 使用,能够更好地管理复杂对象间的依赖关系。