C++智能指针
本文分析了传统C++手动内存管理的问题,重点探讨了智能指针的必要性和实现原理。主要观点包括:1)手动内存管理容易引发内存泄漏,特别是面对异常时;2)内存泄漏会导致程序性能下降甚至崩溃;3)RAII(资源获取即初始化)思想是解决内存管理的有效方法,通过对象生命周期自动管理资源;4)C++提供了auto_ptr、unique_ptr等智能指针实现,其中auto_ptr采用所有权转移但存在缺陷,uniq
文章目录
1. 为什么需要智能指针?
问题分析
在传统C++编程中,手动管理内存容易出现问题:
void Func()
{
// 1. 如果p1这里new抛异常会如何?
// 2. 如果p2这里new抛异常会如何?
// 3. 如果div调用这里又抛异常会如何?
int* p1 = new int;
int* p2 = new int;
cout << div() << endl; // 如果div()抛出异常
delete p1; // 这些delete可能不会执行
delete p2;
}
对于以上代码会存在问题:
第一个new失败:如果new int抛出std::bad_alloc,程序直接终止,没有内存泄漏
第二个new失败:如果第一个new成功,第二个new失败,第一个int的内存泄漏
div()函数异常:如果两个new都成功,但div()抛出异常,两个int的内存都会泄漏
我们可以看到,C++异常安全机制不完善,异常抛出时直接寻找对应catch捕获异常,不会自动释放堆内存。
2. 内存泄漏
2.1 什么是内存泄露
- 内存泄漏不是物理内存消失,而是应用程序分配某段内存后,因设计错误失去对该内存的控制,即申请内存没有释放,或者是系统的其他问题。内存泄漏不是在物理上消失,而是失去了对内存的控制。
- 程序无法再使用该内存,也无法释放它
具体危害:
- 短期程序影响不大,但长期运行的程序(操作系统、服务器、后台服务)影响严重
- 内存泄漏累积导致可用内存越来越少
- 系统响应变慢,频繁交换,最终卡死或崩溃
- 在嵌入式系统等资源受限环境中更为致命
内存泄漏示例
void MemoryLeaks()
{
// 1. 明显的忘记释放
int* p1 = (int*)malloc(sizeof(int));
int* p2 = new int;
FILE* p3 = fopen("log.txt","w");
// 这里直接返回,p1和p2指向的内存泄漏,p3所指的FILE对象也不会释放,造成内存泄露
// 2. 异常安全问题
int* p4 = new int[10];
Func(); // 假设Func()可能抛出异常
delete[] p4; // 如果Func抛出异常,则在上面就会退出函数,这行不会执行
}
2.2 内存泄漏都有哪些类型
堆内存泄漏
我们自己通过malloc/calloc/realloc/new分配的内存,因为忘记释放或者程序执行跳转等原因没有释放(free/delete),会造成内存泄漏,这会导致这块内存无法再次被程序使用。
系统资源泄漏
在操作系统中的一些资源失去控制权,由于我们在调用系统调用的时候出现与堆泄露的类似问题,申请了文件描述符、套接字、管道、数据库连接、互斥锁等类型的数据,没有调用专门的释放函数(close、fclose等),导致系统资源耗尽,甚至影响其他程序。
2.3 避免内存泄漏
-
编码规范
谁申请,谁释放,成对编程:每个new对应delete,每个malloc对应free,但仅靠规范无法解决异常安全问题,因为如果遇到异常,代码的执行流则会跳转,可能无法执行到我们是释放的代码行,因此无法完全解决。
-
RAII思想
资源生命周期与对象绑定,构造时获取资源,析构时自动释放,完美解决因为异常执行流跳转而无法释放内存的问题。当函数结束时,对象需要销毁,就可以顺带进行delet/free释放空间。
智能指针就是具体实现。自动管理内存生命周期,对象生命周期销毁时会自动调用析构函数,即采用RALL思想专门设计的工具。
-
内存管理库
使用自定义内存管理库,内置内存泄漏检测和调试功能
因此,内存泄露非常常见,一般会有2钟解决方案:1.提前预防,比如使用智能指针,2.泄露了查错,如使用内存泄露检测工具。
3. 智能指针的使用及原理
3.1 RAII思想
RAII(Resource Acquisition Is Initialization),直接翻译为 资源获取即初始化,真正意思为获取资源的时候初始化一个对象,这就是RALL思想。
利用RALL的思想,可以做到资源获取即初始化,利用对象的生命周期管理资源
在C++语言中,具体实现为构造函数中获取资源,析构函数中释放资源,资源在对象生命周期内保持有效。我的Linux线程池文章中采用的RALL风格的锁就是一个典型的应用实例。
RALL思想的优点就是:实现资源自动管理,无需手动释放。即使发生异常,在销毁变量的时候也会调用析构自动回收空间。资源的生命周期与对象生命周期是一致的,避免了内存泄露问题。
如果需要使用资源,只需在类中提供相关方法即可。
代码示例:
template<class T>
class SmartPtr {
public:
SmartPtr(T* ptr = nullptr) : _ptr(ptr)
{
// 构造时获取资源
}
~SmartPtr()
{
if(_ptr) {
delete[] _ptr; // 析构时自动释放
_ptr = nullptr;
}
}
private:
T* _ptr;
};
void MemoryLeaks()
{
// 1. 明显的忘记释放
//int* p1 = (int*)malloc(sizeof(int));
//int* p2 = new int;
SmartPtr<int> sp1((int*)malloc(sizeof(int)));
SmartPtr<int> sp2(new int);
// 这里将开辟的资源交给sp1和sp2对象管理,无需手动释放,对象出作用域自动销毁。
// 2. 异常安全问题
//int* p3 = new int[10];
SmartPtr<int> sp3(new int[10]);
Func(); // 假设Func()可能抛出异常
// 如果Func抛出异常,那也需要销毁对象,会自动释放内存。
}
int main()
{
MemoryLeaks();
return 0;
}
3.2 智能指针的原理
智能指针需要具备两个核心特性:
- 管理资源的生命周期
- 自动释放资源
也需要具备和普通指针一样的行为:
- 通过重载运算符模拟指针操作
- operator*:解引用
- operator->:成员访问
3.3 std::auto_ptr(C++98)
C++98提供了智能指针,即为auto_ptr,他除了解决了自动管理生命周期的问题外,还解决了另外一个问题:智能指针的赋值问题。
问题引入:
如果只是单纯的实现上面代码示例中SmartPtr,就会出现赋值问题。
普通指针的赋值,其意为两个指针指向一块空间共同管理。
而我们所写的简易智能指针,再赋值的时候会遇到深浅拷贝问题。首先不能深拷贝,因为我们写这个类的目的是帮助我们自动管理,管理其生命周期,深拷贝会开辟空间,不符合我们的要求。但是浅拷贝也会出现问题,两个指针指向一块空间,但是在析构的时候两个对象都会销毁,会导致重复释放的问题。

而auto_ptr就是解决了这个问题。
核心思想为管理权转移
- 拷贝构造或赋值时,将资源所有权转移给新对象
- 原对象变为空指针
简易的模拟实现:
template <class T>
class auto_ptr {
public:
auto_ptr(T* ptr = nullptr):_ptr(ptr)
{ }
auto_ptr(auto_ptr<T>& a)
{
_ptr = a.relese();
}
T* get()
{
return _ptr;
}
T* relese()
{
T* ret = _ptr;
_ptr = nullptr;
return ret;
}
void reset(T* ptr = nullptr)
{
delete _ptr;
_ptr = ptr;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
auto_ptr<T>& operator= (auto_ptr<T>& p)
{
_ptr = p.relese();
return *this;
}
~auto_ptr()
{
if(_ptr != nullptr)
delete _ptr;
}
private:
T* _ptr;
};
缺陷分析:
std::auto_ptr<int> sp1(new int);
std::auto_ptr<int> sp2(sp1); // 管理权转移
// sp1现在为空指针,访问会导致未定义行为
*sp2 = 10;
cout << *sp2 << endl; // 正常
cout << *sp1 << endl; // 运行时错误!sp1已是空指针
该设计具有明前缺陷,悬空指针问题,因此我们尽量不要使用该指针。
auto_ptr是失败的设计,已被C++11弃用,甚至有些公司明确禁止使用。
3.4 std::unique_ptr
C++11新添的智能指针,唯一型智能指针,不可拷贝,赋值
核心思想:防拷贝
- 独占式所有权,一个资源只能被一个unique_ptr拥有
- 通过删除拷贝构造函数和赋值运算符实现防拷贝
// 默认删除器模板 - 用于普通指针
template<class T>
struct default_delete {
void operator()(T* ptr)
{
delete ptr; // 使用 delete 释放单个对象
}
};
// 默认删除器的特化版本 - 用于数组指针
template<class T>
struct default_delete<T[]> {
void operator()(T* ptr)
{
delete[] ptr; // 使用 delete[] 释放数组
}
};
// 前向声明 unique_ptr 类模板
template <class T, class D = default_delete<T>>
class unique_ptr;
// unique_ptr 主模板 - 管理单个对象的独占所有权
template <class T, class D >
class unique_ptr {
public:
// 构造函数:接受原始指针和删除器(默认构造)
unique_ptr(T* ptr = nullptr, D del = D()) :_ptr(ptr),_del(del)
{}
// 移动构造函数:从右值引用转移所有权
unique_ptr(unique_ptr&& a):_ptr(a._ptr),_del(move(a._del))
{
a._ptr = nullptr; // 将源对象的指针置空
}
// 移动赋值运算符:转移所有权
unique_ptr& operator= (unique_ptr&& a)
{
if (&a != this) // 修正:应该比较地址,原代码 a != *this 不正确
{
reset(a.release()); // 释放当前资源并接管新资源
_del = move(a._del); // 转移删除器
}
return *this;
}
// 禁用拷贝构造函数
unique_ptr(unique_ptr& a) = delete;
// 获取原始指针(不释放所有权)
T* get()
{
return _ptr;
}
// 交换两个 unique_ptr 的内容
void swap(unique_ptr& a)
{
std::swap(_ptr, a._ptr);
std::swap(_del, a._del);
}
// 布尔转换运算符:检查是否持有有效指针
explicit operator bool()const
{
return _ptr != nullptr;
}
// 释放所有权,返回原始指针(调用者负责管理)
T* release()
{
T* ret = _ptr;
_ptr = nullptr;
return ret;
}
// 重置指针:先删除当前对象,再接管新指针
void reset(T* ptr = nullptr)
{
_del(_ptr); // 使用删除器释放当前资源
_ptr = ptr; // 接管新指针
}
// 解引用运算符:访问指向的对象
T& operator*() const
{
return *_ptr;
}
// 箭头运算符:通过指针访问成员
T* operator->() const
{
return _ptr;
}
// 下标运算符:用于数组访问
T& operator[](int pos)const
{
return _ptr[pos];
}
// 禁用拷贝赋值运算符
unique_ptr& operator= (unique_ptr& p) = delete;
// 析构函数:自动释放管理的资源
~unique_ptr()
{
if (_ptr != nullptr)
_del(_ptr); // 使用删除器释放资源
}
private:
T* _ptr; // 管理的原始指针
D _del; // 删除器对象
};
// unique_ptr 的特化版本 - 用于管理动态数组
template <class T, class D>
class unique_ptr<T[], D> {
public:
// 构造函数:接受数组指针和删除器
unique_ptr(T* ptr, D del = D() ) :_ptr(ptr), _del(del)
{
}
// 移动构造函数:转移数组所有权
unique_ptr(unique_ptr&& a) :_ptr(a._ptr), _del(move(a._del))
{
a._ptr = nullptr; // 将源对象指针置空
}
// 禁用拷贝构造函数
unique_ptr(unique_ptr& a) = delete;
// 获取原始指针(不释放所有权)
T* get() const
{
return _ptr;
}
// 布尔转换运算符:检查是否持有有效指针
explicit operator bool()const
{
return _ptr != nullptr;
}
// 移动赋值运算符:转移数组所有权
unique_ptr& operator= (unique_ptr&& a) {
if (&a != this) // 修正:应该比较地址
{
reset(a.release()); // 释放当前数组并接管新数组
_del = move(a._del); // 转移删除器
}
return *this; // 注意:原代码缺少返回值
}
// 交换两个 unique_ptr 的内容
void swap(unique_ptr& a)
{
std::swap(_ptr, a._ptr); // 修正:应该使用 std::swap
std::swap(_del, a._del); // 修正:应该使用 std::swap
}
// 释放数组所有权,返回原始指针
T* release()
{
T* ret = _ptr;
_ptr = nullptr;
return ret;
}
// 重置数组指针:先删除当前数组,再接管新数组
void reset(T* ptr = nullptr)
{
if(_ptr != nullptr)
_del(_ptr); // 使用删除器释放当前数组
_ptr = ptr; // 接管新数组指针
}
// 下标运算符:访问数组元素
T& operator[](int pos)const
{
return _ptr[pos];
}
// 禁用拷贝赋值运算符
unique_ptr& operator= (unique_ptr& p) = delete;
// 析构函数:自动释放管理的数组
~unique_ptr()
{
if(_ptr != nullptr)
_del(_ptr); // 使用删除器释放数组
}
private:
T* _ptr; // 管理的数组指针
D _del; // 删除器对象
};
使用特点:
- 轻量级,几乎无额外开销
- 编译期防拷贝,安全可靠
- 支持移动语义(C++11)
3.5 std::shared_ptr
C++11新添智能指针,支持赋值,拷贝
设计思想:引用计数
- 多个shared_ptr可以共享同一个资源
- 通过引用计数跟踪资源被多少个shared_ptr共享
- 当最后一个shared_ptr销毁时释放资源
template <class T>
class shared_ptr {
public:
// 默认构造函数:接受原始指针,初始化引用计数为1,使用默认删除器
shared_ptr(T* ptr = nullptr)
:_ptr(ptr),
_size(new int{ 1 }),
_del([](T* ptr) {delete ptr; }),
_mutex(new std::mutex)
{
}
// 带自定义删除器的构造函数
template< class D >
shared_ptr(T* ptr, D del)
:_ptr(ptr),
_del(del),
_size(new int{1}),
_mutex(new std::mutex)
{}
// 增加引用计数(线程安全)
void addref()
{
std::lock_guard<std::mutex> lock(*_mutex);
(*_size)++;
}
// 减少引用计数,如果为0则释放资源(线程安全)
void release()
{
std::lock_guard<std::mutex> lock(*_mutex);
if ( _ptr && --(*_size) == 0 )
{
_del(_ptr);
delete _size;
}
}
// 拷贝构造函数:共享所有权,增加引用计数
shared_ptr(shared_ptr& a)
:_ptr(a._ptr),
_size(a._size),
_del(a._del),
_mutex(a._mutex)
{
addref();
}
// 移动构造函数:转移所有权,不增加引用计数
shared_ptr(shared_ptr&& a) noexcept
{
swap(a);
}
// 布尔转换运算符:检查是否持有有效指针
explicit operator bool()const
{
return _ptr != nullptr;
}
// 获取原始指针(不释放所有权)
T* get()
{
return _ptr;
}
// 重置指针:释放当前对象,接管新指针
void reset(T* ptr = nullptr)
{
*this = shared_ptr<T>(ptr); // 创建临时对象并移动赋值
}
// 解引用运算符:访问指向的对象
T& operator*()
{
return *_ptr;
}
// 箭头运算符:通过指针访问成员
T* operator->()
{
return _ptr;
}
// 交换两个 shared_ptr 的内容
void swap(shared_ptr& p)
{
std::swap(_ptr, p._ptr);
std::swap(_size, p._size);
std::swap(_del, p._del);
std::swap(_mutex, p._mutex);
}
// 检查是否是唯一所有者
bool unique() const
{
std::lock_guard<std::mutex> lock(*_mutex);
return *_size == 1;
}
// 下标运算符:用于数组访问
T& operator[](int pos)
{
return _ptr[pos];
}
// 获取当前引用计数
int use_count() const
{
std::lock_guard<std::mutex> lock(*_mutex);
return *_size;
}
// 拷贝赋值运算符:共享所有权
shared_ptr& operator= (shared_ptr& p)
{
if (this != &p) // 防止自赋值
{
release(); // 释放当前资源
_ptr = p._ptr;
_size = p._size;
_del = p._del;
_mutex = p._mutex;
addref(); // 增加新资源的引用计数
}
return *this;
}
// 移动赋值运算符:转移所有权
shared_ptr& operator= (shared_ptr&& p)
{
if (this != &p) // 防止自赋值
{
swap(p); // 交换资源,原资源会在p析构时释放
}
return *this;
}
// 析构函数:减少引用计数,必要时释放资源
~shared_ptr()
{
release();
}
private:
T* _ptr; // 管理的原始指针
int* _size; // 引用计数指针
std::function<void(T*)> _del; // 删除器函数对象
std::mutex* _mutex; // 互斥锁指针,用于线程安全
};
引用计数原理:
初始状态:shared_ptr sp1(new int)
引用计数:1拷贝构造:shared_ptr sp2(sp1)
引用计数:2sp1析构:引用计数减为1
sp2析构:引用计数减为0,释放资源
3.6 线程安全问题详解
两个方面:
- 引用计数操作:线程安全(通过加锁保护)
- 指向的资源访问:非线程安全(需要用户自己同步)
struct Date {
int _year = 0;
int _month = 0;
int _day = 0;
};
void SharePtrFunc(bit::shared_ptr<Date>& sp, size_t n, mutex& mtx) {
for (size_t i = 0; i < n; ++i) {
// 智能指针拷贝:引用计数++(线程安全)
bit::shared_ptr<Date> copy(sp);
// 访问资源:需要用户自己加锁(非线程安全)
{
unique_lock<mutex> lk(mtx);
copy->_year++;
copy->_month++;
copy->_day++;
}
}
}
3.7 循环引用问题及解决方案
类似于死锁,互相拿着对方的释放条件
问题描述:
struct ListNode {
int _data;
shared_ptr<ListNode> _prev;
shared_ptr<ListNode> _next;
~ListNode() { cout << "~ListNode()" << endl; }
};
int main() {
shared_ptr<ListNode> node1(new ListNode);
shared_ptr<ListNode> node2(new ListNode);
node1->_next = node2; // node2引用计数+1
node2->_prev = node1; // node1引用计数+1
// 退出时:
// node1析构:引用计数从2减为1(因为node2->_prev还指向它)
// node2析构:引用计数从2减为1(因为node1->_next还指向它)
// 两者都无法释放,内存泄漏!
}
正常当没有循环引用时,即只有单向的指向:
图片字有点小,可以点击放大观看

在两个指针生命周期结束,资源释放过程如下:


当node1->_next = node2,并且node2-> _ next = node1,此时就会有循环引用
循环引用示意图:


循环引用会导致他们各自的引用计数都是2,在释放的时候都减1不为0不会释放资源,造成内存泄露,此时只有一方的引用计数将为0,才可释放,但是互相引用,就类似于死锁,互相拿着对方的条件,无法推进。
解决方案:std::weak_ptr
不支持RALL,专门辅助shared_ptr来破解循环引用
本质是赋值或者拷贝时,只指向资源,不增加shared_ptr的引用计数
struct ListNode {
int _data;
weak_ptr<ListNode> _prev; // 使用weak_ptr打破循环
weak_ptr<ListNode> _next;
~ListNode() { cout << "~ListNode()" << endl; }
};
weak_ptr特点:
- 不增加引用计数
- 不控制对象生命周期
- 可以通过lock()获取shared_ptr临时访问资源
3.8 自定义删除器
- 非new分配的内存(malloc、fopen等)
- 数组对象(需要delete[])
- 需要特殊清理逻辑的资源
// 仿函数删除器
template<class T>
struct FreeFunc {
void operator()(T* ptr) {
cout << "free:" << ptr << endl;
free(ptr);
}
};
template<class T>
struct DeleteArrayFunc {
void operator()(T* ptr) {
cout << "delete[]" << ptr << endl;
delete[] ptr;
}
};
int main() {
// 使用自定义删除器
FreeFunc<int> freeFunc;
std::shared_ptr<int> sp1((int*)malloc(4), freeFunc);
DeleteArrayFunc<int> deleteArrayFunc;
std::shared_ptr<int> sp2(new int[10], deleteArrayFunc);
// lambda表达式删除器
std::shared_ptr<FILE> sp3(fopen("test.txt", "w"),
[](FILE* p) { fclose(p); });
std::shared_ptr<int> sp4(new int[10], [](int* p) { delete[] p; });
}
更多推荐



所有评论(0)