C++ 智能指针:从异常痛点到内存安全的完美解决方案
C++智能指针是解决内存泄漏和资源管理难题的核心工具。文章从RAII思想出发,详细剖析了标准库中的四种智能指针:auto_ptr(已弃用)、unique_ptr、shared_ptr和weak_ptr。重点讲解了shared_ptr的引用计数机制及其线程安全特性,指出其仅保证引用计数安全,资源访问仍需手动加锁。针对循环引用问题,介绍了weak_ptr的解决方案。文章还对比了不同智能指针的适用场景,
一、开篇:C++ 程序员的 “噩梦”—— 异常与内存泄漏
如果你是一名 C++ 开发者,一定对 “内存管理” 这个词又爱又恨。C++ 赋予我们直接操作内存的权力,但这份权力也带来了巨大的责任 —— 忘记释放内存会导致泄漏,重复释放会导致崩溃,而异常的出现,更是让内存管理的难度直线上升。
想象这样一个场景:你用new
分配了一块内存,计划在使用后用delete
释放。可偏偏在new
和delete
之间,程序抛出了一个未捕获的异常,正常的代码流程被打断,delete
语句永远没有机会执行。久而久之,这些 “被遗忘” 的内存会不断占用系统资源,对于服务器、操作系统这类长期运行的程序来说,最终会导致性能下降甚至卡死。
这不是危言耸听,我们先看一段真实的 “问题代码”,感受下异常是如何导致内存泄漏的:
#include <iostream>
using namespace std;
// 除法函数:当除数为0时抛出异常
double Divide(int a, int b) {
if (b == 0) {
// 抛出异常:中断当前流程,跳转到最近的catch块
throw "Divide by zero condition!";
}
return static_cast<double>(a) / static_cast<double>(b);
}
void Func() {
// 动态分配两个int数组(各10个元素)
int* array1 = new int[10];
int* array2 = new int[10];
int len, time;
cout << "请输入len和time:";
cin >> len >> time;
// 调用Divide:若time=0,会抛出异常
cout << Divide(len, time) << endl;
// 正常情况下的释放逻辑
cout << "释放array1:" << array1 << endl;
delete[] array1;
cout << "释放array2:" << array2 << endl;
delete[] array2;
}
int main() {
try {
Func();
}
// 捕获const char*类型的异常
catch (const char* errmsg) {
cout << "捕获异常:" << errmsg << endl;
}
// 捕获其他标准异常
catch (const exception& e) {
cout << "捕获异常:" << e.what() << endl;
}
// 捕获所有未匹配的异常
catch (...) {
cout << "捕获未知异常" << endl;
}
return 0;
}
1.1 异常的 “破坏力”:打断代码流程
我们运行这段代码,若输入10 0
(除数为 0),会发生什么?
Func()
中,array1
和array2
成功用new
分配内存;- 调用
Divide(10, 0)
,触发b==0
,抛出异常"Divide by zero condition!"
; - 异常会立即中断当前执行流,
Func()
中Divide
之后的delete[]
语句永远不会执行; - 异常被
main()
中的catch (const char* errmsg)
捕获,打印错误信息; - 程序结束,但
array1
和array2
指向的内存从未被释放 —— 内存泄漏发生了!
1.2 手动解决?代码会变得 “丑陋且脆弱”
为了避免泄漏,你可能会想到 “在try-catch
中手动释放内存”,比如这样修改Func()
:
void Func() {
int* array1 = nullptr;
int* array2 = nullptr;
try {
array1 = new int[10];
array2 = new int[10]; // 若这里抛异常,array1已分配但未释放
int len, time;
cin >> len >> time;
cout << Divide(len, time) << endl;
// 正常释放
delete[] array1;
delete[] array2;
}
catch (...) {
// 异常时释放已分配的内存
if (array1) {
delete[] array1;
cout << "异常时释放array1:" << array1 << endl;
}
if (array2) {
delete[] array2;
cout << "异常时释放array2:" << array2 << endl;
}
throw; // 重新抛出异常,让上层处理
}
}
但这种方案有两个致命问题:
- 代码冗余:每分配一个资源,都要在
catch
中手动判断并释放,资源越多,代码越臃肿; - 漏洞隐患:若
new int[10]
本身抛异常(比如内存不足),array1
已分配但array2
未分配,此时catch
中需要精准判断哪些资源已分配,极易出错; - 扩展性差:若后续增加更多资源(如文件指针、网络连接),需要同步修改
catch
中的释放逻辑,维护成本极高。
显然,手动处理异常场景下的资源释放,既不优雅也不可靠。我们需要一种 “一劳永逸” 的方案 —— 这就是智能指针诞生的背景。
二、智能指针的 “基石”:RAII 思想
在介绍智能指针之前,我们必须先理解它的核心设计理念 ——RAII(Resource Acquisition Is Initialization,资源获取即初始化)。RAII 的本质是:用对象的生命周期管理资源。
2.1 RAII 的核心逻辑
RAII 的核心思想可以拆解为 3 步:
- 资源获取即初始化:在对象构造时,获取需要管理的资源(如
new
内存、fopen
文件、lock
互斥锁); - 资源随对象生存:只要对象存在,资源就有效,我们可以通过对象访问资源;
- 资源随对象销毁:当对象生命周期结束(如离开作用域、被 delete),调用对象的析构函数,自动释放资源。
由于 C++ 的析构函数一定会被执行(即使发生异常,栈上的对象也会被自动销毁,即 “栈展开”),RAII 能确保资源无论在正常流程还是异常流程下,都能被安全释放。
2.2 一个简单的 RAII 示例:管理动态数组
我们来实现一个管理int
数组的 RAII 类ArrayManager
,感受下 RAII 的魅力:
#include <iostream>
using namespace std;
class ArrayManager {
public:
// 1. 构造时获取资源(new数组)
explicit ArrayManager(size_t size)
: _ptr(new int[size])
, _size(size) {
cout << "ArrayManager:分配" << _size << "个int的数组,地址:" << _ptr << endl;
}
// 3. 析构时释放资源(delete[]数组)
~ArrayManager() {
if (_ptr) {
delete[] _ptr;
cout << "ArrayManager:释放数组,地址:" << _ptr << endl;
}
}
// 2. 提供访问资源的接口(像普通数组一样使用)
int& operator[](size_t index) {
if (index >= _size) {
throw out_of_range("Array index out of range");
}
return _ptr[index];
}
// 禁止拷贝和赋值(避免资源重复释放,后续会讲)
ArrayManager(const ArrayManager&) = delete;
ArrayManager& operator=(const ArrayManager&) = delete;
private:
int* _ptr; // 管理的资源(动态数组)
size_t _size;// 数组大小
};
现在,我们用ArrayManager
改造之前的Func()
,看看异常场景下是否还会泄漏:
double Divide(int a, int b) {
if (b == 0) {
throw "Divide by zero condition!";
}
return static_cast<double>(a) / static_cast<double>(b);
}
void Func() {
// 用RAII对象管理资源,构造时自动new
ArrayManager array1(10);
ArrayManager array2(10);
// 像普通数组一样赋值
for (size_t i = 0; i < 10; ++i) {
array1[i] = i;
array2[i] = 10 - i;
}
int len, time;
cout << "请输入len和time:";
cin >> len >> time;
cout << Divide(len, time) << endl;
// 无需手动delete!对象离开作用域时析构会自动释放
}
int main() {
try {
Func();
}
catch (const char* errmsg) {
cout << "捕获异常:" << errmsg << endl;
}
catch (const out_of_range& e) {
cout << "捕获异常:" << e.what() << endl;
}
catch (...) {
cout << "捕获未知异常" << endl;
}
return 0;
}
2.3 RAII 的优势:异常下的 “自动保护”
运行代码,输入10 0
(触发异常),输出如下:
可以看到:
- 即使
Divide
抛出异常,array1
和array2
作为栈上对象,会在Func()
退出时被自动销毁; - 析构函数被调用,数组内存被成功释放,没有内存泄漏;
- 我们无需手动写任何释放逻辑,代码简洁且安全。
2.4 从 RAII 到智能指针:让 “资源管理者” 像指针一样
ArrayManager
虽然解决了资源释放问题,但它只能管理int
数组,不够通用。如果我们需要管理double
数组、自定义类对象(如Date
),难道要重复写多个 RAII 类吗?
当然不用!我们可以将 RAII 思想模板化,并让它模拟普通指针的行为(重载*
、->
等运算符)—— 这就是智能指针的本质。
简单来说:智能指针 = RAII 模板 + 指针行为模拟。
三、自定义智能指针:理解底层实现
在学习 C++ 标准库的智能指针之前,我们先手动实现一个简单的智能指针SmartPtr
,掌握其核心逻辑。
3.1 目标:实现一个通用的智能指针
我们的SmartPtr
需要满足以下需求:
- 模板化:支持管理任意类型的资源(如
int
、Date
、数组); - RAII 特性:构造时接收
new
的指针,析构时delete
资源; - 指针行为:重载
*
(解引用)、->
(访问成员)、[]
(数组访问),支持if
判断是否为空; - 避免资源重复释放:禁止拷贝和赋值(后续会优化为支持共享)。
3.2 实现SmartPtr
#include <iostream>
using namespace std;
// 模板参数T:智能指针管理的资源类型
template <typename T>
class SmartPtr {
public:
// 1. 构造:获取资源(接收new的指针)
explicit SmartPtr(T* ptr = nullptr)
: _ptr(ptr) {
if (_ptr) {
cout << "SmartPtr:管理资源,地址:" << _ptr << endl;
} else {
cout << "SmartPtr:空指针" << endl;
}
}
// 2. 析构:释放资源(delete或delete[])
~SmartPtr() {
if (_ptr) {
// 注意:这里默认用delete,若管理数组需要特殊处理(后续讲删除器)
delete _ptr;
cout << "SmartPtr:释放资源,地址:" << _ptr << endl;
}
}
// 3. 模拟指针行为:解引用(*)
T& operator*() const {
if (!_ptr) {
throw runtime_error("SmartPtr:解引用空指针");
}
return *_ptr;
}
// 4. 模拟指针行为:访问成员(->)
T* operator->() const {
if (!_ptr) {
throw runtime_error("SmartPtr:访问空指针成员");
}
return _ptr;
}
// 5. 模拟指针行为:数组访问([])
T& operator[](size_t index) const {
if (!_ptr) {
throw runtime_error("SmartPtr:数组访问空指针");
}
// 注意:这里假设T是数组类型,实际需要特化(后续讲)
return _ptr[index];
}
// 6. 支持if判断:是否管理资源(重载bool类型转换)
explicit operator bool() const {
return _ptr != nullptr;
}
// 7. 禁止拷贝和赋值(避免资源重复释放)
SmartPtr(const SmartPtr<T>&) = delete;
SmartPtr<T>& operator=(const SmartPtr<T>&) = delete;
// (可选)获取原始指针(谨慎使用,避免绕过智能指针管理)
T* get() const {
return _ptr;
}
private:
T* _ptr; // 管理的原始资源指针
};
// 特化SmartPtr<T[]>:支持管理new[]分配的数组(析构用delete[])
template <typename T>
class SmartPtr<T[]> {
public:
explicit SmartPtr(T* ptr = nullptr)
: _ptr(ptr) {
if (_ptr) {
cout << "SmartPtr<T[]>:管理数组资源,地址:" << _ptr << endl;
} else {
cout << "SmartPtr<T[]>:空指针" << endl;
}
}
~SmartPtr() {
if (_ptr) {
delete[] _ptr; // 数组用delete[]释放
cout << "SmartPtr<T[]>:释放数组资源,地址:" << _ptr << endl;
}
}
T& operator[](size_t index) const {
if (!_ptr) {
throw runtime_error("SmartPtr<T[]>:数组访问空指针");
}
return _ptr[index];
}
explicit operator bool() const {
return _ptr != nullptr;
}
SmartPtr(const SmartPtr<T[]>&) = delete;
SmartPtr<T[]>& operator=(const SmartPtr<T[]>&) = delete;
T* get() const {
return _ptr;
}
private:
T* _ptr;
};
3.3 测试SmartPtr
:解决异常泄漏问题
我们用SmartPtr
改造最初的异常示例,看看效果:
3.3.1 测试 1:管理普通对象(如Date
)
先定义一个Date
类,方便观察构造和析构:
class Date {
public:
Date(int year = 1, int month = 1, int day = 1)
: _year(year), _month(month), _day(day) {
cout << "Date(" << _year << "," << _month << "," << _day << ") 构造" << endl;
}
~Date() {
cout << "Date(" << _year << "," << _month << "," << _day << ") 析构" << endl;
}
void Print() const {
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
然后测试异常场景:
double Divide(int a, int b) {
if (b == 0) {
throw "Divide by zero condition!";
}
return static_cast<double>(a) / static_cast<double>(b);
}
void Func() {
// 用SmartPtr管理Date对象(new Date)
SmartPtr<Date> sp1(new Date(2024, 5, 20));
// 用SmartPtr<T[]>管理int数组(new int[10])
SmartPtr<int[]> sp2(new int[10]);
// 像普通指针一样使用
sp1->Print(); // 访问成员:等价于 (*sp1).Print()
(*sp1).Print(); // 解引用
// 数组赋值
for (size_t i = 0; i < 10; ++i) {
sp2[i] = i;
}
// 触发异常
int len = 10, time = 0;
cout << Divide(len, time) << endl;
// 无需手动delete!
}
int main() {
try {
Func();
}
catch (const char* errmsg) {
cout << "捕获异常:" << errmsg << endl;
}
catch (const runtime_error& e) {
cout << "捕获异常:" << e.what() << endl;
}
catch (...) {
cout << "捕获未知异常" << endl;
}
return 0;
}
3.3.2 运行结果:资源被自动释放
结果分析:
- 即使
Divide
抛出异常,sp1
和sp2
在Func()
退出时被析构; sp1
的析构调用delete
,触发Date
的析构;sp2
(SmartPtr<int[]>
特化版)的析构调用delete[]
,释放数组;- 没有内存泄漏,完全解决了异常场景下的资源释放问题!
3.4 SmartPtr
的不足:不支持共享
我们的SmartPtr
禁止拷贝和赋值,这在 “多个指针需要共享同一个资源” 的场景下就不适用了。比如:
void Func() {
SmartPtr<Date> sp1(new Date(2024, 5, 20));
// SmartPtr<Date> sp2 = sp1; // 编译错误:拷贝构造被delete
}
如果我们需要sp1
和sp2
都指向同一个Date
对象,并且只有当最后一个智能指针析构时才释放Date
,该怎么办?
这就需要引入引用计数机制 —— 这也是shared_ptr
的核心原理。
四、C++ 标准库智能指针:从auto_ptr
到weak_ptr
C++ 标准库(<memory>
头文件)提供了 4 种智能指针:auto_ptr
(C++98,已弃用)、unique_ptr
(C++11)、shared_ptr
(C++11)、weak_ptr
(C++11)。它们的核心都是 RAII,但在 “资源共享” 和 “使用场景” 上有本质区别。
4.1 被时代抛弃的auto_ptr
:C++98 的妥协产物
auto_ptr
是 C++98 标准中唯一的智能指针,它试图解决 “异常下的内存泄漏”,但设计上存在严重缺陷,C++11 后已被明确弃用(不推荐使用)。
4.1.1 auto_ptr
的核心逻辑:拷贝时 “转移资源管理权”
auto_ptr
的设计思路是:同一时间,只有一个auto_ptr
能管理资源。当你拷贝一个auto_ptr
时,原auto_ptr
会失去资源管理权(变为空指针),新auto_ptr
接管资源。
4.1.2 代码示例:auto_ptr
的 “坑”
#include <iostream>
#include <memory> // 标准库智能指针头文件
using namespace std;
class Date {
public:
Date(int year = 1, int month = 1, int day = 1)
: _year(year), _month(month), _day(day) {
cout << "Date(" << _year << "," << _month << "," << _day << ") 构造" << endl;
}
~Date() {
cout << "Date(" << _year << "," << _month << "," << _day << ") 析构" << endl;
}
void Print() const {
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main() {
// 用auto_ptr管理Date对象
auto_ptr<Date> ap1(new Date(2024, 5, 20));
ap1->Print(); // 正常访问
// 拷贝ap1到ap2:ap1失去管理权,变为空指针
auto_ptr<Date> ap2 = ap1;
ap2->Print(); // 正常访问(ap2接管资源)
// 危险!ap1已为空,解引用会崩溃
// ap1->Print(); // 运行时错误:访问空指针
return 0;
}
4.1.3 auto_ptr
的致命问题
- 拷贝后原对象悬空:拷贝后原
auto_ptr
变为空,若后续再访问原对象,会导致未定义行为(崩溃); - 不支持容器存储:若将
auto_ptr
存入vector
,容器在扩容时会拷贝元素,导致原auto_ptr
悬空,后续访问容器元素会崩溃; - 逻辑反直觉:拷贝操作本应是 “复制一份资源” 或 “共享资源”,但
auto_ptr
的拷贝却是 “转移资源”,不符合程序员的直觉。
因此,C++11 及以后,永远不要使用auto_ptr
,改用unique_ptr
或shared_ptr
。
4.2 独占资源的unique_ptr
:auto_ptr
的 “正确替代品”
unique_ptr
是 C++11 为 “独占资源” 场景设计的智能指针,它的核心特点是:禁止拷贝,支持移动,确保同一时间只有一个unique_ptr
管理资源。
4.2.1 unique_ptr
的核心特性
- 独占性:通过删除拷贝构造和拷贝赋值运算符,禁止拷贝
unique_ptr
; - 支持移动:允许通过
std::move
转移资源管理权(转移后原unique_ptr
变为空); - 高效轻量:无额外开销(不维护引用计数),性能接近普通指针;
- 支持数组:提供
unique_ptr<T[]>
特化版本,析构时自动用delete[]
释放数组; - 支持删除器:可自定义资源释放逻辑(如管理文件指针、互斥锁)。
4.2.2 代码示例 1:unique_ptr
的基本使用
#include <iostream>
#include <memory>
using namespace std;
class Date {
public:
Date(int year = 1, int month = 1, int day = 1)
: _year(year), _month(month), _day(day) {
cout << "Date(" << _year << "," << _month << "," << _day << ") 构造" << endl;
}
~Date() {
cout << "Date(" << _year << "," << _month << "," << _day << ") 析构" << endl;
}
void Print() const {
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main() {
// 1. 管理普通对象
unique_ptr<Date> up1(new Date(2024, 5, 20));
up1->Print(); // 正常访问
// 2. 禁止拷贝:编译错误(拷贝构造被delete)
// unique_ptr<Date> up2 = up1;
// unique_ptr<Date> up3(up1);
// 3. 支持移动:用std::move转移管理权
unique_ptr<Date> up4 = move(up1);
up4->Print(); // up4接管资源
if (!up1) { // 移动后up1为空
cout << "up1已为空" << endl;
}
// 4. 管理数组:使用unique_ptr<T[]>特化版
unique_ptr<int[]> up5(new int[10]);
for (size_t i = 0; i < 10; ++i) {
up5[i] = i; // 数组访问
}
// 5. 支持if判断:是否为空
if (up4) {
cout << "up4管理资源" << endl;
}
return 0;
}
运行结果:
4.2.3 代码示例 2:unique_ptr
的删除器
默认情况下,unique_ptr
用delete
释放资源(数组用delete[]
)。若资源不是用new
分配的(如fopen
的文件指针、malloc
的内存),需要自定义删除器(一个可调用对象,如函数、仿函数、lambda)。
案例 1:管理文件指针(fopen
/fclose
)
#include <iostream>
#include <memory>
#include <cstdio> // fopen, fclose
using namespace std;
// 自定义删除器:仿函数(释放文件指针)
struct FileDeleter {
void operator()(FILE* fp) const {
if (fp) {
fclose(fp);
cout << "FileDeleter:关闭文件,指针:" << fp << endl;
}
}
};
int main() {
// 用unique_ptr管理文件指针,指定删除器为FileDeleter
unique_ptr<FILE, FileDeleter> up_file(fopen("test.txt", "w"));
if (up_file) {
fprintf(up_file.get(), "Hello, unique_ptr!"); // get()获取原始指针
cout << "文件写入成功" << endl;
}
// 程序结束时,up_file析构,自动调用FileDeleter释放文件
return 0;
}
案例 2:用 lambda 作为删除器
#include <iostream>
#include <memory>
#include <cstdlib> // malloc, free
using namespace std;
int main() {
// 用malloc分配内存,自定义lambda删除器(用free释放)
unique_ptr<int, void(*)(int*)> up_malloc(
static_cast<int*>(malloc(sizeof(int) * 10)),
[](int* ptr) {
if (ptr) {
free(ptr);
cout << "lambda删除器:free内存,地址:" << ptr << endl;
}
}
);
if (up_malloc) {
up_malloc.get()[0] = 100; // 访问内存
cout << "up_malloc[0] = " << up_malloc.get()[0] << endl;
}
return 0;
}
4.2.4 unique_ptr
的适用场景
- 独占资源:资源只需一个所有者,无需共享(如函数返回动态分配的对象、管理局部动态数组);
- 高效场景:对性能要求高,不希望引用计数的额外开销;
- 容器元素:可存入
vector
等容器(需用move
转移所有权)。
4.3 支持共享的shared_ptr
:引用计数的艺术
shared_ptr
是 C++11 为 “共享资源” 场景设计的智能指针,它的核心是引用计数机制,允许多个shared_ptr
共享同一个资源,只有当最后一个shared_ptr
析构时,才释放资源。
4.3.1 shared_ptr
的核心原理:引用计数
引用计数的逻辑可以拆解为 5 步:
- 初始化:当创建一个
shared_ptr
时,会同时在堆上分配一个 “引用计数器”,初始值为 1(表示当前有 1 个shared_ptr
管理资源); - 拷贝:当拷贝一个
shared_ptr
(如sp2 = sp1
)时,两个shared_ptr
指向同一个资源,同时引用计数器加 1(count += 1
); - 赋值:当一个
shared_ptr
赋值给另一个(如sp3 = sp2
)时,先将原sp3
的引用计数器减 1(若减到 0 则释放原资源),再将sp2
的引用计数器加 1; - 析构:当一个
shared_ptr
析构时,引用计数器减 1(count -= 1
); - 释放资源:若引用计数器减到 0,说明当前
shared_ptr
是最后一个管理资源的对象,此时释放资源和引用计数器。
4.3.2 引用计数的示意图
// 初始:sp1管理资源,引用计数=1
shared_ptr<Date> sp1(new Date(2024,5,20));
资源(Date)<-- sp1
|
引用计数器(1)<--
// 拷贝:sp2 = sp1,引用计数=2
shared_ptr<Date> sp2 = sp1;
资源(Date)<-- sp1
|
sp2
|
引用计数器(2)<--
// 析构sp1:引用计数=1,不释放资源
sp1.~shared_ptr();
资源(Date)<-- sp2
|
引用计数器(1)<--
// 析构sp2:引用计数=0,释放资源和计数器
sp2.~shared_ptr();
资源(Date)被释放
引用计数器被释放
4.3.3 代码示例 1:shared_ptr
的基本使用
#include <iostream>
#include <memory>
using namespace std;
class Date {
public:
Date(int year = 1, int month = 1, int day = 1)
: _year(year), _month(month), _day(day) {
cout << "Date(" << _year << "," << _month << "," << _day << ") 构造" << endl;
}
~Date() {
cout << "Date(" << _year << "," << _month << "," << _day << ") 析构" << endl;
}
void Print() const {
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main() {
// 1. 初始化:sp1管理资源,引用计数=1
shared_ptr<Date> sp1(new Date(2024, 5, 20));
cout << "sp1引用计数:" << sp1.use_count() << endl; // 输出1
sp1->Print();
// 2. 拷贝:sp2 = sp1,引用计数=2
shared_ptr<Date> sp2 = sp1;
cout << "sp1引用计数:" << sp1.use_count() << endl; // 输出2
cout << "sp2引用计数:" << sp2.use_count() << endl; // 输出2
sp2->Print();
// 3. 赋值:sp3 = sp2,引用计数=3
shared_ptr<Date> sp3;
sp3 = sp2;
cout << "sp1引用计数:" << sp1.use_count() << endl; // 输出3
cout << "sp3引用计数:" << sp3.use_count() << endl; // 输出3
// 4. 重置sp1:sp1不再管理资源,引用计数=2
sp1.reset(); // 等价于 sp1 = nullptr;
cout << "sp1是否为空:" << (sp1 ? "否" : "是") << endl; // 输出“是”
cout << "sp2引用计数:" << sp2.use_count() << endl; // 输出2
// 5. 离开作用域:sp2、sp3析构,引用计数依次减为1、0,释放资源
cout << "main函数结束,开始析构shared_ptr" << endl;
return 0;
}
运行结果:
4.3.4 代码示例 2:make_shared
—— 更安全的shared_ptr
创建方式
除了用new
构造shared_ptr
,C++11 还提供了std::make_shared
函数,它能直接用对象的构造参数创建shared_ptr
,优势如下:
- 减少内存分配次数:
new
构造需要分配 “资源内存” 和 “引用计数器内存” 两次;make_shared
一次性分配一块内存,同时存储资源和计数器,更高效; - 避免内存泄漏:若
new
成功但shared_ptr
构造失败(如内存不足),new
的资源会泄漏;make_shared
在分配内存时原子操作,不会泄漏; - 代码更简洁:无需手动写
new
。
#include <iostream>
#include <memory>
using namespace std;
class Date {
public:
Date(int year = 1, int month = 1, int day = 1)
: _year(year), _month(month), _day(day) {
cout << "Date(" << _year << "," << _month << "," << _day << ") 构造" << endl;
}
~Date() {
cout << "Date(" << _year << "," << _month << "," << _day << ") 析构" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main() {
// 用make_shared创建shared_ptr:直接传入Date的构造参数
shared_ptr<Date> sp1 = make_shared<Date>(2024, 5, 20);
cout << "sp1引用计数:" << sp1.use_count() << endl; // 1
// 用auto简化类型
auto sp2 = make_shared<Date>(2024, 5, 21);
cout << "sp2引用计数:" << sp2.use_count() << endl; // 1
return 0;
}
注意:make_shared
的唯一缺点是,若资源的析构需要很长时间,引用计数器会和资源一起占用内存,直到最后一个weak_ptr
(后续讲)也析构。但在大多数场景下,make_shared
仍是首选。
4.3.5 代码示例 3:shared_ptr
的删除器
和unique_ptr
类似,shared_ptr
也支持自定义删除器,且语法更简洁(删除器作为构造函数的第二个参数,无需指定模板参数)。
案例 1:管理new[]
数组
#include <iostream>
#include <memory>
using namespace std;
class Date {
public:
Date(int year = 1, int month = 1, int day = 1)
: _year(year), _month(month), _day(day) {
cout << "Date(" << _year << "," << _month << "," << _day << ") 构造" << endl;
}
~Date() {
cout << "Date(" << _year << "," << _month << "," << _day << ") 析构" << endl;
}
};
int main() {
// 方式1:用lambda作为删除器,释放new[]数组
shared_ptr<Date> sp1(new Date[3]{Date(2024,5,20), Date(2024,5,21), Date(2024,5,22)},
[](Date* ptr) {
delete[] ptr;
cout << "lambda删除器:释放Date数组" << endl;
}
);
// 方式2:C++17后,shared_ptr<T[]>特化版支持直接管理数组(无需删除器)
shared_ptr<Date[]> sp2(new Date[2]{Date(2024,5,23), Date(2024,5,24)});
sp2[0]; // 支持数组访问
return 0;
}
运行结果:
案例 2:管理互斥锁
#include <iostream>
#include <memory>
#include <mutex>
using namespace std;
int main() {
mutex mtx;
// 用shared_ptr管理互斥锁的lock/unlock
shared_ptr<mutex> sp_mtx(&mtx,
[](mutex* p) {
if (p->try_lock()) {
p->unlock();
cout << "删除器:解锁互斥锁" << endl;
}
}
);
// 锁定互斥锁
sp_mtx->lock();
cout << "互斥锁已锁定" << endl;
// 离开作用域时,sp_mtx析构,自动解锁
return 0;
}
4.3.6 shared_ptr
的陷阱:循环引用
shared_ptr
虽然强大,但存在一个致命问题 ——循环引用,会导致内存泄漏。
什么是循环引用?
当两个或多个shared_ptr
互相引用,形成 “闭环”,每个shared_ptr
的引用计数都无法减到 0,最终资源无法释放,导致内存泄漏。
代码示例:链表节点的循环引用
#include <iostream>
#include <memory>
using namespace std;
// 链表节点:next和prev都用shared_ptr管理
struct ListNode {
int _data;
shared_ptr<ListNode> _next; // 指向后一个节点
shared_ptr<ListNode> _prev; // 指向前一个节点
ListNode(int data) : _data(data) {
cout << "ListNode(" << _data << ") 构造" << endl;
}
~ListNode() {
cout << "ListNode(" << _data << ") 析构" << endl;
}
};
int main() {
// 创建两个节点
shared_ptr<ListNode> n1 = make_shared<ListNode>(10);
shared_ptr<ListNode> n2 = make_shared<ListNode>(20);
cout << "n1引用计数:" << n1.use_count() << endl; // 1
cout << "n2引用计数:" << n2.use_count() << endl; // 1
// 循环引用:n1->_next指向n2,n2->_prev指向n1
n1->_next = n2;
n2->_prev = n1;
cout << "n1引用计数:" << n1.use_count() << endl; // 2(n1本身 + n2->_prev)
cout << "n2引用计数:" << n2.use_count() << endl; // 2(n2本身 + n1->_next)
// main函数结束,n1和n2析构
cout << "main函数结束" << endl;
return 0;
}
循环引用导致的泄漏
问题分析:
main
结束时,n1
和n2
析构,它们的引用计数各减 1,变为 1;n1
的引用计数由n2->_prev
维持(1),n2
的引用计数由n1->_next
维持(1);- 由于
n1
和n2
互相引用,引用计数永远无法减到 0,ListNode(10)
和ListNode(20)
永远不会析构 —— 内存泄漏!
解决方案:weak_ptr
为了解决shared_ptr
的循环引用问题,C++11 引入了weak_ptr
—— 一种 “弱引用” 智能指针,它不管理资源,也不增加shared_ptr
的引用计数。
4.4 解决循环引用的weak_ptr
:弱引用的 “救星”
weak_ptr
是shared_ptr
的 “辅助工具”,它本身不具备 RAII 特性,不能直接管理资源,只能绑定到shared_ptr
上,观察shared_ptr
管理的资源。
4.4.1 weak_ptr
的核心特性
- 不增加引用计数:绑定
shared_ptr
时,不会增加shared_ptr
的引用计数; - 不管理资源:
weak_ptr
析构时,不会影响资源的释放; - 无法直接访问资源:
weak_ptr
没有重载*
和->
,必须通过lock()
函数获取shared_ptr
后,才能访问资源; - 支持过期检查:通过
expired()
函数检查绑定的shared_ptr
是否已释放资源(若已释放,返回true
)。
4.4.2 用weak_ptr
解决循环引用
修改链表节点的代码,将_next
和_prev
改为weak_ptr
:
#include <iostream>
#include <memory>
using namespace std;
// 链表节点:next和prev用weak_ptr管理
struct ListNode {
int _data;
weak_ptr<ListNode> _next; // 弱引用:不增加引用计数
weak_ptr<ListNode> _prev; // 弱引用:不增加引用计数
ListNode(int data) : _data(data) {
cout << "ListNode(" << _data << ") 构造" << endl;
}
~ListNode() {
cout << "ListNode(" << _data << ") 析构" << endl;
}
};
int main() {
shared_ptr<ListNode> n1 = make_shared<ListNode>(10);
shared_ptr<ListNode> n2 = make_shared<ListNode>(20);
cout << "n1引用计数:" << n1.use_count() << endl; // 1
cout << "n2引用计数:" << n2.use_count() << endl; // 1
// 绑定weak_ptr:不增加引用计数
n1->_next = n2; // weak_ptr = shared_ptr:合法,不增计数
n2->_prev = n1; // weak_ptr = shared_ptr:合法,不增计数
cout << "n1引用计数:" << n1.use_count() << endl; // 1(weak_ptr不增计数)
cout << "n2引用计数:" << n2.use_count() << endl; // 1(weak_ptr不增计数)
// 通过weak_ptr访问资源:需先lock()获取shared_ptr
if (shared_ptr<ListNode> sp = n1->_next.lock()) {
cout << "n1->_next的data:" << sp->_data << endl; // 输出20
}
cout << "main函数结束" << endl;
return 0;
}
4.4.3 运行结果:资源正常释放
问题解决分析:
n1->_next
和n2->_prev
是weak_ptr
,绑定n2
和n1
时不增加引用计数,因此n1
和n2
的引用计数始终为 1;main
结束时,n1
和n2
析构,引用计数减到 0,ListNode(10)
和ListNode(20)
被正常释放;- 循环引用被打破,内存泄漏问题解决!
4.4.4 weak_ptr
的其他用法
用法 1:检查资源是否过期
#include <iostream>
#include <memory>
using namespace std;
int main() {
shared_ptr<int> sp = make_shared<int>(100);
weak_ptr<int> wp = sp;
cout << "wp是否过期:" << (wp.expired() ? "是" : "否") << endl; // 否
cout << "wp绑定的引用计数:" << wp.use_count() << endl; // 1
// 释放sp的资源
sp.reset();
cout << "wp是否过期:" << (wp.expired() ? "是" : "否") << endl; // 是
cout << "wp绑定的引用计数:" << wp.use_count() << endl; // 0
// 过期后lock()返回空shared_ptr
if (shared_ptr<int> sp2 = wp.lock()) {
cout << "sp2管理的资源:" << *sp2 << endl;
} else {
cout << "wp已过期,无法获取资源" << endl; // 输出此句
}
return 0;
}
用法 2:在回调函数中避免循环引用
在面向对象编程中,若对象 A 的回调函数持有对象 B 的shared_ptr
,对象 B 又持有对象 A 的shared_ptr
,会形成循环引用。此时可将对象 B 中的shared_ptr
改为weak_ptr
。
#include <iostream>
#include <memory>
#include <functional>
using namespace std;
class B; // 前向声明
class A {
public:
A() { cout << "A 构造" << endl; }
~A() { cout << "A 析构" << endl; }
// 存储B的弱引用
weak_ptr<B> _wp_b;
// 回调函数:通过weak_ptr访问B
void Callback() {
if (shared_ptr<B> sp_b = _wp_b.lock()) {
cout << "A::Callback:访问B的资源" << endl;
} else {
cout << "A::Callback:B已释放" << endl;
}
}
};
class B {
public:
B() { cout << "B 构造" << endl; }
~B() { cout << "B 析构" << endl; }
// 存储A的强引用(shared_ptr)
shared_ptr<A> _sp_a;
// 调用A的回调
void CallA() {
_sp_a->Callback();
}
};
int main() {
shared_ptr<A> sp_a = make_shared<A>();
shared_ptr<B> sp_b = make_shared<B>();
// 绑定:A持有B的weak_ptr,B持有A的shared_ptr(无循环引用)
sp_a->_wp_b = sp_b;
sp_b->_sp_a = sp_a;
// 调用B的方法,间接调用A的回调
sp_b->CallA();
return 0;
}
运行结果:
可以看到,
A
和B
都能正常析构,没有循环引用。
五、shared_ptr 的线程安全问题:你必须知道的细节
在实际开发中,多线程场景极为常见,而shared_ptr
作为 C++ 中最常用的智能指针,其线程安全特性很容易被误解 —— 很多开发者认为 “shared_ptr
是线程安全的,用它管理资源就无需担心多线程问题”,但事实并非如此。shared_ptr
的线程安全需要拆解为两个独立层面分析,二者的责任边界完全不同,必须清晰区分才能避免踩坑。
5.1 先明确:shared_ptr 线程安全的 “责任边界”
C++ 标准对shared_ptr
的线程安全有明确规定,核心可概括为 **“管自己,不管资源”**:
- 自身引用计数的线程安全:
shared_ptr
内部的引用计数存储在堆上,多线程对同一个shared_ptr
进行拷贝、赋值、析构(本质是修改引用计数)时,引用计数的增减操作是原子的,不会出现竞争条件(标准库已内部实现保护); - 指向资源的线程安全:
shared_ptr
仅负责管理资源的生命周期,不保证对 “资源本身” 的访问安全。若多线程同时通过shared_ptr
读取或修改资源(比如修改资源的成员变量),必须由用户手动保证线程安全,否则会出现数据竞争。
简单来说:shared_ptr
只保证 “自己的引用计数不出错”,至于它指向的对象是否安全,完全是用户的责任。
5.2 代码示例 1:引用计数的线程安全(标准库已保证)
首先通过一个示例验证 “引用计数的线程安全”—— 多线程频繁拷贝shared_ptr
,观察最终引用计数是否正确。
5.2.1 测试代码
#include <iostream>
#include <memory>
#include <thread>
#include <vector>
using namespace std;
// 测试用资源类:仅用于观察构造和析构
class TestResource {
public:
TestResource() {
cout << "TestResource 构造(资源地址:" << this << ")" << endl;
}
~TestResource() {
cout << "TestResource 析构(资源地址:" << this << ")" << endl;
}
};
int main() {
// 创建一个shared_ptr,管理TestResource资源
shared_ptr<TestResource> sp = make_shared<TestResource>();
const size_t thread_count = 8; // 启动8个线程
const size_t loop_count = 100000; // 每个线程拷贝10万次
// 线程函数:循环拷贝shared_ptr,修改引用计数
auto copy_task = [&]() {
for (size_t i = 0; i < loop_count; ++i) {
shared_ptr<TestResource> temp_sp = sp; // 拷贝:引用计数+1
// 临时变量temp_sp析构时,引用计数自动-1
}
};
// 创建并启动所有线程
vector<thread> threads;
for (size_t i = 0; i < thread_count; ++i) {
threads.emplace_back(copy_task);
}
// 等待所有线程执行完毕
for (auto& t : threads) {
t.join();
}
// 最终引用计数:仅sp本身持有资源,应为1
cout << "最终引用计数:" << sp.use_count() << endl;
return 0;
}
5.2.2 运行结果
5.2.3 结果分析
- 8 个线程各拷贝 10 万次,意味着引用计数被频繁执行 “+1”(拷贝时)和 “-1”(临时变量析构时),总计 160 万次修改;
- 最终引用计数仍为 1,说明引用计数的增减是原子操作,没有出现 “计数错乱”(比如少加 1、少减 1);
- 若引用计数不是线程安全的,最终计数可能小于 1(导致资源提前释放)或大于 1(导致资源泄漏),但标准库的实现避免了这些问题。
5.3 代码示例 2:资源的线程不安全(用户需手动处理)
既然引用计数是安全的,那为什么还会有 “shared_ptr 线程安全问题”?核心在于资源本身的访问安全。下面通过一个 “多线程修改共享计数器” 的示例,展示资源的线程不安全问题。
5.3.1 测试代码(无锁保护,线程不安全)
#include <iostream>
#include <memory>
#include <thread>
#include <vector>
using namespace std;
// 共享资源:计数器(用于多线程修改)
struct SharedCounter {
int count = 0; // 待修改的共享变量
// 自增函数:无锁保护
void increment() {
count++; // 非原子操作:拆解为「读取count→加1→写回count」三步
}
};
int main() {
// shared_ptr管理SharedCounter资源
shared_ptr<SharedCounter> sp_counter = make_shared<SharedCounter>();
const size_t thread_count = 2; // 2个线程
const size_t loop_count = 100000; // 每个线程自增10万次
// 线程函数:循环调用increment修改共享变量
auto increment_task = [&]() {
for (size_t i = 0; i < loop_count; ++i) {
sp_counter->increment(); // 多线程同时修改count
}
};
// 启动线程
thread t1(increment_task);
thread t2(increment_task);
// 等待线程结束
t1.join();
t2.join();
// 预期结果:2*100000=200000;实际结果会小于200000
cout << "最终计数结果:" << sp_counter->count << endl;
return 0;
}
5.3.2 运行结果(示例)
5.3.3 问题分析:为什么计数会 “丢失”?
核心原因是count++
不是原子操作,多线程执行时会出现 “数据竞争”:
- 线程 1 读取
count=100
后,还没来得及加 1,线程 2 也读取到count=100
; - 线程 1 将
100+1=101
写回 count,此时 count=101; - 线程 2 也将
100+1=101
写回 count,覆盖了线程 1 的结果; - 原本应两次自增(count 从 100→102),最终只实现了一次(100→101),导致 “计数丢失”。
这里shared_ptr
本身没有问题(引用计数始终正确),但它指向的SharedCounter
资源因为没有锁保护,出现了线程安全问题 —— 这就是 “shared_ptr
不管资源安全” 的具体体现。
5.4 解决方案:为共享资源添加锁保护
要解决资源的线程不安全问题,最直接的方式是为资源的 “临界区操作”(如increment
)加锁,确保同一时间只有一个线程能修改共享变量。C++ 标准库提供的std::mutex
(互斥锁)是常用工具。
5.4.1 改进代码(加锁保护,线程安全)
#include <iostream>
#include <memory>
#include <thread>
#include <vector>
#include <mutex> // 引入互斥锁头文件
using namespace std;
struct SharedCounter {
int count = 0;
mutex mtx; // 互斥锁:保护count的修改
void increment() {
// unique_lock:构造时加锁,析构时自动解锁(避免忘记解锁导致死锁)
unique_lock<mutex> lock(mtx);
count++; // 临界区:此时只有当前线程能执行
}
};
int main() {
shared_ptr<SharedCounter> sp_counter = make_shared<SharedCounter>();
const size_t thread_count = 2;
const size_t loop_count = 100000;
auto increment_task = [&]() {
for (size_t i = 0; i < loop_count; ++i) {
sp_counter->increment();
}
};
thread t1(increment_task);
thread t2(increment_task);
t1.join();
t2.join();
// 预期结果:200000(稳定)
cout << "最终计数结果:" << sp_counter->count << endl;
return 0;
}
5.4.2 运行结果
5.4.3 原理分析:互斥锁如何保证安全?
- 互斥性:当线程 1 调用
increment()
时,unique_lock<mutex>
会锁定mtx
,此时线程 2 再调用increment()
会阻塞(等待锁释放); - 自动解锁:
unique_lock
是 RAII 风格的锁管理工具,当它离开作用域(即increment()
函数结束)时,会自动调用mtx.unlock()
,避免 “忘记解锁” 导致的死锁; - 临界区保护:
count++
被包裹在 “加锁→解锁” 之间,成为 “原子操作”,彻底解决了数据竞争。
5.5 深入:自定义 shared_ptr 时的引用计数安全
虽然标准库的shared_ptr
已保证引用计数安全,但了解其底层实现逻辑(尤其是 “如何保证引用计数原子性”),能帮助我们更深刻理解线程安全的本质。下面通过 “自定义简易 shared_ptr” 的示例,展示引用计数安全的实现方式。
5.5.1 问题背景:朴素引用计数的线程不安全
若自定义shared_ptr
时,用普通int*
存储引用计数,多线程修改会出现问题:
#include <iostream>
#include <thread>
#include <vector>
using namespace std;
template <typename T>
class UnsafeSharedPtr {
public:
// 构造:初始化资源和引用计数(普通int*)
explicit UnsafeSharedPtr(T* ptr = nullptr)
: _ptr(ptr)
, _pcount(new int(1)) {} // 引用计数初始为1
// 拷贝构造:引用计数+1(非原子操作)
UnsafeSharedPtr(const UnsafeSharedPtr<T>& other)
: _ptr(other._ptr)
, _pcount(other._pcount) {
(*_pcount)++; // 普通int自增:线程不安全
}
// 析构:引用计数-1,为0时释放资源
~UnsafeSharedPtr() {
if (--(*_pcount) == 0) { // 普通int自减:线程不安全
delete _ptr;
delete _pcount;
cout << "资源和引用计数释放" << endl;
}
}
private:
T* _ptr; // 指向资源的指针
int* _pcount;// 指向引用计数的指针(普通int)
};
// 测试用资源
struct TestObj {};
int main() {
UnsafeSharedPtr<TestObj> sp(new TestObj());
const size_t thread_count = 5;
const size_t loop_count = 10000;
auto copy_task = [&]() {
for (size_t i = 0; i < loop_count; ++i) {
UnsafeSharedPtr<TestObj> temp = sp; // 多线程拷贝
}
};
vector<thread> threads;
for (size_t i = 0; i < thread_count; ++i) {
threads.emplace_back(copy_task);
}
for (auto& t : threads) {
t.join();
}
return 0;
}
问题表现
程序可能崩溃或出现 “资源提前释放”:
- 多线程同时修改
*_pcount
时,可能出现 “引用计数提前减为 0”(比如线程 A 和 B 同时读取*_pcount=1
,A 先减为 0 并释放资源,B 再减为 - 1,后续操作访问已释放内存); - 最终可能导致
delete
已释放的_ptr
或_pcount
,触发程序崩溃。
5.5.2 解决方案:用原子类型保证引用计数安全
C++11 提供的std::atomic
是原子类型,其++
、--
等操作是原子的,不会被线程打断。将引用计数从int*
改为atomic<int>*
,即可解决线程安全问题:
#include <iostream>
#include <thread>
#include <vector>
#include <atomic> // 引入原子类型头文件
using namespace std;
template <typename T>
class SafeSharedPtr {
public:
explicit SafeSharedPtr(T* ptr = nullptr)
: _ptr(ptr)
, _pcount(new atomic<int>(1)) {} // 引用计数改为atomic<int>*
SafeSharedPtr(const SafeSharedPtr<T>& other)
: _ptr(other._ptr)
, _pcount(other._pcount) {
(*_pcount)++; // 原子自增:线程安全
}
~SafeSharedPtr() {
// 原子自减:先减1,再判断是否为0
if (--(*_pcount) == 0) {
delete _ptr;
delete _pcount;
cout << "资源和引用计数释放" << endl;
}
}
private:
T* _ptr; // 指向资源的指针
atomic<int>* _pcount; // 指向原子引用计数的指针
};
// 测试用资源
struct TestObj {};
int main() {
SafeSharedPtr<TestObj> sp(new TestObj());
const size_t thread_count = 5;
const size_t loop_count = 10000;
auto copy_task = [&]() {
for (size_t i = 0; i < loop_count; ++i) {
SafeSharedPtr<TestObj> temp = sp;
}
};
vector<thread> threads;
for (size_t i = 0; i < thread_count; ++i) {
threads.emplace_back(copy_task);
}
for (auto& t : threads) {
t.join();
}
cout << "程序正常结束" << endl;
return 0;
}
运行结果
原理分析
atomic<int>
的操作由编译器和硬件保证 “不可分割”,多线程同时执行(*_pcount)++
或--(*_pcount)
时,不会出现竞争;- 即使 5 个线程各拷贝 1 万次,引用计数的修改也能准确执行,最终析构时
--(*_pcount)
会正确减为 0,安全释放资源。
5.6 shared_ptr 线程安全的核心要点
通过以上分析,我们可以将shared_ptr
的线程安全总结为 **“两个层面,两种责任”**:
1. 引用计数层面(标准库负责安全)
- 标准库
shared_ptr
的引用计数基于原子类型实现,多线程对同一shared_ptr
的拷贝、赋值、析构不会导致计数错误; - 自定义
shared_ptr
时,需将引用计数改为atomic<int>*
或加锁,避免线程安全问题。
2. 资源访问层面(用户负责安全)
shared_ptr
不保证其指向资源的访问安全,多线程操作资源时,必须通过std::mutex
(互斥锁)、std::atomic
(原子变量)等工具手动保护;- 忽略资源的线程安全会导致数据竞争,表现为结果错误、程序崩溃等不可预期行为。
简言之:使用shared_ptr
时,无需担心 “引用计数错乱”,但必须关注 “资源是否被多线程安全访问”—— 前者是标准库的责任,后者是开发者的责任。
六、总结:智能指针的使用建议与价值
智能指针是 C++ 解决内存管理问题的 “核心工具”,其本质是 RAII 思想的工程化实现。通过本文的梳理,我们可总结出以下使用建议:
-
优先选择合适的智能指针:
- 若资源无需共享,用
unique_ptr
(高效、无额外开销); - 若资源需要共享,用
shared_ptr
(灵活、支持拷贝); - 若存在
shared_ptr
循环引用,用weak_ptr
(弱引用、不增计数); - 永远不要使用
auto_ptr
(已弃用,存在悬空风险)。
- 若资源无需共享,用
-
注意 shared_ptr 的细节:
- 用
make_shared
创建shared_ptr
(更高效、避免泄漏); - 明确
shared_ptr
的线程安全边界(引用计数安全,资源访问需手动加锁); - 避免用
shared_ptr
管理非new
分配的资源(需自定义删除器,确保释放逻辑正确)。
- 用
-
理解 RAII 的本质:
- 智能指针的核心是 “对象生命周期管理资源”,不仅限于内存,还可用于文件、锁、网络连接等;
- 自定义资源管理类时,遵循 RAII 思想,确保 “获取即初始化,销毁即释放”。
智能指针的出现,让 C++ 开发者从 “手动管理内存的繁琐与风险” 中解放出来,显著提升了代码的安全性和可维护性。掌握智能指针的原理与用法,是每个 C++ 开发者的必备技能,也是写出高质量 C++ 代码的基础。
更多推荐
所有评论(0)