1. 智能指针的使用场景分析
以下程序示例中,虽然我们在 new
操作后进行了 delete
处理,但由于异常抛出导致后续的 delete
未能执行,从而引发内存泄漏。理论上,我们可以在 new
操作后捕获异常,并在异常处理中执行 delete
后再重新抛出异常。然而,new
操作本身可能抛出异常,且连续多个 new
操作和后续的 Divide
函数调用都可能产生异常,这使得异常处理变得异常复杂。在这种情况下,使用智能指针可以显著简化问题的处理流程。
double Divide(int a, int b)
{
// 当b == 0时抛出异常
if (b == 0)
{
throw "Divide by zero condition!";
}
else
{
return (double)a / (double)b;
}
}
void Func()
{
// 这里可以看到如果发生除0错误抛出异常,另外下面的array1和array2没有得到释放。
// 所以这里捕获异常后并不处理异常,异常还是交给外面处理,这里捕获了再重新抛出去。
// 但是如果array2new的时候抛异常呢,就还需要套一层捕获释放逻辑,这里更好解决方案
// 是智能指针,否则代码太挫了
int* array1 = new int[10];
int* array2 = new int[10]; // 抛异常呢
try
{
int len, time;
cin >> len >> time;
cout << Divide(len, time) << endl;
}
catch (...)
{
cout << "delete []" << array1 << endl;
cout << "delete []" << array2 << endl;
delete[] array1;
delete[] array2;
throw; // 异常重新抛出,捕获到什么抛出什么
}
// ...
cout << "delete []" << array1 << endl;
delete[] array1;
cout << "delete []" << array2 << endl;
delete[] array2;
}
int main()
{
try
{
Func();
}
catch (const char* errmsg)
{
cout << errmsg << endl;
}
catch (const exception& e)
{
cout << e.what() << endl;
}
catch (...)
{
cout << "未知异常" << endl;
}
return 0;
}
运行结果:
但是如果array2new的时候抛异常呢,就还需要套一层捕获释放逻辑,这里更好解决方案是智能指针,否则代码太挫了
2. RAII和智能指针的设计思路
• RAII(Resource Acquisition Is Initialization,资源获取即初始化)是C++中一种重要的资源管理范式。它的核心思想是将资源的生命周期与对象的生命周期绑定,通过对象的构造和析构来管理资源的获取和释放。具体实现包含三个关键步骤:
- 在构造函数中获取资源(如new分配内存、fopen打开文件、lock获取锁等)
- 在对象生命周期内保持资源可用
- 在析构函数中自动释放资源(如delete释放内存、fclose关闭文件、unlock释放锁等)
这种机制确保了即使在异常发生时,资源也能被正确释放。例如:
class FileHandle {
public:
FileHandle(const char* filename) {
file = fopen(filename, "r");
}
~FileHandle() {
if(file) fclose(file);
}
private:
FILE* file;
};
• 智能指针是RAII思想的典型应用,它不仅管理资源生命周期,还提供便捷的访问接口。常见的智能指针类型包括:
- unique_ptr:独占所有权,不可复制但可移动
- shared_ptr:共享所有权,使用引用计数
- weak_ptr:不增加引用计数,解决循环引用问题
智能指针通过运算符重载提供自然的使用方式:
std::unique_ptr<Object> ptr(new Object);
(*ptr).method(); // 重载operator*
ptr->method(); // 重载operator->
ptr[0].method(); // 重载operator[] (仅适用于数组)
这种设计既保证了资源安全,又保持了原生指针的使用体验,是现代C++开发中的重要工具。在实际应用中,智能指针可以显著减少内存泄漏和资源管理错误,特别是在多线程环境和复杂对象关系中。
template<class T>
class SmartPtr
{
public:
// RAII
SmartPtr(T* ptr)
:_ptr(ptr)
{}
~SmartPtr()
{
cout << "delete[] " << _ptr << endl;
delete[] _ptr;
}
// 重载运算符,模拟指针的行为,方便访问资源
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T& operator[](size_t i)
{
return _ptr[i];
}
private:
T* _ptr;
};
double Divide(int a, int b)
{
// 当b == 0时抛出异常
if (b == 0)
{
throw "Divide by zero condition!";
}
else
{
return (double)a / (double)b;
}
}
void Func()
{
// 这里使用RAII的智能指针类管理new出来的数组以后,程序简单多了
SmartPtr<int> sp1 = new int[10];
SmartPtr<int> sp2 = new int[10];
for (size_t i = 0; i < 10; i++)
{
sp1[i] = sp2[i] = i;
}
int len, time;
cin >> len >> time;
cout << Divide(len, time) << endl;
}
int main()
{
try
{
Func();
}
catch (const char* errmsg)
{
cout << errmsg << endl;
}
catch (const exception& e)
{
cout << e.what() << endl;
}
catch (...)
{
cout << "未知异常" << endl;
}
return 0;
}
运行结果:
代码中将申请到的内存空间交给了SmartPtr对象进行管理,这样即使代码抛异常了,我们申请的资源也能让SmartPtr对象帮我们释放,避免了内存泄漏
3. C++标准库智能指针的使用
• C++标准库中的智能指针均定义在<memory>头文件中,包含该头文件即可使用。智能指针家族包括auto_ptr(C++98)、unique_ptr、shared_ptr和weak_ptr(C++11)等类型。除weak_ptr外都严格遵循RAII(Resource Acquisition Is Initialization)原则,即资源获取即初始化,通过构造函数获取资源,析构函数释放资源。所有智能指针都支持指针式访问,通过operator*和operator->来访问资源。它们的主要区别在于处理拷贝行为的方式不同,这是选择使用哪种智能指针的关键考量因素。
• auto_ptr是C++98引入的最初版本智能指针,其特点是拷贝时会转移资源管理权给拷贝对象。这种行为是通过拷贝构造函数和赋值运算符实现的,转移后原auto_ptr变为nullptr。这种设计存在严重缺陷,会导致被拷贝对象变为悬空指针,进而引发访问错误。例如:
auto_ptr<int> ap1(new int(10));
auto_ptr<int> ap2 = ap1; // 所有权转移
*ap1; // 运行时错误,ap1已为空
C++11推出新智能指针后,强烈建议避免使用auto_ptr。事实上在C++11之前,许多公司(如Google、Microsoft)就已明文禁止使用该类型,并在编码规范中将其列为禁用项。
• unique_ptr是C++11引入的智能指针,其名称意为"唯一指针"。特点是禁止拷贝操作(拷贝构造函数和赋值运算符被删除),仅支持移动语义(通过std::move转移所有权)。这种设计确保了资源所有权的唯一性,避免了auto_ptr的问题。在不需要拷贝的场景中,推荐优先使用unique_ptr,因为它的开销比shared_ptr更小。典型使用场景包括:
unique_ptr<File> file(new File("test.txt"));
// 转移所有权
unique_ptr<File> file2 = std::move(file);
• shared_ptr同样是C++11引入的智能指针,名为"共享指针"。它支持拷贝和移动操作,适用于需要共享所有权的场景。其内部采用引用计数机制实现资源管理,每拷贝一次引用计数加1,析构时引用计数减1,当计数为0时释放资源。shared_ptr是线程安全的,但仅针对控制块(引用计数)的操作,不保证所管理资源的线程安全。典型使用场景:
shared_ptr<Connection> conn(new Connection);
shared_ptr<Connection> conn2 = conn; // 引用计数+1
• weak_ptr是C++11提供的特殊智能指针,称为"弱指针"。与其他类型不同,它不遵循RAII原则,不能直接管理资源。weak_ptr主要用于解决shared_ptr循环引用导致的内存泄漏问题。它不增加引用计数,需要通过lock()方法获取可用的shared_ptr。典型使用场景:
class Node {
shared_ptr<Node> next;
weak_ptr<Node> prev; // 避免循环引用
};
• 智能指针默认在析构时调用delete释放资源。这意味着若将非new分配的资源交给智能指针管理,析构时会导致程序崩溃。例如:
int x;
unique_ptr<int> up(&x); // 错误,会尝试delete栈变量
智能指针支持在构造时指定删除器——一个可调用对象,用于自定义资源释放方式。例如:
void fileDeleter(FILE* fp) { fclose(fp); }
unique_ptr<FILE, decltype(&fileDeleter)> fp(fopen("a.txt","r"), fileDeleter);
为简化new[]操作,unique_ptr和shared_ptr都提供了特化版本:
unique_ptr<Date[]> up1(new Date[5]); // 会调用delete[]
shared_ptr<Date[]> sp1(new Date[5]); // C++17支持
•
template <class T, class... Args> shared_ptr<T> make_shared(Args&&... args);
make_shared函数是创建shared_ptr的推荐方式,它比直接new更高效,因为能一次性分配控制块和对象所需内存。使用示例:
auto sp = make_shared<Widget>(10, "hello"); // 相当于new Widget(10, "hello")
• shared_ptr支持两种构造方式:通过资源指针构造,或使用make_shared函数直接初始化资源对象。前者需要两次内存分配(对象和控制块),后者只需一次。但某些情况下必须使用前者,例如:
// 需要自定义删除器时
shared_ptr<FILE> sp(fopen("a.txt","r"), fclose);
• shared_ptr和unique_ptr都重载了operator bool,可将智能指针对象直接用于if条件判断:空指针返回false,非空返回true。例如:
if (sp) { // 等价于sp.get() != nullptr
// 使用sp
}
• shared_ptr和unique_ptr的构造函数均使用explicit修饰,防止普通指针隐式转换为智能指针对象。这意味着必须显式构造智能指针:
void foo(shared_ptr<int> sp);
int* p = new int;
foo(p); // 错误,不能隐式转换
foo(shared_ptr<int>(p)); // 正确
具体示例如下:
下面统一使用Date对象来演示,方便析构函数打印查看
struct Date
{
int _year;
int _month;
int _day;
Date(int year = 1, int month = 1, int day = 1)
:_year(year)
,_month(month)
, _day(day)
{}
~Date()
{
cout << "~Date()" << endl;
}
};
auto_ptr:
auto_ptr<Date> ap1(new Date);
// 拷贝时,管理权限转移,被拷贝对象ap1悬空
auto_ptr<Date> ap2(ap1);
// 空指针访问,ap1对象已经悬空
//ap1->_year++;
通过调试和运行结果可以看出,拷贝时,管理权限发生了转移,并且ap1对象也被悬空,容易引发空指针访问
unique_ptr:
unique_ptr<Date> up1(new Date);
// 不支持拷贝
//unique_ptr<Date> up2(up1);
// 支持移动,但是移动后up1也悬空,所以使用移动要谨慎
unique_ptr<Date> up3(move(up1));
使用拷贝会编译报错
但是支持移动,但是移动后,原来的资源是会被“窃取”的,所以也会产生悬空,但是这里我们自己move的时候心里是有数的
运行结果:
调用一次析构函数
shared_ptr:
shared_ptr<Date> sp1(new Date);
// 支持拷贝
shared_ptr<Date> sp2(sp1);
shared_ptr<Date> sp3(sp2);
cout << sp1.use_count() << endl;
sp1->_year++;
cout << sp1->_year << endl;
cout << sp2->_year << endl;
cout << sp3->_year << endl;
// 支持移动,但是移动后sp1也悬空,所以使用移动要谨慎
shared_ptr<Date> sp4(move(sp1));
注意:use_count成员函数是用来获取当前对象管理的资源所对应的引用计数
运行结果:
定制删除器:
智能指针默认在析构时调用delete释放资源。这意味着若将非new分配的资源(如malloc分配的内存或文件描述符)交给智能指针管理,析构时会导致程序崩溃。智能指针支持在构造时指定删除器——一个可调用对象,用于自定义资源释放方式。例如:
1. 问题背景:默认删除器不适用于数组
// 默认删除器使用delete,但数组需要delete[]
unique_ptr<Date> up1(new Date[10]); // 程序崩溃
shared_ptr<Date> sp1(new Date[10]); // 程序崩溃
问题分析:
智能指针默认使用
delete
释放资源但数组需要使用
delete[]
释放类型不匹配导致未定义行为(通常是崩溃)
2. 解决方案1:使用特化版本
// unique_ptr和shared_ptr提供的数组特化版本
unique_ptr<Date[]> up1(new Date[5]); // 使用delete[]
shared_ptr<Date[]> sp1(new Date[5]); // 使用delete[]
特点:
标准库内置的数组特化
析构时自动使用
delete[]
最简洁的数组管理方案
shared_ptr
的数组特化需要C++17支持
3. 解决方案2:自定义删除器
3.1 定义删除器(三种方式)
// 方式1:函数模板
template<class T>
void DeleteArrayFunc(T* ptr) {
delete[] ptr;
}
// 方式2:仿函数模板
template<class T>
class DeleteArray {
public:
void operator()(T* ptr) {
delete[] ptr;
}
};
// 方式3:特定资源仿函数
class Fclose {
public:
void operator()(FILE* ptr) {
cout << "fclose:" << ptr << endl;
fclose(ptr);
}
};
3.2 使用仿函数删除器
// unique_ptr:模板参数指定删除器类型
unique_ptr<Date, DeleteArray<Date>> up2(new Date[5]);
// shared_ptr:构造函数参数传递删除器实例
shared_ptr<Date> sp2(new Date[5], DeleteArray<Date>());
关键区别:
unique_ptr
的删除器是类型的一部分(模板参数)shared_ptr
的删除器是运行时绑定(构造参数)仿函数对象可以直接用于
unique_ptr
(无需构造时传递)
3.3 使用函数指针删除器
// unique_ptr:需指定函数指针类型
unique_ptr<Date, void(*)(Date*)> up3(new Date[5], DeleteArrayFunc<Date>);
// shared_ptr:直接传递函数指针
shared_ptr<Date> sp3(new Date[5], DeleteArrayFunc<Date>);
注意事项:
函数指针类型必须精确匹配
unique_ptr
需要显式声明函数指针类型shared_ptr
自动推导函数指针类型
3.4 使用Lambda删除器
// 定义lambda删除器
auto delArrOBJ = [](Date* ptr) { delete[] ptr; };
// unique_ptr:使用decltype推导lambda类型
unique_ptr<Date, decltype(delArrOBJ)> up4(new Date[5], delArrOBJ);
// shared_ptr:直接传递lambda对象
shared_ptr<Date> sp4(new Date[5], delArrOBJ);
Lambda优势:
语法简洁,就地定义
适合一次性使用的删除逻辑
需要捕获变量时可使用捕获列表
4. 管理非内存资源
// 使用仿函数管理文件
shared_ptr<FILE> sp5(fopen("Test.cpp", "r"), Fclose());
// 使用lambda管理文件
shared_ptr<FILE> sp6(fopen("Test.cpp", "r"), [](FILE* ptr) {
cout << "fclose:" << ptr << endl;
fclose(ptr);
});
应用场景:
文件句柄(
fclose
)网络套接字(
closesocket
)数据库连接(
disconnect
)任何需要自动释放的资源
关键差异总结
特性 | unique_ptr |
shared_ptr |
---|---|---|
删除器位置 | 模板参数(编译时绑定) | 构造参数(运行时绑定) |
类型影响 | 影响智能指针类型 | 不影响智能指针类型 |
仿函数使用 | 可不传实例(直接使用默认构造) | 必须传递实例 |
函数指针使用 | 需显式指定指针类型 | 自动类型推导 |
Lambda使用 | 需用decltype 推导类型 |
直接传递lambda对象 |
性能 | 无额外开销(可能编译期优化) | 有运行时开销(类型擦除) |
最佳实践建议
优先使用特化版本:
unique_ptr<Date[]> arr(new Date[5]); // 首选方案
资源类型匹配原则:
动态数组 → 数组删除器
单个对象 → 默认删除器
文件/网络等 → 定制删除器
工厂函数推荐:
auto sp = make_shared<Date>(...); // 单个对象 auto up = make_unique<Date[]>(5); // C++20起支持数组
删除器实现选择:
通用资源 → 模板仿函数
特定资源 → 专用仿函数/Lambda
简单逻辑 → Lambda表达式
unique_ptr
删除器技巧:// 使用类型别名简化复杂声明 using FilePtr = unique_ptr<FILE, decltype([](FILE* f){ fclose(f); })>; FilePtr fp(fopen("a.txt", "r"));
由于Lambda 表达式本质上是一个匿名函数对象,没有显式类型,所以这里需要用到decltype来推导
decltype:
作用:获取 lambda 表达式的类型
必要性:每个 lambda 表达式都有唯一的匿名类型,必须使用
decltype
获取优势:不需要显式写出复杂的类型名称
注意:
decltype
中的 lambda 仅用于类型推导,不会被执行
以上示例完整展示了智能指针删除器的各种用法,并且强调了unique_ptr
和shared_ptr
在删除器实现上的重要差异,以及如何优雅地管理各种类型的资源。
make_shared,operator bool和explicit:
shared_ptr支持两种构造方式:通过资源指针构造,或使用make_shared函数直接初始化资源对象。前者需要两次内存分配(对象和控制块),后者只需一次。
shared_ptr和unique_ptr都重载了operator bool,可将智能指针对象直接用于if条件判断:空指针返回false,非空返回true。
shared_ptr<Date> sp1(new Date(2024, 9, 11));
shared_ptr<Date> sp2 = make_shared<Date>(2024, 9, 11);
shared_ptr<Date> sp4;
// if (sp1.operator bool())
if (sp1)
cout << "sp1 is not nullptr" << endl;
if (!sp4)
cout << "sp1 is nullptr" << endl;
// 报错,本质是通过隐式类型转换构造,shared_ptr和unique_ptr的构造函数均使用explicit修饰,
// 防止普通指针隐式转换为智能指针对象。这意味着必须显式构造智能指针
//shared_ptr<Date> sp5 = new Date(2024, 9, 11);
//unique_ptr<Date> sp6 = new Date(2024, 9, 11);
运行结果:
4. 智能指针的原理
• 下面我们模拟实现了auto_ptr和unique_ptr的核心功能,这两个智能指针的实现相对简单,主要目的是帮助理解原理。auto_ptr的实现思路是在拷贝时将资源管理权转移给被拷贝对象,但这种设计存在缺陷,已不被推荐使用。unique_ptr则通过禁止拷贝来实现资源管理。
auto_ptr模拟实现:
namespace RO
{
template<class T>
class auto_ptr
{
public:
auto_ptr() = default;
auto_ptr(T* ptr)
:_ptr(ptr)
{}
auto_ptr(auto_ptr& ap) // 注意:非 const 引用,需要修改ap对象
:_ptr(ap._ptr)
{
ap._ptr = nullptr;
}
auto_ptr& operator=(auto_ptr& ap)
{
// 检测是否为自己给自己赋值
if (this != &ap)
{
if(_ptr) delete _ptr;
// 转移ap的资源
_ptr = ap._ptr;
ap._ptr = nullptr;
}
return *this;
}
~auto_ptr()
{
if (_ptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
_ptr = nullptr;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
}
测试一下:
RO::auto_ptr<Date> ap1(new Date);
// 拷贝时,管理权限转移,被拷贝对象ap1悬空
RO::auto_ptr<Date> ap2(ap1);
RO::auto_ptr<Date> ap3;
ap3 = ap2;
可以看到没有问题,最后资源都转移给了ap3
unique_ptr模拟实现:
namespace RO
{
template<class T>
class unique_ptr
{
public:
unique_ptr() = default;
explicit unique_ptr(T* ptr)
:_ptr(ptr)
{}
unique_ptr(const unique_ptr& up) = delete;
unique_ptr& operator=(unique_ptr& up) = delete;
unique_ptr(unique_ptr&& up) noexcept
:_ptr(up._ptr)
{
up._ptr = nullptr;
}
unique_ptr& operator=(unique_ptr&& up) noexcept
{
if (this != &up)
{
if (_ptr) delete _ptr;
_ptr = up._ptr;
up._ptr = nullptr;
}
return *this;
}
~unique_ptr()
{
if (_ptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
_ptr = nullptr;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
}
测试一下:
RO::unique_ptr<Date> up1(new Date);
RO::unique_ptr<Date> up2(new Date);
up1 = move(up1);
RO::unique_ptr<Date> up3(move(up2));
• shared_ptr的设计是重点研究对象,其核心在于引用计数机制的设计。需要注意以下几点:
- 每份资源需要对应一个独立的引用计数
- 静态成员方式无法实现引用计数
- 必须在堆上动态分配引用计数
- 创建智能指针对象时,需要为每份资源new一个引用计数
- 多个shared_ptr指向同一资源时,引用计数递增
- shared_ptr对象析构时,引用计数递减
- 当引用计数减至0时,表示当前析构的shared_ptr是最后一个资源管理者,此时需要释放资源
shared_ptr模拟实现:
shared_ptr
不使用成员变量或静态成员变量来存储引用计数,原因如下:
1. 成员变量计数的问题
所有权不共享:如果引用计数是对象的成员变量(即每个对象自带一个计数),则:
class BadSharedPtr { T* ptr; int count; // 成员变量计数 };
当多个智能指针指向同一个对象时,每个智能指针需要共享同一个计数器。但成员变量属于对象本身,不同智能指针无法共享同一个计数器(每个智能指针有自己的
count
副本)。示例错误场景:
T* obj = new T; BadSharedPtr p1(obj); // p1.count = 1 BadSharedPtr p2(obj); // p2.count = 1(独立计数,无法共享)
当
p1
析构时,count
减为 0 会删除obj
,但p2
仍指向已释放的内存(悬空指针)。
2. 静态成员变量计数的问题
全局共享计数:如果引用计数是静态成员变量(所有智能指针共享一个计数器):
class BadSharedPtr { T* ptr; static int count; // 静态计数 };
所有智能指针实例共享同一个计数器,导致:
BadSharedPtr p1(new T); // 静态 count = 1 BadSharedPtr p2(new T); // 静态 count = 2(错误!两个对象共享计数)
当
p1
析构时,count
减为 1,但此时本应释放p1
的对象,却因count > 0
未释放。而p2
析构时,count
减为 0 会错误地释放p2
的对象两次(或释放未分配的内存)。
3. 正确实现:动态分配控制块
std::shared_ptr
的解决方案是:
独立控制块:在堆上动态分配一个控制块(包含引用计数、弱引用计数等)。
共享计数:所有指向同一对象的
shared_ptr
共享同一个控制块。std::shared_ptr<T> p1(new T); // 创建控制块,计数=1 std::shared_ptr<T> p2 = p1; // 共享控制块,计数=2
线程安全:控制块的引用计数操作是原子的,保证多线程安全。
关键区别总结
方案 | 问题 | 后果 |
---|---|---|
成员变量计数 | 不同智能指针无法共享同一对象的计数 | 重复释放或内存泄漏 |
静态成员计数 | 所有对象共享全局计数,无法区分不同对象 | 错误释放/未释放对象 |
动态控制块 | 每个对象独立控制块,同一对象的智能指针共享计数 | 安全且正确 |
结论
std::shared_ptr
必须使用堆分配的控制块(而非成员/静态变量)来确保:
同一对象的所有智能指针共享同一个计数器。
不同对象的计数器完全独立。
线程安全的原子操作。
下面我们只是简单模拟实现shared_ptr,所以我们在堆上动态分配引用计数
namespace RO
{
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(const shared_ptr& sp)
:_ptr(sp._ptr)
,_pcount(sp._pcount)
,_del(sp._del)
{
++(*_pcount);
}
shared_ptr& operator=(const shared_ptr& sp)
{
// 不同对象共享同一份资源,不能通过 this != &sp 来判断
if (_ptr != sp._ptr)
{
if (--(*_pcount) == 0)
{
_del(_ptr);
delete _pcount;
_ptr = _pcount = nullptr;
}
_ptr = sp._ptr;
_pcount = sp._pcount;
++(*_pcount);
_del = sp._del;
}
return *this;
}
shared_ptr(shared_ptr&& sp) noexcept
:_ptr(sp._ptr)
,_pcount(sp._pcount)
,_del(move(sp._del))
{
sp._ptr = nullptr;
sp._pcount = nullptr;
}
shared_ptr& operator=(shared_ptr&& sp) noexcept
{
// 检查自赋值
if (this != &sp) // 这里不能使用_ptr != sp._ptr来判断,会导致指向相同资源的对象之 间进行移动赋值时不能进入下面代码,导致移动后的对象未被悬空
{
if (_pcount && --(*_pcount) == 0) // 先判空避免对移动后的对象重新移动赋值,出现空指针解引用的问题
{
_del(_ptr);
delete _pcount;
}
_ptr = sp._ptr;
_pcount = sp._pcount;
_del = move(sp._del);
sp._ptr = nullptr;
sp._pcount = nullptr;
}
return *this;
}
~shared_ptr()
{
// 检查计数指针是否为空
if (_pcount == nullptr)
{
return; // 已被移动过的对象,无需操作
}
if (--(*_pcount) == 0)
{
if (_ptr)
{
cout << "delete:" << _ptr << endl;
_del(_ptr);
_ptr = nullptr;
}
delete _pcount;
_pcount = nullptr;
}
}
int use_count() const
{
return *_pcount;
}
T* get() const
{
return _ptr;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
int* _pcount;
function<void(T*)> _del = [](T* ptr) { delete ptr; };
};
}
核心设计思想
1. 引用计数机制
使用
_pcount
指针动态分配引用计数所有共享同一资源的智能指针共享同一个计数器
当引用计数归零时自动释放资源
2. 自定义删除器的实现(关键设计)
为何使用 std::function
包装器
function<void(T*)> _del = [](T* ptr) { delete ptr; };
这种设计提供了三大优势:
类型擦除(Type Erasure)
可以接受任何可调用对象作为删除器
函数指针、lambda 表达式、函数对象等
统一存储为
std::function<void(T*)>
类型
灵活的删除策略
支持不同资源的释放方式
数组:
[](T* p) { delete[] p; }
文件句柄:
[](FILE* f) { fclose(f); }
自定义资源释放逻辑
默认行为与自定义行为无缝切换
默认使用
delete ptr
释放资源用户可提供自定义删除器覆盖默认行为
3. 删除器参数的构造函数
template<class D>
shared_ptr(T* ptr, D del)
:_ptr(ptr)
,_pcount(new int(1))
,_del(del) // 存储自定义删除器
{}
这个模板构造函数的设计亮点:
模板参数
D
:自动推导删除器类型完美转发:直接存储删除器对象
类型安全:确保删除器签名匹配
void(T*)
测试一下:
RO::shared_ptr<Date> sp1(new Date);
// 支持拷贝
RO::shared_ptr<Date> sp2(sp1);
cout << sp1.use_count() << endl;
sp1->_year++;
cout << sp1->_year << endl;
cout << sp2->_year << endl;
// 支持移动,但是移动后sp1也悬空,所以使用移动要谨慎
RO::shared_ptr<Date> sp4(move(sp1));
sp2 = move(sp4);
sp1 = move(sp2);
sp1 = move(sp1);
// 自定义文件删除器
//auto file_deleter = [](FILE* ptr) {
// cout << "fclose:" << ptr << endl;
// fclose(ptr);
// };
使用自定义删除器
//FILE* fp = fopen("data.txt", "w");
//RO::shared_ptr<FILE> file_ptr(fp, file_deleter);
// 等价于上面
RO::shared_ptr<FILE> sp6(fopen("Test.cpp", "r"), [](FILE* ptr) {
cout << "fclose:" << ptr << endl;
fclose(ptr);
});
// 数组需要特殊删除器
auto array_deleter = [](int* p) { delete[] p; };
RO::shared_ptr<int> arr(new int[10], array_deleter);
运行结果:
5. shared_ptr与weak_ptr
5.1 shared_ptr循环引用问题
• shared_ptr在大多数情况下非常适合管理资源,既支持RAII机制,也支持拷贝操作。但在循环引用场景中会导致资源无法释放,造成内存泄漏。我们需要理解循环引用的产生原因,并掌握使用weak_ptr解决此类问题的方法。
• 如图所示场景中,当n1和n2析构后,两个节点的引用计数仅降至1:
struct ListNode
{
int _data;
std::shared_ptr<ListNode> _next;
std::shared_ptr<ListNode> _prev;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
int main()
{
// 循环引用 -- 内存泄露
std::shared_ptr<ListNode> n1(new ListNode);
std::shared_ptr<ListNode> n2(new ListNode);
cout << n1.use_count() << endl;
cout << n2.use_count() << endl;
n1->_next = n2;
n2->_prev = n1;
cout << n1.use_count() << endl;
cout << n2.use_count() << endl;
return 0;
}
- 右边节点的释放时机:由左边节点的_next成员管理,_next析构后右边节点才会释放。
- _next的析构时机:作为左边节点的成员,需要左边节点先释放。
- 左边节点的释放时机:由右边节点的_prev成员管理,_prev析构后左边节点才会释放。
- _prev的析构时机:作为右边节点的成员,需要右边节点先释放。
• 这样就形成了逻辑上的循环依赖关系,导致双方都无法释放,最终造成内存泄漏。
运行结果:
并没有调用析构函数,造成内存泄漏。
• 解决方案是将ListNode结构体中的_next和_prev改为weak_ptr。weak_ptr在绑定到shared_ptr时不会增加引用计数,使得_next和_prev不再参与资源管理,从而有效打破循环引用。
struct ListNode
{
int _data;
//std::shared_ptr<ListNode> _next;
//std::shared_ptr<ListNode> _prev;
// 这里改成weak_ptr,当n1->_next = n2;绑定shared_ptr时
// 不增加n2的引用计数,不参与资源释放的管理,就不会形成循环引用了
std::weak_ptr<ListNode> _next;
std::weak_ptr<ListNode> _prev;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
int main()
{
std::shared_ptr<ListNode> n1(new ListNode);
std::shared_ptr<ListNode> n2(new ListNode);
cout << n1.use_count() << endl;
cout << n2.use_count() << endl;
n1->_next = n2;
n2->_prev = n1;
cout << n1.use_count() << endl;
cout << n2.use_count() << endl;
return 0;
}
运行结果:
可以看到使用weak_ptr后,没有增加引用计数,最后也正确调用了2次析构函数
不过需要注意weak_ptr的用法
// weak_ptr不支持管理资源,不支持RAII
// weak_ptr是专门绑定shared_ptr,不增加他的引用计数,作为一些场景的辅助管理
std::weak_ptr<ListNode> wp(new ListNode);
例如这样使用是会报错的
5.2 weak_ptr
• weak_ptr不具备RAII特性,也无法直接访问资源。根据文档说明,weak_ptr在构造时只能绑定到shared_ptr对象,而不能直接绑定到资源。这种绑定方式不会增加shared_ptr的引用计数,从而有效解决了循环引用问题。
• weak_ptr没有重载operator*和operator->等操作符,因为它不参与资源管理。如果其绑定的shared_ptr已经释放资源,weak_ptr访问资源将存在安全隐患。weak_ptr提供expired()方法检查资源是否过期,use_count()方法获取shared_ptr的引用计数。当需要访问资源时,可以调用lock()方法返回一个管理该资源的shared_ptr对象:若资源已释放则返回空shared_ptr;若资源未释放,则通过返回的shared_ptr安全地访问资源。
int main()
{
std::shared_ptr<string> sp1(new string("111111"));
std::shared_ptr<string> sp2(sp1);
std::weak_ptr<string> wp = sp1;
cout << wp.expired() << endl;
cout << wp.use_count() << endl;
// sp1和sp2都指向了其他资源,则weak_ptr就过期了
sp1 = make_shared<string>("222222");
cout << wp.expired() << endl;
cout << wp.use_count() << endl;
sp2 = make_shared<string>("333333");
cout << wp.expired() << endl;
cout << wp.use_count() << endl;
wp = sp1;
//std::shared_ptr<string> sp3 = wp.lock();
auto sp3 = wp.lock();
cout << wp.expired() << endl;
cout << wp.use_count() << endl;
*sp3 += "###";
cout << *sp1 << endl;
return 0;
}
运行结果:
5.3 weak_ptr模拟实现
template<class T>
class weak_ptr
{
public:
weak_ptr()
{}
weak_ptr(const shared_ptr<T>& sp)
:_ptr(sp.get())
{}
weak_ptr<T>& operator=(const shared_ptr<T>& sp)
{
_ptr = sp.get();
return *this;
}
private:
T* _ptr = nullptr;
};
需要注意的是我们这里实现的shared_ptr和weak_ptr都是以最简洁的方式实现的,只能满足基本的功能,这里的weak_ptr lock等功能是无法实现的,想要实现就要把shared_ptr和weak_ptr一起改了,把引用计数拿出来放到一个单独类型,shared_ptr和weak_ptr都要存储指向这个类的对象才能实现,有兴趣可以去翻翻源代码
6. shared_ptr的线程安全问题
• shared_ptr的引用计数对象位于堆上。当多个线程同时对shared_ptr对象进行拷贝或析构操作时,会访问和修改同一个引用计数,从而产生线程安全问题。因此,shared_ptr的引用计数需要通过加锁或原子操作来保证线程安全性。
• shared_ptr管理的对象本身也存在线程安全问题,但这不属于shared_ptr的管理范畴,应由使用shared_ptr的上层代码负责控制对象的线程安全。
• 以下程序可能导致崩溃或资源泄漏(A资源未释放)。
struct AA
{
int _a1 = 0;
int _a2 = 0;
~AA()
{
cout << "~AA()" << endl;
}
};
int main()
{
RO::shared_ptr<AA> p(new AA);
const size_t n = 100000;
mutex mtx;
auto func = [&]()
{
for (size_t i = 0; i < n; ++i)
{
// 这里智能指针拷贝会++计数
RO::shared_ptr<AA> copy(p);
{
unique_lock<mutex> lk(mtx);
copy->_a1++;
copy->_a2++;
}
}
};
thread t1(func);
thread t2(func);
t1.join();
t2.join();
cout << p->_a1 << endl;
cout << p->_a2 << endl;
cout << p.use_count() << endl;
return 0;
}
代码分析
代码行为
创建
shared_ptr<AA>
对象p
指向一个AA
实例创建两个线程
t1
和t2
,每个线程执行:循环 100,000 次
每次循环创建
p
的拷贝copy
在互斥锁保护下修改
copy
指向对象的成员变量
主线程最后输出:
_a1
和_a2
的值(预期为 200,000)引用计数
use_count()
(预期为 1)
线程安全问题
问题主要出现在引用计数操作上:
// 拷贝构造函数中的非原子操作
shared_ptr(const shared_ptr& sp)
:_ptr(sp._ptr)
,_pcount(sp._pcount)
,_del(sp._del)
{
++(*_pcount); // 非原子操作,线程不安全!
}
// 析构函数中的非原子操作
~shared_ptr()
{
if (--(*_pcount) == 0) // 非原子操作,线程不安全!
{
// ...
}
}
具体问题表现
引用计数错误:
多个线程同时执行
++(*_pcount)
或--(*_pcount)
可能导致计数不准确(小于实际值)
最终
use_count()
输出可能不等于 1
对象提前释放:
如果计数错误地变为 0
线程可能提前释放
AA
对象导致其他线程访问已释放内存(崩溃)
数据竞争:
虽然
_a1
和_a2
的修改有互斥锁保护但引用计数操作没有保护
导致未定义行为
运行此代码时,可能出现以下情况:
程序崩溃:
引用计数错误归零,对象被提前释放
后续访问
p->_a1
时访问已释放内存
输出错误结果:
_a1
和_a2
小于 200,000(计数错误导致部分操作未执行)引用计数不等于 1(计数操作未同步)
运行结果:
可以通过以下方案解决:
- 将bit::shared_ptr的引用计数从int改为atomic<int>,确保引用计数操作的原子性
- 使用互斥锁对引用计数操作进行保护
解决方案:使用原子引用计数
namespace RO
{
template<class T>
class shared_ptr
{
private:
T* _ptr;
std::atomic<int>* _pcount; // 改为原子指针
function<void(T*)> _del = [](T* ptr) { delete ptr; };
public:
explicit shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
,_pcount(new std::atomic<int>(1)) // 初始化原子计数器
{}
// 拷贝构造(使用原子操作)
shared_ptr(const shared_ptr& sp)
:_ptr(sp._ptr)
,_pcount(sp._pcount)
,_del(sp._del)
{
++(*_pcount); // 原子增加
}
// 析构函数(使用原子操作)
~shared_ptr()
{
if (_pcount == nullptr) return;
// 原子减少并获取结果
if (--(*_pcount) == 0) // 原子操作
{
if (_ptr)
{
_del(_ptr);
_ptr = nullptr;
}
delete _pcount;
_pcount = nullptr;
}
}
// 其他成员函数保持不变...
};
}
关键修改说明
使用
std::atomic<int>
:std::atomic<int>* _pcount;
原子类型保证计数操作的原子性
++
和--
操作是线程安全的
正确的初始化:
_pcount(new std::atomic<int>(1))
使用
new
分配原子计数器初始值设为 1
保持其他逻辑不变:
移动语义
拷贝赋值
自定义删除器
这些逻辑不受原子计数影响
为什么原子计数能解决问题?
原子操作特性
不可分割性:
++(*_pcount)
和--(*_pcount)
成为单指令操作不会被线程切换打断
内存顺序保证:
默认使用
memory_order_seq_cst
保证操作在所有线程中顺序一致
无数据竞争:
多个线程同时修改计数时
结果总是确定且正确的
性能考虑
原子操作代价:
比普通操作稍慢
但比互斥锁高效得多
与标准库一致:
std::shared_ptr
使用原子操作管理计数这是标准做法
7. C++11和boost中智能指针的关系
• Boost库作为C++标准库的重要补充,是一个由全球C++开发者共同维护的开源项目。它最初由Beman Dawes于1998年发起,旨在为C++标准化工作提供实践参考。Boost库中超过80%的组件最终都被纳入了C++标准,其中智能指针的发展历程尤为典型。Dawes本人不仅是Boost社区的创始人,还担任C++标准委员会库工作组的负责人,这为Boost与标准C++的协同演进提供了制度保障。
• C++98标准首次尝试引入智能指针概念,提供了auto_ptr。但它在所有权转移时采用移动语义的设计存在严重缺陷:
- 会导致源指针被置空
- 不支持STL容器
- 容易引发悬挂指针问题 例如:
std::auto_ptr<int> p1(new int(10));
std::auto_ptr<int> p2 = p1; // p1变为NULL
• Boost库针对auto_ptr的缺陷进行了全面改进:
- scoped_ptr:不可拷贝的独占指针,通过私有化拷贝构造函数实现
- scoped_array:专用于数组的scoped_ptr版本
- shared_ptr:基于引用计数的共享所有权指针
- shared_array:支持数组的shared_ptr版本
- weak_ptr:解决shared_ptr循环引用问题的观察者指针 典型用例:
boost::shared_ptr<Resource> res1(new Resource);
boost::shared_ptr<Resource> res2 = res1; // 引用计数+1
• 2005年的C++技术报告TR1(Technical Report 1)作为标准化的过渡方案,首次在标准文档中引入shared_ptr等组件。但需要注意的是:
- TR1不是正式标准,而是实验性规范
- 各编译器实现存在差异
- 通常需要包含<tr1/memory>头文件
• C++11正式将智能指针纳入标准:
- unique_ptr:取代auto_ptr和boost::scoped_ptr
- 通过删除拷贝构造函数确保独占所有权
- 支持移动语义(std::move)
- 可自定义删除器
- shared_ptr/weak_ptr:与boost版本基本兼容 改进示例:
std::unique_ptr<Object> obj(new Object);
std::unique_ptr<Object> obj2 = std::move(obj); // 合法所有权转移
// 自定义删除器
auto deleter = [](FILE* fp){ fclose(fp); };
std::unique_ptr<FILE, decltype(deleter)> file(fopen("data.txt","r"), deleter);
标准库智能指针的实现直接参考了Boost的成熟设计,但做了以下优化:
- 更好的移动语义支持
- 更完善的内存模型
- 与标准库其他组件更紧密的集成
- 线程安全性保证
8. 内存泄漏
8.1 内存泄漏的定义及其危害
定义:内存泄漏是指由于程序设计疏忽或错误导致程序无法释放已不再使用的内存。常见原因包括忘记释放内存或异常情况下未能执行释放操作。需要注意的是,内存泄漏并非物理内存的消失,而是应用程序在分配内存后,因设计缺陷失去对该内存的控制权,从而造成内存资源的浪费。
危害:对于短期运行的程序,内存泄漏的影响相对有限,因为进程终止时会自动解除内存映射关系并释放物理内存。然而,对于长期运行的程序(如操作系统、后台服务或持续运行的客户端),内存泄漏会导致严重后果:随着时间推移,可用内存不断减少,系统响应速度逐渐变慢,最终可能导致程序崩溃或系统卡死。
int main()
{
// 申请一个1G未释放,这个程序多次运行也没啥危害
// 因为程序马上就结束,进程结束各种资源也就回收了
char* ptr = new char[1024 * 1024 * 1024];
cout << (void*)ptr << endl;
return 0;
}
8.2 内存泄漏检测方法(了解)
• Linux平台:Linux下几款C++程序中的内存泄露检查工具
• Windows平台:windows下的内存泄露检测工具VLD使用
8.3 内存泄漏预防措施
• 规范开发流程:
- 建立良好的设计规范和编码习惯
- 确保内存申请与释放成对出现
- 注意:异常场景下仍需智能指针保证安全
• 资源管理建议:
- 优先使用智能指针管理资源
- 特殊场景可基于RAII思想实现自定义资源管理器
• 定期检测:
- 项目上线前必须进行内存泄漏检测
- 注意评估检测工具的可靠性(部分工具可能存在收费或准确性问题)
总结: 内存泄漏常见解决方案分为两类:
- 事前预防:如智能指针等机制
- 事后排查:借助专业检测工具