一、智能指针的用途与传统指针的问题
问题场景:手动内存管理的隐患
int div() {
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
// 手动管理内存时,异常可能导致资源泄漏
void f() {
pair<string, string>* p1 = new pair<string, string>;
pair<string, string>* p2 = new pair<string, string>;
pair<string, string>* p3 = new pair<string, string>;
pair<string, string>* p4 = new pair<string, string>;
try {
div(); // 若此处抛出异常,p2/p3/p4未释放
} catch (...) {
delete p1; // 仅释放p1,其他指针泄漏
cout << "delete:" << p1 << endl;
throw;
}
delete p1; // 重复释放p1(若div未抛异常)
cout << "delete:" << p1 << endl;
}
核心问题:手动new/delete
在异常场景下易导致内存泄漏或重复释放。
二、自定义智能指针(基础实现)
RAII 思想:资源由对象生命周期管理
template<class T>
class SmartPtr {
public:
SmartPtr(T* ptr) : _ptr(ptr) {} // 构造时获取资源
~SmartPtr() { // 析构时释放资源
cout << "delete:" << _ptr << endl;
delete _ptr;
}
T& operator*() { return *_ptr; } // 行为像指针
T* operator->() { return _ptr; }
private:
T* _ptr;
};
// 使用示例:自动释放资源,无需手动delete
void f() {
SmartPtr<pair<string, string>> sp1(new pair<string, string>("1111", "22222"));
SmartPtr<pair<string, string>> sp2(new pair<string, string>);
SmartPtr<string> sp4(new string("xxxxx"));
cout << *sp4 << endl; // 解引用
cout << sp1->first << endl; // 指针访问
div(); // 若抛异常,sp1/sp2/sp4析构时自动释放
}
浅拷贝问题:未自定义赋值导致内存泄漏
//浅拷贝问题:SmartPtr未自定义赋值导致内存泄漏
int main() {
SmartPtr<string> sp1(new string("xxxxx"));
SmartPtr<string> sp2(new string("yyyyy"));
sp1 = sp2; // 浅拷贝:两个对象指向同一地址,析构时重复释放,或原资源未释放
return 0;
}
三、C++98 auto_ptr
(已过时,不推荐使用)
设计缺陷:管理权转移导致悬空指针
class A {
public:
A(int a = 0) : _a(a) { cout << "A(int a = 0)" << endl; }
~A() { cout << this << " ~A()" << endl; }
int _a;
};
int main() {
auto_ptr<A> ap1(new A(1));
auto_ptr<A> ap3(ap1); // 管理权转移:ap1._ptr置空,ap3获取资源
ap1->_a++; // 崩溃!ap1指向nullptr(悬空指针)
return 0;
}
auto_ptr
模拟实现(管理权转移逻辑)
template<class T>
class auto_ptr {
public:
auto_ptr(T* ptr) : _ptr(ptr) {}
~auto_ptr() { delete _ptr; }
// 拷贝构造时转移管理权
auto_ptr(auto_ptr<T>& ap) : _ptr(ap._ptr) { ap._ptr = nullptr; }
T& operator*() {
return *_ptr;
}
T* operator->() {
return _ptr;
}
private:
T* _ptr;
};
四、C++11 unique_ptr
(独占所有权,禁止拷贝)
设计原则:简单粗暴,禁止拷贝 / 赋值
int main() {
unique_ptr<A> up1(new A(1));
// unique_ptr<A> up3(up1); // 编译错误:拷贝构造被删除
// up1 = up2; // 编译错误:赋值运算符被删除
return 0;
}
模拟实现(删除拷贝构造和赋值运算符)
template<class T>
class unique_ptr {
public:
unique_ptr(T* ptr) : _ptr(ptr) {}
~unique_ptr() { delete _ptr; }
// 防拷贝:删除默认拷贝构造和赋值
unique_ptr(unique_ptr<T>& ap) = delete;
unique_ptr<T>& operator=(unique_ptr<T>& ap) = delete;
T& operator*() {
return *_ptr;
}
T* operator->() {
return _ptr;
}
private:
T* _ptr;
};
五、C++11 shared_ptr
(共享所有权,引用计数)
适用场景:需要拷贝 / 共享资源
int main() {
shared_ptr<A> sp1(new A(1));
shared_ptr<A> sp2(sp1); // 拷贝:引用计数+1
sp1->_a = 10; // sp2同步可见
cout << sp2->_a << endl; // 输出10
return 0;
}
模拟实现(引用计数核心逻辑)
// 类模板定义
template<class T>
class shared_ptr {
public:
// 构造函数:初始化指针和引用计数(初始为1)
shared_ptr(T* ptr = nullptr)
: _ptr(ptr)
, _pcount(new int(1)) {}
// 析构函数:引用计数-1,为0时释放资源和计数对象
~shared_ptr() {
if (--(*_pcount) == 0) {
std::cout << "delete:" << _ptr << endl;
delete _ptr;
delete _pcount;
}
}
// 重载*运算符:返回资源对象引用
T& operator*() {
return *_ptr;
}
// 重载->运算符:返回原始指针,支持成员访问
T* operator->() {
return _ptr;
}
// 拷贝构造函数:共享资源,引用计数+1
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) // 处理自赋值
return *this;
// 释放旧资源(若引用计数为0)
if (--(*_pcount) == 0) {
delete _ptr;
delete _pcount;
}
// 接管新资源,引用计数+1
_ptr = sp._ptr;
_pcount = sp._pcount;
++(*_pcount);
return *this;
}
// 获取当前引用计数(补充功能)
int use_count() const {
return *_pcount;
}
// 获取原始指针(补充功能)
T* get() const {
return _ptr;
}
private:
T* _ptr; // 指向管理的资源
int* _pcount; // 指向引用计数(每个资源独立,非静态)
};
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
中if (--(*_pcount) == 0)
的目的:这一步的目的是在进行赋值操作时,确保旧的资源能够被正确释放。当一个shared_ptr
对象被赋值为另一个shared_ptr
对象时,它需要先释放当前所管理的资源(如果这是最后一个引用),然后再接管新的资源。示例代码:int main() { shared_ptr<A> sp1(new A(1)); shared_ptr<A> sp2(new A(2)); // 现在 sp1 指向新的资源,旧的资源需要被释放 sp1 = sp2; cout << sp1.use_count() << endl; // 输出2 cout << sp2.use_count() << endl; // 输出2 return 0; }
在上述代码中,
sp1
最初管理一个A
对象,其引用计数为 1。当执行sp1 = sp2
时,sp1
需要先释放它当前管理的资源。通过--(*_pcount) == 0
的检查,如果引用计数变为 0,说明这是最后一个引用,就可以安全地删除_ptr
和_pcount
。-
2. shared_ptr类中的独立引用技术类型为什么必须是
int* _pcount
指针类型而不能是int
和static int
类型?-
不能用
int
类型的原因若使用
int
类型,每个shared_ptr
对象都会有一份独立的引用计数副本,而非多个shared_ptr
对象共享同一个引用计数。这就会致使在进行拷贝构造或者赋值操作时,引用计数无法正确更新,进而引发资源的多次释放或者资源泄漏问题。 -
不能用
static int
类型的原因static int
类型的变量会被该类的所有对象共享。也就是说,所有shared_ptr
对象都会使用同一个引用计数,这就无法对不同资源的引用计数进行独立管理。
-
循环引用问题:导致内存泄漏
struct Node {
A _val;
shared_ptr<Node> _next;
shared_ptr<Node> _prev;
};
int main() {
shared_ptr<Node> sp1(new Node); // sp1计数=1,sp1->_next=空
shared_ptr<Node> sp2(new Node); // sp2计数=1,sp2->_prev=空
sp1->_next = sp2; // 节点A的_next指向B,此时sp2的计数+1 → sp2计数=2
sp2->_prev = sp1; // 节点B的_prev指向A,此时sp1的计数+1 → sp1计数=2
return 0;//sp1析构:sp1计数-1 → 计数=1(未归零,不释放节点A); sp2析构:sp2计数-1 → 计数=1(未归零,不释放节点B)。
}
六、C++11 weak_ptr
(解决循环引用,不参与计数)
定位:专门解决 shared_ptr
循环引用的辅助类,不参与资源管理(非 RAII)
修改节点结构:使用 weak_ptr 替代 shared_ptr
// 修正:用weak_ptr定义非管理型引用
struct Node {
A _val;
weak_ptr<Node> _next; // weak_ptr不增加引用计数
weak_ptr<Node> _prev;
};
int main() {
shared_ptr<Node> sp1(new Node); // sp1计数1
shared_ptr<Node> sp2(new Node); // sp2计数1
cout << sp1.use_count() << endl; // 输出1(_next是weak_ptr,不影响计数)
cout << sp2.use_count() << endl; // 输出1
sp1->_next = sp2; // weak_ptr赋值,sp2计数不变(仍为1)
sp2->_prev = sp1; // weak_ptr赋值,sp1计数不变(仍为1)
cout << sp1.use_count() << endl; // 输出1(_prev是weak_ptr,未增加计数)
cout << sp2.use_count() << endl; // 输出1
// 结论:无循环引用,main结束时sp1/sp2计数降为0,资源正常释放
return 0;
}
- 核心特性:
- 不增加引用计数:仅弱引用资源,不影响
shared_ptr
的计数逻辑 - 观察资源生命周期:可判断资源是否存活,或临时获取
shared_ptr
- 配合
shared_ptr
使用:不能单独管理资源,必须依赖shared_ptr
存在
- 不增加引用计数:仅弱引用资源,不影响
weak_ptr 模拟实现(核心逻辑)
template<class T>
class weak_ptr {
public:
weak_ptr() :_ptr(nullptr) {} // 空构造
weak_ptr(const shared_ptr<T>& sp) :_ptr(sp.get()) {} // 从shared_ptr获取原始指针
weak_ptr<T>& operator=(const shared_ptr<T>& sp) { // 赋值时获取原始指针
_ptr = sp.get();
return *this;
}
T& operator*() { return *_ptr; } // 像指针一样解引用
T* operator->() { return _ptr; } // 像指针一样访问成员
private:
T* _ptr; // 存储shared_ptr管理的原始指针(弱引用,不控制生命周期)
};
七、定制删除器
1.定制删除器的需求场景
默认删除器的局限性(问题引入)
class A {
public:
A(int a = 0) : _a(a) {
cout << "A(int a = 0)" << endl;
}
~A() {
cout << this << " ~A()" << endl;
}
int _a;
};
int main() {
// 场景1:管理动态数组(默认delete导致未定义行为)
shared_ptr<A> sp1(new A[10]); // ❌ 错误!默认用delete释放数组,仅释放首元素,导致内存泄漏
// 场景2:管理非new分配的内存(如malloc)
shared_ptr<A> sp2((A*)malloc(sizeof(A))); // ❌ 错误!delete与malloc不匹配,导致程序崩溃
return 0;
}
核心问题:
shared_ptr
默认使用delete ptr
释放资源- 对 数组 需用
delete[]
,对 malloc 内存 需用free
,对 特殊资源(如文件句柄)需专用释放逻辑
2.定制删除器的实现方式
仿函数定制删除器(适用于数组释放)
template<class T>
struct DeleteArray {
void operator()(T* ptr) { // 重载()运算符,定义释放逻辑
delete[] ptr; // 正确释放数组内存
}
};
int main() {
// 传入仿函数删除器,解决数组释放问题
shared_ptr<A> sp1(new A[10], DeleteArray<A>());
// 传入Lambda表达式,解决malloc内存释放问题
shared_ptr<A> sp2((A*)malloc(sizeof(A)), [](A* ptr){free(ptr);});
// 自定义文件句柄释放(避免fclose被遗漏)
shared_ptr<FILE> sp3(fopen("Test.cpp", "r"),
[](FILE* ptr) { cout << "fclose:" << ptr << endl; fclose(ptr); });
return 0;
}
关键点:
shared_ptr
允许通过第二个参数传入删除器函数对象(仿函数或 Lambda),替代默认的delete
。- Lambda 表达式可直接捕获上下文变量,简化自定义释放逻辑(如文件句柄释放时打印日志)。
3.自定义 shared_ptr 类:实现定制删除器的底层原理
带删除器的 shared_ptr 类模板(关键修改点)
#include <functional> // 引入function包装器
template<class T>
class shared_ptr {
public:
// 构造函数(默认删除器:lambda表达式[](T* ptr){delete ptr;})
shared_ptr(T* ptr = nullptr)
: _ptr(ptr), _pcount(new int(1)), _del([](T* ptr) { delete ptr; }) {}
// 带定制删除器的构造函数(通过模板参数D接收任意可调用对象)
template<class D>
shared_ptr(T* ptr, D del)
: _ptr(ptr), _pcount(new int(1)), _del(del) {}
~shared_ptr() {
if (--(*_pcount) == 0) { // 引用计数为0时释放资源
cout << "delete:" << _ptr << endl;
_del(_ptr); // 调用存储的删除器(关键!替代默认delete)
delete _pcount;
}
}
// ...其他成员函数(拷贝构造、赋值运算符等)...//
private:
T* _ptr; // 管理的原始指针
int* _pcount; // 引用计数
function<void(T*)> _del = [](T* ptr) { delete ptr; }; // 核心:用function包装器存储任意删除器,同时也要确保即使不传入删除器,也会使用默认的delete释放单个对象。
};
为什么需要 function 包装器?
- 问题:Lambda 表达式和仿函数是不同类型的可调用对象,直接用普通成员变量无法统一存储。
- 解决方案:
std::function
是一个类型擦除的包装器,可以统一存储任意可调用对象(函数指针、仿函数、Lambda),只要其签名符合void(T*)
。- 例如:
DeleteArray<A>()
(仿函数)和[](A* ptr){free(ptr);}
(Lambda)都会被function
转换为统一类型。
- 例如:
4.对比 unique_ptr 的定制删除器
unique_ptr 的删除器类型定义
template<class T, class D = default_delete<T>> class unique_ptr;
- 特点:
- 删除器类型
D
是模板参数,直接影响unique_ptr
的类型(如unique_ptr<A, DeleteArray<A>>
)。 - 不支持引用计数,删除器直接存储在
unique_ptr
对象内,而非共享。
- 删除器类型
- 优势:无需
function
包装器,直接通过模板参数支持定制删除器,效率更高。