在 C++ 程序开发中,内存泄漏与异常安全问题始终是开发者的"隐形陷阱"。手动通过 new/delete 管理动态内存,不仅易因遗漏释放、异常跳转导致资源泄漏,更会让代码逻辑冗余且难以维护。而智能指针的出现,正是以 RAII(资源获取即初始化)思想为核心,通过封装原始指针、利用对象生命周期自动管理资源,从根源上解决内存管理痛点,成为现代 C++ 开发中不可或缺的工具。
1. 智能指针的使用场景分析
问题场景:手动管理内存导致的泄漏风险
手动 new/delete 时,若发生异常,delete 可能无法执行,造成内存泄漏;且多层 new 抛异常时,异常捕获逻辑会非常复杂。
cpp
// 除0时抛出异常
double Divide(int a, int b)
{
if (b == 0)
{
throw "Divide by zero condition!";
}
else
{
return (double)a / (double)b;
}
}
void Func()
{
int* array1 = new int[10];
int* array2 = new int[10]; // 若此处抛异常,array1 无法释放
try
{
int len, time;
cin >> len >> time;
cout << Divide(len, time) << endl;
}
catch (...)
{
// 捕获异常后手动释放内存
cout << "delete []" << array1 << endl;
delete[] array1;
cout << "delete []" << array2 << endl;
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;
}
具体风险点
- Func 函数内的风险:
int* array1 = new int[10]; int* array2 = new int[10]; 连续分配了两块数组内存。
如果在 new int[10] 过程中抛异常(例如内存不足),或者 Divide(len, time) 抛出除0异常,程序会跳转到 catch (...) 块。
在 catch 块中,虽然释放了 array1 和 array2,但如果是第一次 new 就失败,array2 根本没有被赋值,此时执行 delete[] array2 会导致未定义行为(程序崩溃)。
- 当 array2 = new int[10] 抛出异常(比如内存不足)时:
array1 已经成功分配内存
程序会直接跳转到最近的异常处理块,不会执行 try 内部的代码,也不会执行 Func 末尾的 delete[]
此时 array1 没有被释放,会造成内存泄漏; array2 还没成功创建,不需要释放
关键问题:new 操作在 try 块之前,异常发生时不会进入 catch 块,array1 无法被释放。
解决方案 1:把 new 放进 try 块(手动管理)
cpp
void Func()
{
int* array1 = nullptr;
int* array2 = nullptr;
try
{
array1 = new int[10];
array2 = new int[10]; // 若失败,array1 已分配,会在 catch 中释放
int len, time;
cin >> len >> time;
cout << Divide(len, time) << endl;
}
catch (...)
{
cout << "delete []" << array1 << endl;
delete[] array1; // 安全释放已分配的 array1
cout << "delete []" << array2 << endl;
delete[] array2; // array2 可能为 nullptr,delete nullptr 是安全的
throw;
}
// 正常流程释放
cout << "delete []" << array1 << endl;
delete[] array1;
cout << "delete []" << array2 << endl;
delete[] array2;
}
✅ 优点:能处理第二个 new 失败的情况 ❌ 缺点:代码依然繁琐,容易漏写 delete
解决方案 2:使用智能指针(推荐,RAII 自动管理)
cpp
#include <memory> // 必须包含
void Func()
{
// 用 unique_ptr 管理数组,构造即接管内存,析构自动释放
unique_ptr<int[]> array1(new int[10]);
unique_ptr<int[]> array2;
try
{
array2.reset(new int[10]); // 若失败,array1 会自动析构释放
int len, time;
cin >> len >> time;
cout << Divide(len, time) << endl;
}
catch (...)
{
// 无需手动 delete,array1 和 array2 会自动释放
throw;
}
// 正常流程:无需任何释放代码
}
✅ 优点:无论 new 成功/失败、正常/异常流程,unique_ptr 出作用域时都会自动释放内存;完全避免内存泄漏,代码更简洁;delete[] nullptr 的问题也被智能指针内部处理了
2. RAII 和智能指针的设计思路
RAII 思想
RAII(Resource Acquisition Is Initialization):资源获取即初始化,利用对象生命周期管理动态资源(内存、文件、锁等)。
资源在对象构造时获取,对象生命周期内有效;资源在对象析构时自动释放,保证异常安全;避免手动 delete,从根源解决内存泄漏
智能指针的核心设计
智能指针是封装了原始指针的类,满足:
-
RAII 机制:析构函数自动释放资源
-
指针行为模拟:重载 operator*/operator->/operator[] 等运算符,像原生指针一样访问资源
cpp
// 模板类:简易智能指针,基于RAII思想实现
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; // 封装的原生指针,管理动态内存
};
void Func()
{
// 构造时接管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;
// 函数结束,sp1、sp2局部对象析构
// 自动调用析构函数释放内存,无需手动delete[]
}
第二个 new 失败时发生了什么?
sp1 已构造完成,是合法栈对象; sp2 构造到一半抛异常,构造失败; C++ 保证:已成功构造的局部对象会被正常析构; sp1 析构 → 执行 ~SmartPtr() → delete[] _ptr; 内存安全释放,无泄漏
3. C++ 标准库智能指针的使用
头文件与分类
所有智能指针定义在 <memory> 头文件中,核心类型:
|------------|-------|----------------------|----------------------|
| 智能指针 | 版本 | 核心特点 | 适用场景 |
| auto_ptr | C++98 | 拷贝时转移资源管理权,被拷贝对象悬空 | 已废弃,C++11 后禁止使用 |
| unique_ptr | C++11 | 独占所有权,不支持拷贝,仅支持移动 | 不需要共享、不需要拷贝的场景 |
| shared_ptr | C++11 | 共享所有权,引用计数实现,支持拷贝/移动 | 需要多个指针共享同一份资源 |
| weak_ptr | C++11 | 弱引用,不增加引用计数,不管理资源 | 解决 shared_ptr 循环引用问题 |
基础示例代码
cpp
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;
}
};
int main()
{
// auto_ptr:拷贝后原对象悬空(危险,已弃用)
auto_ptr<Date> ap1(new Date);
auto_ptr<Date> ap2(ap1); // ap1 变为 nullptr
// ap1->_year++; // 空指针访问,未定义行为
// unique_ptr:独占所有权,不支持拷贝,支持移动
unique_ptr<Date> up1(new Date);
// unique_ptr<Date> up2(up1); // 编译报错:拷贝构造被删除
unique_ptr<Date> up3(move(up1)); // 移动后 up1 悬空
// shared_ptr:共享所有权,引用计数
shared_ptr<Date> sp1(new Date);
shared_ptr<Date> sp2(sp1); // 引用计数+1
shared_ptr<Date> sp3(sp2); // 引用计数+1
cout << sp1.use_count() << endl; // 输出 3
sp1->_year++;
cout << sp1->_year << endl; // 所有指针指向同一块内存
cout << sp2->_year << endl;
cout << sp3->_year << endl;
shared_ptr<Date> sp4(move(sp1)); // 移动后 sp1 悬空,引用计数不变
return 0;
}
核心知识点
- auto_ptr(已废弃)
机制:拷贝时转移资源所有权,原指针置空
缺陷:易造成悬空指针、容器中使用会导致程序崩溃
结论:C++11 后彻底弃用,一律用 unique_ptr 替代
- unique_ptr(独占型)
核心特性:同一时间只能有一个指针拥有资源
语法限制:禁用拷贝构造、拷贝赋值,仅支持移动语义
适用场景:不需要共享、追求极致性能的场景
内存管理:析构时自动释放资源,异常安全
- shared_ptr(共享型)
核心机制:引用计数,多个指针共享同一份资源
计数规则:拷贝构造 / 赋值 → 计数 +1 析构 / 重置 → 计数 -1 计数为 0 → 释放资源
常用接口:use_count() 获取引用计数
注意:移动语义不会改变引用计数,仅转移所有权
- 通用指针行为
智能指针均重载 * / ->,使用方式与原生指针一致
栈上的智能指针对象,出作用域自动析构,无需手动释放
配合自定义类的析构函数,可直观观察资源生命周期
数组资源与删除器
智能指针默认用 delete 释放资源,若管理 new[] 分配的数组,需指定删除器:
方案1:特化版本(推荐)
cpp
#include <iostream>
#include <memory> // 智能指针头文件
using namespace std;
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;
}
};
int main()
{
// ==================== unique_ptr 数组特化 ====================
// 1. 数组特化版本:<Date[]> 表示管理数组,析构自动用 delete[]
unique_ptr<Date[]> up1(new Date[5]);
// 数组特化后,支持 operator[] 下标访问,不支持 * / ->
for (int i = 0; i < 5; ++i) {
up1[i] = Date(2025, 3, 19); // 赋值
}
// ==================== shared_ptr 数组特化 ====================
// 2. C++17 起支持数组特化 <Date[]>,析构自动用 delete[]
shared_ptr<Date[]> sp1(new Date[5]);
// 同样支持 operator[] 下标访问
sp1[0] = Date(2024, 12, 31);
return 0;
}
1、unique_ptr<Date[]> 详解
- 核心机制
模板参数写 Date[],代表管理数组,是标准库提供的偏特化版本。
析构函数默认调用 delete[],完美匹配 new[]。
- 接口限制(特化后)
✅ 支持:operator[](数组下标访问)
❌ 不支持:operator*、operator->(因为是数组,不是单个对象)
- 为什么要用它?
如果写成 unique_ptr<Date> 管理数组:unique_ptr<Date> up(new Date[5]); // 错误!
析构会用 delete 而非 delete[] → 未定义行为(内存错乱/泄漏)。
2、shared_ptr<Date[]> 详解
- 版本要求
C++17 及以上 才支持 shared_ptr<Date[]> 数组特化。 C++11/14 只能用自定义删除器实现。
- 核心特性
引用计数机制不变,共享数组资源。析构自动调用 delete[]。支持 operator[] 下标访问。
3、C++11/14 下 shared_ptr 管理数组(兼容方案)
C++11/14 没有 shared_ptr<Date[]> 特化,必须指定删除器:
cpp
// 方式1:lambda 删除器
shared_ptr<Date> sp1(new Date[5], [](Date* p){ delete[] p; });
// 方式2:函数对象删除器
shared_ptr<Date> sp2(new Date[5], default_delete<Date[]>());
注意:C++11/14 的 shared_ptr<Date> 不支持 [],只能用 get()[i] 访问:
cpp
sp1.get()[0] = Date(2025, 3, 19);
4、关键知识点总结
- 匹配原则
new 单个对象 → unique_ptr<Date> / shared_ptr<Date> → delete
new[] 数组 → unique_ptr<Date[]> / shared_ptr<Date[]> → delete[]
- unique_ptr<Date[]>
C++11 就支持数组特化。 天然支持 [],禁止 */->,安全简洁。
- shared_ptr<Date[]>
C++17 正式支持。 支持 [],引用计数管理数组,自动 delete[]。
- 常见坑
不用数组特化版本管理数组 → 错用 delete → 未定义行为。
C++11/14 用 shared_ptr 管理数组必须手写删除器。
方案2:自定义删除器
cpp
#include <iostream>
#include <memory> // 智能指针
#include <cstdio> // fopen, fclose, FILE
using namespace std;
struct Date
{
int _year, _month, _day;
Date(int y=1, int m=1, int d=1) : _year(y), _month(m), _day(d) {}
~Date() { cout << "~Date()" << endl; }
};
// ==================== 1. 仿函数删除器 ====================
// 重载 operator(),作为删除器:调用 delete[]
template<class T>
class DeleteArray
{
public:
void operator()(T* ptr)
{
delete[] ptr;
}
};
// ==================== 2. 函数指针删除器 ====================
// 普通模板函数,用于释放数组
template<class T>
void DeleteArrayFunc(T* ptr)
{
delete[] ptr;
}
int main()
{
// -------------------- 仿函数删除器 --------------------
// unique_ptr:第二个模板参数是删除器类型
unique_ptr<Date, DeleteArray<Date>> up2(new Date[5], DeleteArray<Date>());
// shared_ptr:构造时传入删除器对象,自动推导类型
shared_ptr<Date> sp2(new Date[5], DeleteArray<Date>());
// -------------------- 函数指针删除器 --------------------
// unique_ptr:删除器类型为 函数指针 void(*)(Date*)
unique_ptr<Date, void(*)(Date*)> up3(new Date[5], DeleteArrayFunc<Date>);
shared_ptr<Date> sp3(new Date[5], DeleteArrayFunc<Date>);
// -------------------- lambda 删除器 --------------------
auto delArrOBJ = [](Date* ptr) { delete[] ptr; };
// unique_ptr:需要用 decltype 获取 lambda 类型
unique_ptr<Date, decltype(delArrOBJ)> up4(new Date[5], delArrOBJ);
shared_ptr<Date> sp4(new Date[5], delArrOBJ);
// ==================== 扩展:管理文件资源 ====================
// 自定义仿函数,用于关闭文件
class Fclose
{
public:
void operator()(FILE* ptr)
{
cout << "fclose:" << ptr << endl;
fclose(ptr);
}
};
// 仿函数方式管理 FILE
shared_ptr<FILE> sp5(fopen("Test.cpp", "r"), Fclose());
// lambda 方式管理 FILE
shared_ptr<FILE> sp6(fopen("Test.cpp", "r"), [](FILE* ptr) {
cout << "fclose:" << ptr << endl;
fclose(ptr);
});
return 0;
}
1、为什么需要自定义删除器?
智能指针默认用 delete 释放资源
管理 new[] 数组、文件、套接字、锁等资源时,需要自定义释放逻辑
本质:让智能指针支持非内存资源的 RAII 管理
2、三种删除器对比
- 仿函数删除器(推荐)
优点:无额外开销、类型安全、可复用 unique_ptr 需指定删除器类型:DeleteArray<Date>
适合工程化、频繁复用的场景
- 函数指针删除器
优点:简单直观 缺点:有函数指针开销,unique_ptr 写法繁琐 适合简单场景
- Lambda 删除器(最常用)
优点:就地编写、简洁灵活 unique_ptr 需用 decltype( lambda名 ) 作为模板参数
现代 C++ 首选方案
3、unique_ptr & shared_ptr 删除器区别
unique_ptr:删除器是类型的一部分,必须写在模板参数里;无运行时开销,效率更高;格式:unique_ptr<T, 删除器类型> ptr(资源, 删除器对象)
shared_ptr:删除器是构造参数,不影响类型;内部存储删除器,有轻微开销;格式:shared_ptr<T> ptr(资源, 删除器)
4、核心应用场景
1) 管理动态数组:new[] 必须搭配 delete[] 2) 管理文件:fopen → fclose
3) 管理网络套接字:socket → close 4) 管理互斥锁:lock → unlock
5) 第三方库资源:如 malloc 分配内存 → free
5、关键结论
数组优先用:unique_ptr<Date[]>、shared_ptr<Date[]>(C++17+)
兼容旧标准 / 自定义资源:使用Lambda 删除器最简洁
自定义删除器是 RAII 思想的延伸,让智能指针不只管理内存,可管理所有资源
其他实用特性
make_shared:更安全高效地创建 shared_ptr,一次分配内存(对象+引用计数)
cpp
// 直接构造并返回 shared_ptr<Date>,参数会完美转发给 Date 构造函数
shared_ptr<Date> sp2 = make_shared<Date>(2024, 9, 11);
// auto 自动推导类型,写法更简洁,效果和上面完全一致
auto sp3 = make_shared<Date>(2024, 9, 11);
std::make_shared 是 C++11 提供的标准工厂函数,用于安全、高效地创建 shared_ptr。
底层优势
-
内存分配更高效:直接一次分配:对象内存 + 引用计数内存(连续块);对比 shared_ptr<Date>(new Date):要分两次分配(对象 + 计数)
-
异常更安全:杜绝new成功但计数初始化失败导致的内存泄漏;资源创建与智能指针封装是原子操作
-
代码更简洁:不用手写 new,避免裸指针暴露;配合 auto,代码极度精简
空指针判断:shared_ptr/unique_ptr 支持 operator bool,可直接判断是否为空
cpp
// 智能指针重载了 operator bool,可以直接放在 if 里做判空
if (sp1) {
cout << "sp1 is not nullptr" << endl;
}
// 取反判断:指针为空时成立
if (!sp4) {
cout << "sp4 is nullptr" << endl;
}
- 隐式类型转换(operator bool):unique_ptr / shared_ptr 都重载了布尔转换运算符
作用:直接在 if / while 等条件中,判断智能指针是否为空(是否托管有效资源)
- 判断规则
if (sp):托管了有效指针 → 为 true;为空 → 为 false if (!sp):为空 → 为 true;不为空 → 为 false
- 等价写法(更显式)
cpp
if (sp1.get() != nullptr) // 效果一样
if (sp4.get() == nullptr)
-
适用范围:所有标准智能指针:unique_ptr/shared_ptr/weak_ptr(weak_ptr用lock()后再判空)
-
使用场景:在访问指针成员、解引用前,做空指针安全检查,避免未定义行为。
explicit 构造:防止普通指针隐式转换为智能指针,避免意外接管资源
cpp
template<class T>
class shared_ptr {
// 加 explicit:禁止隐式转换
explicit shared_ptr(T* ptr = nullptr)
: _ptr(ptr), _pcount(new int(1))
{}
...
};
explicit 只能修饰单参数构造函数,作用:禁止 普通指针 → 智能指针的隐式类型转换;只能显式构造智能指针,避免意外接管内存、造成重复释放
不加 explicit 会出什么问题(危险场景)
cpp
void func(shared_ptr<Date> sp) {
// ...
}
int main() {
Date* p = new Date;
// 危险:隐式把 p 转成 shared_ptr
func(p);
// 函数结束,sp 析构 → delete p
// 但外面还持有 p,变成野指针!
p->_year = 2025; // 未定义行为
}
调用 func(p) 时,编译器隐式构造临时 shared_ptr 接管 p;函数结束临时对象析构 → delete p;外层 p 变成野指针,继续访问直接崩溃
加 explicit 后(安全)
cpp
explicit shared_ptr(T* ptr = nullptr);
// 再写
func(p); // 编译报错!不允许隐式转换
// 必须显示构造
func(shared_ptr<Date>(p)); // 合法,显式构造
// p 已经交给 shared_ptr 托管,后面不能再手动管理。
正确写法(模拟标准库)
cpp
template<class T>
class SmartPtr {
public:
// 单参数构造 + explicit
explicit SmartPtr(T* ptr = nullptr)
: _ptr(ptr)
{}
~SmartPtr() {
delete _ptr;
}
private:
T* _ptr;
};
// 错误(有 explicit 时):
// SmartPtr<int> p = new int[10]; // 拷贝初始化 = 隐式转换,报错
// 正确(显式构造):
SmartPtr<int> p(new int[10]); // 直接初始化,允许
总结:explicit 构造 = 禁止普通指针隐式变智能指针;目的:防止意外接管资源、重复释放、野指针
标准库 shared_ptr / unique_ptr 构造函数全都带 explicit,是工程必备规范。
4. 智能指针的原理
auto_ptr 模拟实现(了解,已弃用)
核心:拷贝/赋值时转移资源管理权,被拷贝对象变为 nullptr
cpp
// 自定义命名空间,避免与 std::auto_ptr 冲突
namespace gxy
{
// 模拟 C++98 废弃的 auto_ptr 智能指针
template<class T>
class auto_ptr
{
public:
// 1. 构造函数:接管裸指针资源
auto_ptr(T* ptr)
:_ptr(ptr)
{}
// 2. 拷贝构造函数:核心设计------管理权转移
auto_ptr(auto_ptr<T>& sp)
:_ptr(sp._ptr) // 接管原指针指向的资源
{
sp._ptr = nullptr; // 原指针置空,实现资源独占
}
// 3. 赋值运算符重载:同样是管理权转移
auto_ptr<T>& operator=(auto_ptr<T>& ap)
{
if (this != &ap) // 防止自赋值
{
if (_ptr)
delete _ptr; // 先释放当前对象管理的资源
_ptr = ap._ptr; // 接管新资源
ap._ptr = nullptr; // 原对象置空
}
return *this;
}
// 4. 析构函数:自动释放资源
~auto_ptr()
{
if (_ptr) // 指针非空时才释放
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
}
// 5. 重载指针运算符,模拟原生指针行为
T& operator*() { return *_ptr; } // 解引用
T* operator->() { return _ptr; } // 成员访问
private:
T* _ptr; // 托管的原生指针
};
}
核心知识点总结
- 核心设计:管理权转移
拷贝/赋值时把资源所有权转移给新对象,原对象强制置空;同一时刻,只有一个auto_ptr持有资源
- 致命缺陷(为何被 C++11 废弃):拷贝后原对象悬空,极易踩坑
cpp
gxy::auto_ptr<int> ap1(new int(10));
gxy::auto_ptr<int> ap2 = ap1;
// ap1 已为 nullptr,继续使用会崩溃
- 与 unique_ptr 本质区别
auto_ptr:靠运行时管理权转移实现独占,语法不安全;unique_ptr:编译期禁用拷贝,只允许移动,安全可靠。
unique_ptr 模拟实现
核心:独占所有权,禁止拷贝,仅允许移动
cpp
// 自定义命名空间,避免与标准库冲突
namespace gxy
{
// 模拟 C++11 独占式智能指针 unique_ptr
template<class T>
class unique_ptr
{
public:
// 1. 显式构造函数,接管裸指针
// explicit 禁止隐式转换:如 unique_ptr<int> p = new int; 编译报错
explicit unique_ptr(T* ptr)
:_ptr(ptr)
{}
// 2. 析构函数:自动释放资源
~unique_ptr()
{
if (_ptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
}
// 3. 核心:禁止拷贝构造、拷贝赋值
// 从语法层面杜绝拷贝,保证独占性,编译期就报错
unique_ptr(const unique_ptr<T>& sp) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete;
// 4. 支持移动语义(转移所有权)
// 右值引用:只能接收临时对象或 std::move 后的对象
unique_ptr(unique_ptr<T>&& sp)
:_ptr(sp._ptr)
{
sp._ptr = nullptr; // 原指针置空,避免重复释放
}
// 移动赋值重载
unique_ptr<T>& operator=(unique_ptr<T>&& sp)
{
if (this != &sp) // 防止自移动
{
delete _ptr; // 先释放当前资源
_ptr = sp._ptr; // 接管新资源
sp._ptr = nullptr; // 原对象置空
}
return *this;
}
// 5. 重载运算符,模拟原生指针行为
T& operator*() { return *_ptr; } // 解引用
T* operator->() { return _ptr; } // 访问成员
private:
T* _ptr; // 托管的原生指针
};
}
核心知识点
- 核心设计:编译期独占
同一时刻只能有一个 unique_ptr 管理资源
靠 =delete 直接禁用拷贝,不是运行时置空,而是编译报错,比 auto_ptr 安全得多
- 关键语法:= delete
显式声明函数为删除函数; 任何拷贝行为直接编译失败,从根源避免悬空指针、重复释放
- 移动语义(&&)
允许转移所有权,但必须显式使用 std::move;转移后原对象置空,不影响程序安全;兼顾独占性与灵活性(可作为函数返回值、容器转移)
- explicit 构造
禁止 unique_ptr<int> p = new int; 这种隐式构造;必须显式写:unique_ptr<int> p(new int);
防止意外接管裸指针,提高安全性
- 与 auto_ptr 的本质区别
auto_ptr:拷贝时运行时置空,隐患极大 unique_ptr:拷贝直接编译报错,安全可控
都属于独占指针,但 unique_ptr 是现代 C++ 标准方案
- 适用场景:不需要共享、不需要拷贝的资源管理;性能敏感场景(无引用计数开销,效率接近裸指针);作为类成员指针,防止异常泄漏
cpp
int main()
{
gxy::unique_ptr<int> up1(new int(10));
// gxy::unique_ptr<int> up2 = up1; // 报错:拷贝被删除
gxy::unique_ptr<int> up2 = std::move(up1); // 允许:移动转移
*up2 = 20;
return 0;
}
shared_ptr 模拟实现(核心)
核心:引用计数,多个指针共享资源,计数为 0 时释放资源
cpp
#include<iostream>
#include<memory>
#include<functional>
#include<atomic>
using namespace std;
// 测试用的日期类
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;
}
};
// 自定义删除器:关闭文件(仿函数)
class Fclose
{
public:
// 重载(),让类对象可以像函数一样调用
void operator()(FILE* ptr)
{
cout << "fclose: " << ptr << endl;
fclose(ptr); // 关闭文件,避免文件句柄泄漏
}
};
// 自定义删除器:释放数组(函数模板)
template<class T>
void DeleteArrayFunc(T* ptr)
{
delete[] ptr; // 数组必须用 delete[],匹配 new[]
}
namespace gxy
{
// 模拟实现 shared_ptr 共享型智能指针
template<class T>
class shared_ptr
{
public:
// 1. 普通构造函数
// 管理原生指针,引用计数初始化为 1
shared_ptr(T* ptr)
: _ptr(ptr)
, _pcount(new atomic<int>(1))
{}
// 2. 带自定义删除器的构造函数
// 可传入 lambda/函数/仿函数,自定义资源释放方式
template<class D>
shared_ptr(T* ptr, D del)
: _ptr(ptr)
, _pcount(new atomic<int>(1))
, _del(del)
{}
// 3. 析构函数
// 引用计数 --,减为 0 时真正释放资源
~shared_ptr()
{
if (--(*_pcount) == 0)
{
_del(_ptr); // 调用删除器释放管理的资源
delete _pcount; // 释放引用计数的内存
}
}
// 4. 拷贝构造函数
// 共享同一块资源,引用计数 ++
shared_ptr(const shared_ptr<T>& sp)
: _ptr(sp._ptr)
, _pcount(sp._pcount)
{
(*_pcount)++;
}
// 5. 赋值运算符重载
// 先释放旧资源,再共享新资源,维护引用计数
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
// 避免管理同一块资源时重复操作
if (_ptr != sp._ptr)
{
// 先处理当前管理的旧资源
if (--(*_pcount) == 0)
{
_del(_ptr); // 用删除器释放旧对象
delete _pcount; // 释放旧计数
}
// 共享新对象的指针和引用计数
_pcount = sp._pcount;
_ptr = sp._ptr;
// 新资源计数 ++
++(*_pcount);
}
return *this;
}
// 6. 解引用重载,模拟指针解引用行为
T& operator*()
{
return *_ptr;
}
// 7. -> 重载,访问指针指向的成员
T* operator->()
{
return _ptr;
}
// 8. 获取当前引用计数
int use_count()
{
return *_pcount;
}
private:
T* _ptr; // 指向管理的资源
atomic<int>* _pcount; // 原子引用计数,保证多线程安全
// 删除器:默认使用 delete 释放单个对象,支持自定义
function<void(T*)> _del = [](T* ptr) {delete ptr; };
};
};
// 测试代码
int main()
{
// 测试1:管理单个对象
gxy::shared_ptr<Date> sp1(new Date);
// 测试2:管理数组对象,使用 DeleteArrayFunc 删除器
gxy::shared_ptr<Date> sp2(new Date[5], DeleteArrayFunc<Date>);
// 管理数组,传自定义删除器(lambda),用 delete[]
gxy::shared_ptr<Date> sp3(new Date[10], [](Date* ptr) {delete[] ptr; });
// 测试3:管理文件指针,使用 Fclose 删除器
FILE* f = fopen("test.txt", "w");
gxy::shared_ptr<FILE> sp4(f, Fclose());
return 0;
}
1、核心原理
共享式智能指针,多个指针管理同一块资源。 通过引用计数实现资源自动释放。计数为 0 时,才真正释放资源。 引用计数必须在堆上开辟,保证共享。使用 atomic 原子类型,保证多线程安全。
2、自定义删除器(三种写法)
1)lambda:[](T* p){ delete[] p; }
2)函数指针:DeleteArrayFunc<T>
3)仿函数:class Fclose { void operator()(FILE* p) { fclose(p); } };
3、重点结论
new ↔ delete new[] ↔ delete[](必须传删除器) FILE* ↔ fclose(必须传删除器)
默认删除器不能用于数组。 引用计数是共享、堆上、原子的。
4、atomic
atomic 就是 原子类型,专门用来解决 多线程同时修改同一个变量时的数据错乱问题。
普通的 int count 在多线程里 ++、-- 不是一步完成的,会被打断,导致计数算错。
atomic<int> 能让 ++ / -- / 读写 变成一个不可分割的原子操作,不会算错。
为什么要用 atomic?(重点):引用计数 _pcount 会被 多个线程同时共享、同时++/--。
++(*_pcount);
如果是普通 int*:线程A读到 2,线程B也读到 2,两个线程都写成 3 → 计数少加1,彻底错乱
如果是 atomic<int>*:同一时间只有一个线程能修改要么全做完,要么完全不做 → 计数绝对正确
atomic<int>* _pcount; 保证 引用计数的 ++ 和 -- 线程安全;保证 多线程拷贝、赋值、析构时计数不会算错;这是 shared_ptr 线程安全的基础
普通 int:多线程不安全,计数会算错;atomic:多线程安全,计数一定正确
标准库 shared_ptr / unique_ptr 的用法
cpp
int main()
{
// 单个对象
std::shared_ptr<Date> sp1(new Date);
// 支持数组特化版本,自动用 delete[]
std::shared_ptr<Date[]> sp2(new Date[10]);
// lambda 删除器(最推荐)
std::shared_ptr<Date> sp3(new Date[10], [](Date* ptr) {delete[] ptr; });
// 函数指针删除器
std::shared_ptr<Date> sp4(new Date[5], DeleteArrayFunc<Date>);
// 管理文件,仿函数删除器
std::shared_ptr<FILE> sp5(fopen("Test.cpp", "r"), Fclose());
// 管理文件,lambda 删除器
shared_ptr<FILE> sp6(fopen("Test.cpp", "r"), [](FILE* ptr) {
fclose(ptr);
});
// unique_ptr 单个对象
std::unique_ptr<Date> up1(new Date);
// unique_ptr 数组特化,默认 delete[]
std::unique_ptr<Date[]> up2(new Date[10]);
// unique_ptr + 仿函数删除器(类型要传)
std::unique_ptr<FILE, Fclose> up3(fopen("Test.cpp", "r"));
// unique_ptr + lambda 删除器(需要传decltype)
auto fcloseFunc = [](FILE* ptr) {fclose(ptr); };
std::unique_ptr<FILE, decltype(fcloseFunc)> up4(fopen("Test.cpp", "r"), fcloseFunc);
return 0;
}
-
shared_ptr 数组:shared_ptr<Date[]> 自动用 delete[],或自己传删除器
-
unique_ptr 数组:unique_ptr<Date[]> 默认就是 delete[]
-
删除器写法:shared_ptr:lambda 最方便,unique_ptr:仿函数推荐,lambda 麻烦
隐式类型转换、空判断、make_shared
cpp
int main()
{
// 裸指针构造
std::shared_ptr<Date> sp1(new Date(2024, 9, 11));
// make_shared 写法(更安全、更高效)
shared_ptr<Date> sp2 = make_shared<Date>(2024, 9, 11);
shared_ptr<Date> sp4;
// 智能指针可以直接当 bool 用(重载了 operator bool)
if (sp1)
cout << "sp1 is not nullptr" << endl;
// 判断空指针
if (!sp4)
cout << "sp4 is nullptr" << endl;
// 错误!explicit 构造,不允许隐式类型转换
// shared_ptr<Date> sp5 = new Date(2024, 9, 11);
// unique_ptr<Date> sp6 = new Date(2024, 9, 11);
return 0;
}
-
make_shared:比 new 构造更高效、异常安全
-
operator bool():if(sp1) 判断是否为空指针
-
explicit 构造:不允许 shared_ptr<Date> p = new Date;只能 shared_ptr<Date> p(new Date);
weak_ptr 模拟实现
核心:弱引用,不增加引用计数,不管理资源生命周期
cpp
namespace gxy
{
// 弱智能指针:只观察,不管理,不影响引用计数
template<class T>
class weak_ptr
{
public:
// 默认构造:空指针
weak_ptr() = default;
// 用 shared_ptr 构造 weak_ptr
weak_ptr(const shared_ptr<T>& sp)
:_ptr(sp.get()) // 只拿原生指针,不增加计数
{}
// 用 shared_ptr 赋值 weak_ptr
weak_ptr<T>& operator=(const shared_ptr<T>& sp)
{
_ptr = sp.get(); // 同样只赋值指针,不增减计数
return *this;
}
private:
T* _ptr = nullptr; // 仅仅保存观察用的原生指针
};
}
- weak_ptr 是什么?观察者指针;只观察 shared_ptr 管理的对象;不参与管理权,不增加引用计数
不会调用析构,不会释放资源
-
它做了什么?只存一个原生指针 _ptr,从 shared_ptr 拿地址,完全不碰引用计数
-
重点特性:拷贝、赋值、析构 都不影响引用计数;不能直接 *、-> 访问对象(你这个简易版没做限制);真正的 weak_ptr 必须通过 lock() 转为 shared_ptr 才能使用
-
为什么需要 weak_ptr?解决 shared_ptr 循环引用 导致的内存泄漏。
比如:A 里面有 shared_ptr,B 里面有 shared_ptr,互相持有,计数永远不为 0 → 永远不释放。
用 weak_ptr 打破循环就行。
weak_ptr = 只看不管的智能指针;不增加计数,不释放资源,专门解决循环引用。
5. shared_ptr 和 weak_ptr
5.1 shared_ptr 循环引用问题
循环引用:两个 shared_ptr 互相指向对方,导致引用计数永远无法为 0,资源泄漏。
cpp
struct ListNode
{
int _data;
shared_ptr<ListNode> _next;
shared_ptr<ListNode> _prev;
~ListNode() { cout << "~ListNode()" << endl; }
};
int main()
{
std::shared_ptr<ListNode> n1(new ListNode);
std::shared_ptr<ListNode> n2(new ListNode);
n1->_next = n2; // n1 引用 n2
n2->_prev = n1; // n2 引用 n1
cout << n1.use_count() << endl; // 2
cout << n2.use_count() << endl; // 2
// n1、n2 析构后,引用计数减为 1,资源永远无法释放
return 0;
}
构造两个节点:n1->_next = n2; n2->_prev = n1; 形成:n1 ↔ n2,互相持有对方的 shared_ptr。
引用计数情况:n1 的 use_count:2,n2 的 use_count:2
为什么?n1 自己 + n2->_prev,n2 自己 + n1->_next
cpp
n1 的计数:2
↳ 外部 n1 智能指针
↳ n2->_prev 持有
n2 的计数:2
↳ 外部 n2 智能指针
↳ n1->_next 持有
出作用域会发生什么?1) n1 销毁 → 计数 2 → 1 2) n2 销毁 → 计数 2 → 1
结果:两个对象的引用计数都变成 1,永远不会降到 0→ 永远不调用析构函数 → 内存泄漏!
为什么会这样?
要释放 n1:必须先释放 n2->_prev,要释放 n2:必须先释放 n1->_next,互相等待,死锁释放
解决方案:将 _next/_prev 改为 weak_ptr,弱引用不增加引用计数
cpp
struct ListNode
{
int _data;
weak_ptr<ListNode> _next; // 弱引用
weak_ptr<ListNode> _prev; // 弱引用
~ListNode() { cout << "~ListNode()" << endl; }
};
weak_ptr 不增加引用计数,n1 的 use_count 最终能减到 0,n2 的 use_count 最终也能减到 0
→ 正常释放,没有泄漏
5.2 weak_ptr 核心特性
不支持 RAII:不管理资源生命周期,不释放资源
不增加引用计数:绑定 shared_ptr 时不影响引用计数
不重载指针运算符:operator*/operator-> 未重载,无法直接访问资源
安全访问:需调用 lock() 获取 shared_ptr,若资源已释放则返回空 shared_ptr
状态检查:expired() 检查资源是否过期,use_count() 获取当前引用计数
cpp
int main()
{
// sp1 管理 "111111",引用计数 = 1
std::shared_ptr<string> sp1(new string("111111"));
// sp2 拷贝 sp1,引用计数 = 2
std::shared_ptr<string> sp2(sp1);
// 用 shared_ptr 构造 weak_ptr
// wp 只观察,不增加引用计数
std::weak_ptr<string> wp = sp1;
// expired():检查资源是否已释放
// 资源还在 → 返回 false(0)
cout << wp.expired() << endl;
// use_count():查看对应 shared_ptr 的引用计数 → 2
cout << wp.use_count() << endl;
// sp1 指向新资源 "222222"
// 旧资源 "111111" 计数 2 → 1
sp1 = make_shared<string>("222222");
// 资源还在 → false(0)
cout << wp.expired() << endl;
// 计数变为 1
cout << wp.use_count() << endl;
// sp2 也指向新资源 "333333"
// 旧资源 "111111" 计数 1 → 0,被释放
sp2 = make_shared<string>("333333");
// 资源已销毁 → true(1)
cout << wp.expired() << endl;
// 计数变为 0
cout << wp.use_count() << endl;
// wp 现在观察 sp1("222222")
wp = sp1;
// lock():
// 如果资源还在 → 返回一个有效的 shared_ptr,计数+1
// 如果资源没了 → 返回空的 shared_ptr
auto sp3 = wp.lock();
// 资源还在 → false(0)
cout << wp.expired() << endl;
// sp1+sp3 共同管理 → 计数 2
cout << wp.use_count() << endl;
// sp1 指向新资源 "4444444"
// 旧资源 "222222" 计数 2 → 1(还被 sp3 指着)
sp1 = make_shared<string>("4444444");
// 资源还在(sp3 还拿着)→ false(0)
cout << wp.expired() << endl;
// 计数 1
cout << wp.use_count() << endl;
return 0;
}
核心接口知识点
-
wp.expired():判断观察的对象是否已经被释放,资源还在 → false,资源没了 → true
-
wp.use_count():返回对应 shared_ptr 的引用计数;不增加计数,只是查看
-
wp.lock():返回一个 shared_ptr;如果资源还活着:得到有效指针,计数+1;如果资源已销毁:得到空指针;作用:安全地访问对象,避免悬空指针
6. shared_ptr 的线程安全问题
线程安全范围
-
引用计数:多线程拷贝/析构 shared_ptr 时,引用计数修改需线程安全(标准库实现用原子操作或锁)
-
资源本身:shared_ptr 不保证指向资源的线程安全,需外层控制(如互斥锁)
线程安全示例
cpp
struct AA
{
int _a1 = 0;
int _a2 = 0;
~AA() { cout << "~AA()" << endl; }
};
int main()
{
gxy::shared_ptr<AA> p(new AA);
const size_t n = 100000;
mutex mtx;
auto func = [&]()
{
// 智能指针拷贝 ++计数
for (size_t i = 0; i < n; ++i)
{
gxy::shared_ptr<AA> copy(p); // 拷贝时引用计数+1(线程安全)
unique_lock<mutex> lk(mtx);
copy->_a1++;
copy->_a2++;
}
};
thread t1(func);
thread t2(func);
t1.join();
t2.join();
cout << p->_a1 << endl; // 200000
cout << p->_a2 << endl; // 200000
cout << p.use_count() << endl; // 1
return 0;
}
-
shared_ptr 的线程安全:引用计数的 ++ / -- 是线程安全的(因为用了 atomic),多个线程同时拷贝 shared_ptr 不会错乱
-
但指向的资源不是线程安全的:_a1++、_a2++ 不是原子操作,必须加 mutex / lock 保护,否则结果会少算
-
为什么要写 gxy::shared_ptr<AA> copy(p);:每循环一次都拷贝一次智能指针,保证在线程工作期间,对象不会被释放,更安全、更规范
7. C++11 和 Boost 中智能指针的关系
Boost 库:C++ 标准库的先驱,提供了 scoped_ptr/shared_ptr/weak_ptr 等实现
版本演进:
C++98:auto_ptr(设计缺陷)
Boost:scoped_ptr/scoped_array/shared_ptr/shared_array/weak_ptr
C++ TR1:引入 shared_ptr(非标准)
C++11:引入 unique_ptr/shared_ptr/weak_ptr,其中 unique_ptr 对应 Boost scoped_ptr,实现参考 Boost
8. 内存泄漏
8.1 什么是内存泄漏
定义:程序未释放已不再使用的内存,失去对内存的控制,造成资源浪费。
原因:忘记 delete、异常导致 delete 未执行、设计错误等
危害:
短期程序:影响小,进程结束后内存回收
长期程序(操作系统、后台服务):内存不断减少,响应变慢,最终卡死
代码示例:new 大块内存不释放
cpp
int main()
{
// 申请 1GB 空间,不释放
char* ptr = new char[1024 * 1024 * 1024];
cout << (void*)ptr << endl;
return 0;
}
-
进程结束,操作系统会自动回收所有内存:即使你不 delete[],程序退出后系统也会回收,不会造成"系统内存泄漏"
-
危害:只有在长期运行不退出的程序(服务器、后台程序)才会产生内存泄漏,短期小程序跑一次就退,完全没问题
-
为什么可以这么写?现代操作系统:进程资源独立,进程销毁 → 全部虚拟内存回收
8.2 如何检测内存泄漏
Linux:valgrind、AddressSanitizer 等工具
Windows:VLD(Visual Leak Detector)等第三方工具
8.3 如何避免内存泄漏
- 事前预防:
良好编码规范:new 与 delete 匹配;优先使用智能指针(RAII)管理资源; 特殊场景自定义 RAII 类
- 事后查错:定期用内存泄漏检测工具排查;项目上线前重点检测
9. 总结
RAII 是智能指针的核心思想,利用对象生命周期自动管理资源
unique_ptr:独占所有权,高效无额外开销,适合不需要共享的场景
shared_ptr:共享所有权,引用计数实现,适合多对象共享资源
weak_ptr:弱引用,解决 shared_ptr 循环引用问题
智能指针从根源解决异常安全和内存泄漏问题,是现代 C++ 资源管理的首选
智能指针不止是语法糖,更是C++资源管理思想的体现。把内存交给对象生命周期去托管,代码更简洁,程序更安全,这正是现代C++的魅力所在。