目录:
1. 智能指针的使用
只要是堆上 new 出来的资源,不想手动管理 delete,优先考虑智能指针;能用unique_ptr就不用shared_ptr。
int main()
{
try
{
Func();
}
catch (const char* errmsg)
{
cout << errmsg << endl;
}
catch (const std::exception& e)
{
cout << e.what() << endl;
}
catch (...)
{
cout << "未知异常" << endl;
}
return 0;
}
void Func()
{
int* array1 = new int[10];
int* array2 = new int[10]; // ⚠️重点:这一行有可能抛出异常!
try
{
int len, time;
cin >> len >> time;
cout << Divide(len, time) << endl; //除0抛异常
}
catch (...)
{
//释放两块内存,重新抛出异常
delete[] array1;
delete[] array2;
throw;
}
//正常执行路径释放
delete[] array1;
delete[] array2;
}
两个场景分析:
场景 1:Divide()函数抛出异常(除 0)
进入catch(...),执行delete[] array1、array2,内存正常释放。 👉 这个场景看起来没问题。
场景 2:int* array2 = new int[10]; 这里直接抛出异常(内存分配失败)
new分配内存失败,会抛出bad_alloc异常!
-
array1 = new int[10]成功,内存分配出来; -
执行 array2 的 new 时抛异常;
-
函数立刻终止,直接跳出 Func;
-
还没有进入 try 块!catch 捕获不到!
-
array1指向的内存没有释放 → 内存泄漏!
使用 unique_ptr 改造:
#include <memory>
void Func()
{
// 全部用智能指针管理
std::unique_ptr<int[]> array1 = std::make_unique<int[]>(10);
std::unique_ptr<int[]> array2 = std::make_unique<int[]>(10);
int len, time;
cin >> len >> time;
cout << Divide(len, time) << endl;
// 不需要任何try-catch手动释放!
}
分析两种异常情况
- array2 构造时 new 抛异常
array1是局部栈对象,已经构造完成。发生异常,函数退出,array1 自动调用析构释放内存。 - Divide 函数抛异常 函数提前退出,
array1、array2都是局部对象,离开作用域自动析构,两块内存全部释放。
2. RAII和智能指针
RAII 全称:Resource Acquisition Is Initialization,资源获取即初始化。 它是 C++ 管理资源最核心的设计思想。
核心思想一句话概括:
把资源交给局部对象托管:对象构造时获取资源,对象析构时释放资源;依靠对象生命周期,保证资源一定会释放。

智能指针就是应用 RAII 思想封装裸指针的类模板。 只靠 RAII 还不够,我们希望它用起来像普通指针,所以还要重载运算符模拟指针行为。
两大关键点:
- 遵循 RAII:构造接收裸指针,析构执行
delete自动释放内存; - 重载运算符:
operator*、operator->、operator[],让智能指针拥有和裸指针一样的访问语法。
简易智能指针示例:
template<class T>
class SmartPtr
{
public:
// 构造函数:获取资源,接收new出来的裸指针(RAII第一步)
SmartPtr(T* ptr)
:_ptr(ptr)
{}
// 析构函数:释放资源(RAII核心)
~SmartPtr()
{
cout << "delete[] " << _ptr << endl;
delete[] _ptr;
}
// 重载解引用 *sp
T& operator*()
{
return *_ptr;
}
// 重载箭头 ->sp
T* operator->()
{
return _ptr;
}
// 重载下标 [] ,支持数组访问 sp[i]
T& operator[](size_t i)
{
return _ptr[i];
}
private:
T* _ptr; // 私有成员:封装裸指针
};
template<class T>类模板:支持任意类型指针int*、自定义类型*;- 私有成员
T* _ptr:封装裸指针,资源保存在对象内部; - 构造函数:接收外部
new出来的地址,完成资源获取; - 析构函数:对象销毁自动执行
delete[],自动释放堆内存; - 三个运算符重载:对外提供和原生指针一模一样的使用方式。
3. C++标准库智能指针的使用
智能指针概览:
C++ 标准库提供了多种智能指针,除了 weak_ptr 外,它们都符合 RAII 行为。它们的主要区别在于拷贝时的思路不同:
-
auto_ptr(C++98):拷贝时转移管理权(已废弃)。 -
unique_ptr(C++11):独占所有权,不支持拷贝,只支持移动。 -
shared_ptr(C++11):共享所有权,基于引用计数。 -
weak_ptr(C++11):弱引用,不控制生命周期,用于解决循环引用问题。
3.1 auto_ptr
auto_ptr 是 C++98 设计出来的智能指针。它的特点是:拷贝时会把被拷贝对象的资源管理权转移给拷贝对象。
致命缺陷:
这是一个非常糟糕的设计。当发生拷贝时,原对象(被拷贝者)会被置空(悬空)。如果后续继续访问原对象,就会导致程序崩溃。
代码示例:
#include <iostream>
#include <memory>
using namespace std;
struct Date {
int _year;
Date(int year = 1) : _year(year) {}
~Date() { cout << "~Date()" << endl; }
};
int main() {
auto_ptr<Date> ap1(new Date);
// 拷贝时,管理权限转移,被拷贝对象 ap1 悬空
auto_ptr<Date> ap2(ap1);
// 空指针访问,ap1 对象已经悬空,下面代码会崩溃
// ap1->_year++;
return 0;
}
3.2 unique_ptr
unique_ptr 翻译过来是“唯一指针”。它的特点非常鲜明:不支持拷贝,只支持移动。
-
适用场景: 如果不需要拷贝的场景,非常建议使用它。
-
移动语义: 虽然不支持拷贝,但支持
std::move移动。注意,移动后原对象也会悬空,所以使用移动要谨慎。
示例:
#include <iostream>
#include <memory>
using namespace std;
struct Date {
int _year;
Date(int year = 1) : _year(year) {}
~Date() { cout << "~Date()" << endl; }
};
int main() {
unique_ptr<Date> up1(new Date);
// 不支持拷贝
// unique_ptr<Date> up2(up1); // 编译报错
// 支持移动,但是移动后 up1 也悬空
unique_ptr<Date> up3(move(up1));
return 0;
}
3.3 shared_ptr
shared_ptr 是“共享指针”。它的特点是支持拷贝,也支持移动。底层是通过引用计数的方式实现的。
-
机制: 每当有一个新的
shared_ptr指向同一块资源,引用计数 +1;每当一个shared_ptr析构,引用计数 -1。当引用计数为 0 时,释放资源。 -
适用场景: 如果需要拷贝的场景,就需要使用它。
示例:
int main() {
shared_ptr<Date> sp1(new Date);
// 支持拷贝,引用计数增加
shared_ptr<Date> sp2(sp1);
shared_ptr<Date> sp3(sp2);
cout << sp1.use_count() << endl; // 输出 3
sp1->_year++;
cout << sp1->_year << endl; // 输出 2
cout << sp2->_year << endl; // 输出 2
cout << sp3->_year << endl; // 输出 2
// 支持移动,但是移动后 sp1 也悬空
shared_ptr<Date> sp4(move(sp1));
return 0;
}
3.4 weak_ptr
weak_ptr 是“弱指针”。它完全不同于上面的智能指针。
-
特点: 不支持 RAII,意味着不能用它直接管理资源。它不增加引用计数。
-
产生本质: 是为了解决
shared_ptr的循环引用导致内存泄漏的问题。
weak_ptr 的核心价值:通常发生在两个对象互相持有对方的 shared_ptr 时,导致引用计数永远无法归零。

错误版本:
#include <iostream>
#include <memory>
struct Node {
int val;
std::shared_ptr<Node> next; // shared_ptr互相引用
Node(int v) : val(v) {
std::cout << "构造 Node " << val << "\n";
}
~Node() {
std::cout << "析构 Node " << val << "\n";
}
};
int main() {
auto a = std::make_shared<Node>(1);
auto b = std::make_shared<Node>(2);
a->next = b;
b->next = a; // 循环引用!
// main函数结束,a、b离开作用域
// 但是a、b内部互相持有shared_ptr,引用计数 != 0
// 析构函数不会调用 → 内存泄漏!
return 0;
}
weak_ptr 修复版本:
#include <iostream>
#include <memory>
struct Node {
int val;
std::weak_ptr<Node> next; // weak_ptr 弱指针,不增加引用计数
Node(int v) : val(v) {
std::cout << "构造 Node " << val << "\n";
}
~Node() {
std::cout << "析构 Node " << val << "\n";
}
};
int main() {
auto a = std::make_shared<Node>(1);
auto b = std::make_shared<Node>(2);
a->next = b;
b->next = a;
// weak_ptr使用:lock() 尝试提升为shared_ptr
if (auto sp = a->next.lock()) {
std::cout << "a 的 next 值:" << sp->val << "\n";
}
// main结束,a,b销毁;weak_ptr不影响计数,对象正常释放
return 0;
}
3.5 make_shared
shared_ptr 除了支持用指向资源的指针构造,还支持 make_shared 用初始化资源对象的值直接构造。
template <class T, class... Args>
shared_ptr<T> make_shared(Args&&... args);
基本用法:
#include <memory>
// 直接使用构造函数
std::shared_ptr<Foo> p1(new Foo(1, 2));
// 使用 make_shared(推荐)
auto p2 = std::make_shared<Foo>(1, 2);
make_shared 将参数完美转发给 Foo 的构造函数,返回值是一个 shared_ptr<Foo>。
优势:
-
内存分配次数少: 直接构造
shared_ptr需要两次内存分配(对象本身 + 控制块),而make_shared只分配一次(对象和控制块在同一块内存)。 -
异常安全: 避免因参数求值顺序导致的内存泄漏。
int main() {
// 直接构造
shared_ptr<Date> sp1(new Date(2024, 9, 11));
// 使用 make_shared 构造
shared_ptr<Date> sp2 = make_shared<Date>(2024, 9, 11);
// 配合 auto 使用更简洁
auto sp3 = make_shared<Date>(2024, 9, 11);
return 0;
}
3.6 explicit 修饰构造函数
shared_ptr 和 unique_ptr 的构造函数都使用了 explicit 修饰。这是为了防止普通指针隐式类型转换成智能指针对象。
#include <iostream>
#include <memory>
struct Date {
int _year;
Date(int year = 1) : _year(year) {}
~Date() { std::cout << "~Date()" << std::endl; }
};
void takeSmartPtr(std::shared_ptr<Date> sp) {
std::cout << "管理权已移交,当前引用计数: " << sp.use_count() << std::endl;
}
int main() {
// 错误:禁止隐式转换 (编译报错)
// shared_ptr<Date> sp1 = new Date();
// takeSmartPtr(new Date());
// 正确:必须显式调用构造函数
std::shared_ptr<Date> sp2(new Date());
takeSmartPtr(sp2);
// 正确:使用 make_shared (更加推荐)
auto sp3 = std::make_shared<Date>();
takeSmartPtr(sp3);
return 0;
}
4. 删除器
智能指针析构时默认是进行 delete 释放资源。这就意味着,如果不是 new 出来的资源,交给智能指针管理,析构时就会崩溃。
为了解决这个问题,智能指针支持在构造时给定一个删除器。删除器本质是一个可调用对象(仿函数、函数指针、Lambda),在智能指针析构时会调用它来释放资源。
4.1 数组的特化版本
因为 new[] 经常使用,为了简洁,unique_ptr 和 shared_ptr 都特化了一份 [] 的版本。这样在析构时就会自动调用 delete[]。
// 如果没有特化,直接这样写会崩溃(因为默认 delete 不匹配 new[])
// unique_ptr<Date> up1(new Date[10]);
// 使用特化版本,管理 new[] 的资源
unique_ptr<Date[]> up1(new Date[5]);
shared_ptr<Date[]> sp1(new Date[5]);
4.2 自定义删除器
unique_ptr 和 shared_ptr 支持删除器的方式有所不同:
-
unique_ptr:是在类模板参数中支持的。 -
shared_ptr:是在构造函数参数中支持的。
方法1:
template<class T>
class DeleteArray {
public:
void operator()(T* ptr) {
delete[] ptr;
}
};
// unique_ptr 需要将删除器类型作为模板参数
unique_ptr<Date, DeleteArray<Date>> up2(new Date[5]);
// shared_ptr 只需要在构造函数中传入删除器对象
shared_ptr<Date> sp2(new Date[5], DeleteArray<Date>());
方法2:
// 函数指针做删除器
template<class T>
void DeleteArrayFunc(T* ptr) {
delete[] ptr;
}
// unique_ptr 传入函数指针类型
unique_ptr<Date, void(*)(Date*)> up3(new Date[5], DeleteArrayFunc<Date>);
// shared_ptr 直接传函数指针
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 直接传 Lambda
shared_ptr<Date> sp4(new Date[5], delArrOBJ);
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/xiaoxin_________/article/details/164885759



