文章目录
-
- C++智能指针详解(一):从问题到解决方案
- 一、为什么需要智能指针
-
- [1.1 传统指针的困境](#1.1 传统指针的困境)
- [1.2 智能指针的优雅方案](#1.2 智能指针的优雅方案)
- 二、RAII:智能指针的设计思想
-
- [2.1 什么是RAII](#2.1 什么是RAII)
- [2.2 简单的智能指针实现](#2.2 简单的智能指针实现)
- [2.3 RAII的其他应用](#2.3 RAII的其他应用)
- 三、C++标准库的智能指针
-
- [3.1 智能指针家族概览](#3.1 智能指针家族概览)
- [3.2 auto_ptr:历史的教训](#3.2 auto_ptr:历史的教训)
- [3.3 unique_ptr:独占的智能指针](#3.3 unique_ptr:独占的智能指针)
- [3.4 shared_ptr:共享的智能指针](#3.4 shared_ptr:共享的智能指针)
- 四、智能指针的高级特性
-
- [4.1 自定义删除器](#4.1 自定义删除器)
- [4.2 智能指针的类型转换](#4.2 智能指针的类型转换)
- [4.3 获取原始指针](#4.3 获取原始指针)
- 五、智能指针使用建议
-
- [5.1 选择合适的智能指针](#5.1 选择合适的智能指针)
- [5.2 性能考虑](#5.2 性能考虑)
- [5.3 常见陷阱](#5.3 常见陷阱)
- 六、总结与展望
-
- [6.1 本文要点回顾](#6.1 本文要点回顾)
- [6.2 下一篇预告](#6.2 下一篇预告)
C++智能指针详解(一):从问题到解决方案
💬 欢迎讨论:智能指针是现代C++中最重要的特性之一,它优雅地解决了内存管理的难题。如果你在学习过程中有任何疑问,欢迎在评论区留言交流!
👍 点赞、收藏与分享:这是C++智能指针系列的第一篇,建议收藏后系统学习。如果觉得有帮助,请分享给更多的朋友!
🚀 系列导航:本文将从实际问题出发,介绍智能指针的设计思想,并详细讲解C++标准库提供的各种智能指针的使用方法。
一、为什么需要智能指针
1.1 传统指针的困境
在使用传统指针时,我们经常会遇到这样的问题:
cpp
void Func()
{
int* array1 = new int[10];
int* array2 = new int[10];
// ... 一些操作
delete[] array1;
delete[] array2;
}
看起来很完美对吧?但是,如果中间发生了异常呢?
问题代码
cpp
double Divide(int a, int b)
{
if (b == 0)
{
throw "Divide by zero condition!";
}
return (double)a / b;
}
void Func()
{
int* array1 = new int[10];
int* array2 = new int[10];
try
{
int len, time;
cin >> len >> time;
cout << Divide(len, time) << endl;
}
catch (...)
{
// 捕获异常后要释放资源
delete[] array1;
delete[] array2;
throw; // 重新抛出异常
}
// 正常情况也要释放
delete[] array1;
delete[] array2;
}
这种方案的问题
- 代码重复:释放代码出现了两次
- 容易遗漏:如果new了更多资源,每处都要记得释放
- 不够优雅:大量的try-catch嵌套,代码可读性差
- 更复杂的情况:如果array2在new时就抛异常,还需要更多的try-catch
cpp
void Func()
{
int* array1 = new int[10];
try
{
int* array2 = new int[10]; // 这里可能抛异常
try
{
// 业务逻辑
}
catch (...)
{
delete[] array1;
delete[] array2;
throw;
}
delete[] array1;
delete[] array2;
}
catch (...)
{
delete[] array1; // array2都没构造成功
throw;
}
}
这样的代码实在太糟糕了!有没有更好的解决方案呢?
1.2 智能指针的优雅方案
如果我们使用智能指针,代码会变得非常简洁:
cpp
void Func()
{
// 使用智能指针管理资源
SmartPtr<int> sp1(new int[10]);
SmartPtr<int> sp2(new int[10]);
// 即使这里抛异常,sp1和sp2的析构函数也会被调用
// 资源会被自动释放,不需要手动delete
int len, time;
cin >> len >> time;
cout << Divide(len, time) << endl;
}
优势一目了然
- 不需要手动delete
- 异常安全:即使抛异常,资源也会被正确释放
- 代码简洁清晰
- 不容易出错
二、RAII:智能指针的设计思想
2.1 什么是RAII
RAII(Resource Acquisition Is Initialization)是一种资源管理的设计思想,核心理念是:
- 资源获取即初始化:在对象构造时获取资源
- 利用对象生命周期管理资源:资源的生命周期绑定到对象的生命周期
- 自动释放资源:在对象析构时自动释放资源
RAII的优势
- 异常安全:即使发生异常,栈展开时析构函数也会被调用
- 不易遗漏:不需要记住在每个退出点释放资源
- 代码简洁:资源管理逻辑集中在类中
2.2 简单的智能指针实现
根据RAII思想,我们可以设计一个简单的智能指针:
cpp
template<class T>
class SmartPtr
{
public:
// 构造函数:获取资源
SmartPtr(T* ptr)
: _ptr(ptr)
{
cout << "SmartPtr构造: " << _ptr << endl;
}
// 析构函数:释放资源(RAII的核心)
~SmartPtr()
{
if (_ptr)
{
cout << "delete: " << _ptr << endl;
delete _ptr;
_ptr = nullptr;
}
}
// 重载运算符,模拟指针行为
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
使用示例
cpp
struct Date
{
int _year;
int _month;
int _day;
Date(int year = 2024, int month = 1, int day = 1)
: _year(year), _month(month), _day(day)
{}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
};
void TestSmartPtr()
{
SmartPtr<Date> sp(new Date(2024, 12, 27));
// 像普通指针一样使用
sp->Print();
(*sp)._year = 2025;
sp->Print();
// 函数结束时,sp的析构函数自动调用
// 不需要手动delete
}
输出
bash
SmartPtr构造: 0x...
2024-12-27
2025-12-27
delete: 0x...
2.3 RAII的其他应用
RAII不仅可以管理内存,还可以管理其他资源:
文件资源管理
cpp
class FileGuard
{
public:
FileGuard(const char* filename, const char* mode)
{
_file = fopen(filename, mode);
if (!_file)
{
throw runtime_error("文件打开失败");
}
cout << "文件打开: " << _file << endl;
}
~FileGuard()
{
if (_file)
{
fclose(_file);
cout << "文件关闭: " << _file << endl;
}
}
FILE* get() { return _file; }
private:
FILE* _file;
};
void UseFile()
{
FileGuard file("test.txt", "r");
// 使用文件...
// 即使抛异常,file的析构函数也会被调用,文件会被关闭
}
锁资源管理
cpp
class LockGuard
{
public:
LockGuard(mutex& mtx) : _mtx(mtx)
{
_mtx.lock();
cout << "锁已获取" << endl;
}
~LockGuard()
{
_mtx.unlock();
cout << "锁已释放" << endl;
}
private:
mutex& _mtx;
};
三、C++标准库的智能指针
3.1 智能指针家族概览
C++标准库提供了多种智能指针,它们位于<memory>头文件中:
| 智能指针 | C++版本 | 特点 | 使用建议 |
|---|---|---|---|
auto_ptr |
C++98 | 拷贝转移所有权 | 已废弃,不要使用 |
unique_ptr |
C++11 | 独占所有权 | 单一所有权场景首选 |
shared_ptr |
C++11 | 共享所有权 | 需要共享时使用 |
weak_ptr |
C++11 | 不控制生命周期 | 解决循环引用 |
3.2 auto_ptr:历史的教训
auto_ptr是C++98引入的第一个智能指针,但它的设计存在严重缺陷。
auto_ptr的问题
cpp
struct Date
{
int _year;
int _month;
int _day;
Date(int year = 2024, int month = 1, int day = 1)
: _year(year), _month(month), _day(day)
{}
~Date()
{
cout << "~Date()" << endl;
}
};
int main()
{
auto_ptr<Date> ap1(new Date);
// 拷贝时,所有权转移给ap2
auto_ptr<Date> ap2(ap1);
// 危险!ap1已经是空指针了
// ap1->_year++; // 崩溃!
// ap2是有效的
ap2->_year++;
return 0;
}
为什么auto_ptr不好?
- 拷贝语义反直觉:拷贝后原对象变成空指针
- 容易产生悬空指针:不小心使用拷贝后的原对象会崩溃
- 不能用于容器:STL容器需要正常的拷贝语义
- 已被废弃:C++17正式移除
结论 :永远不要使用auto_ptr!
3.3 unique_ptr:独占的智能指针
unique_ptr是C++11引入的智能指针,用于独占资源的所有权。
基本特点
- 独占所有权:一个资源只能由一个unique_ptr管理
- 不支持拷贝:避免了auto_ptr的问题
- 支持移动:可以转移所有权
- 零开销:几乎没有性能损失
基本使用
cpp
int main()
{
// 构造unique_ptr
unique_ptr<Date> up1(new Date(2024, 12, 27));
// 使用->和*访问对象
up1->_year = 2025;
cout << up1->_year << endl;
// 不支持拷贝(编译错误)
// unique_ptr<Date> up2(up1); // 错误!
// unique_ptr<Date> up2 = up1; // 错误!
// 支持移动
unique_ptr<Date> up3(std::move(up1));
// 注意:移动后up1变成空指针
if (up1 == nullptr)
{
cout << "up1已经是空指针" << endl;
}
// up3现在拥有资源
cout << up3->_year << endl;
return 0;
}
数组版本
cpp
int main()
{
// 管理动态数组
unique_ptr<int[]> up(new int[10]);
// 可以使用下标访问
for (int i = 0; i < 10; ++i)
{
up[i] = i;
}
return 0;
}
什么时候使用unique_ptr?
- 明确单一所有权的场景
- 函数返回动态分配的对象
- 作为类成员管理资源
- 不需要共享资源时的首选
cpp
// 工厂函数返回unique_ptr
unique_ptr<Date> CreateDate(int year, int month, int day)
{
return unique_ptr<Date>(new Date(year, month, day));
}
// 类成员使用unique_ptr
class Calendar
{
public:
Calendar()
: _today(new Date(2024, 12, 27))
{}
private:
unique_ptr<Date> _today; // 自动管理,不需要手动delete
};
3.4 shared_ptr:共享的智能指针
shared_ptr是C++11引入的智能指针,允许多个指针共享同一个资源。
核心机制:引用计数
cpp
int main()
{
shared_ptr<Date> sp1(new Date(2024, 12, 27));
cout << "引用计数: " << sp1.use_count() << endl; // 1
// 拷贝,引用计数增加
shared_ptr<Date> sp2(sp1);
cout << "引用计数: " << sp1.use_count() << endl; // 2
cout << "引用计数: " << sp2.use_count() << endl; // 2
{
shared_ptr<Date> sp3(sp2);
cout << "引用计数: " << sp1.use_count() << endl; // 3
} // sp3析构,引用计数减1
cout << "引用计数: " << sp1.use_count() << endl; // 2
// sp1和sp2都指向同一个对象
sp1->_year++;
cout << sp2->_year << endl; // 2025
return 0;
} // sp1和sp2析构,引用计数归零,对象被释放
输出
bash
引用计数: 1
引用计数: 2
引用计数: 2
引用计数: 3
引用计数: 2
2025
~Date()
shared_ptr的特点
- 支持拷贝:多个shared_ptr可以指向同一个资源
- 支持移动:移动不增加引用计数
- 引用计数:最后一个shared_ptr析构时释放资源
- 线程安全:引用计数的增减是线程安全的(后续详细讲)
make_shared:推荐的创建方式
cpp
int main()
{
// 方式1:直接构造(两次内存分配)
shared_ptr<Date> sp1(new Date(2024, 12, 27));
// 方式2:make_shared(一次内存分配,推荐)
auto sp2 = make_shared<Date>(2024, 12, 27);
return 0;
}
make_shared的优势
- 性能更好:只需要一次内存分配
- 异常安全:避免某些潜在的内存泄漏
- 代码更简洁:不需要写new
什么时候使用shared_ptr?
- 需要共享资源的所有权
- 不确定哪个对象最后释放资源
- 多个对象需要访问同一资源
cpp
class Image
{
public:
Image(const string& path)
{
// 加载图片...
cout << "加载图片: " << path << endl;
}
~Image()
{
cout << "释放图片" << endl;
}
};
class Button
{
public:
Button(shared_ptr<Image> img) : _img(img) {}
private:
shared_ptr<Image> _img;
};
class Window
{
public:
Window(shared_ptr<Image> img) : _img(img) {}
private:
shared_ptr<Image> _img;
};
int main()
{
// 多个对象共享同一个图片资源
auto img = make_shared<Image>("icon.png");
Button btn(img);
Window win(img);
cout << "引用计数: " << img.use_count() << endl; // 3
return 0;
} // 所有对象析构后,图片才被释放
四、智能指针的高级特性
4.1 自定义删除器
默认情况下,智能指针使用delete释放资源。但有时我们需要自定义释放方式。
为什么需要自定义删除器?
- 数组资源 :需要使用
delete[] - C风格资源 :需要使用
fclose、free等 - 特殊资源:网络连接、数据库连接等
unique_ptr的删除器
cpp
template<class T>
class DeleteArray
{
public:
void operator()(T* ptr)
{
cout << "delete[]: " << ptr << endl;
delete[] ptr;
}
};
int main()
{
// 方式1:使用类模板参数指定删除器类型
unique_ptr<int, DeleteArray<int>> up(new int[10]);
return 0;
}
shared_ptr的删除器
cpp
int main()
{
// 方式1:仿函数
shared_ptr<int> sp1(new int[10], DeleteArray<int>());
// 方式2:函数指针
shared_ptr<int> sp2(new int[10], [](int* p) {
cout << "delete[]: " << p << endl;
delete[] p;
});
// 方式3:管理FILE*
shared_ptr<FILE> sp3(fopen("test.txt", "r"), [](FILE* fp) {
cout << "fclose: " << fp << endl;
fclose(fp);
});
return 0;
}
数组的特化版本(推荐)
C++11为数组提供了特化版本,使用更方便:
cpp
int main()
{
// unique_ptr的数组特化
unique_ptr<int[]> up(new int[10]);
up[0] = 1;
up[9] = 10;
// shared_ptr的数组特化(C++17)
shared_ptr<int[]> sp(new int[10]);
sp[0] = 1;
sp[9] = 10;
return 0;
}
4.2 智能指针的类型转换
检查空指针
cpp
int main()
{
shared_ptr<Date> sp1(new Date);
shared_ptr<Date> sp2;
// 方式1:使用operator bool
if (sp1)
{
cout << "sp1不是空指针" << endl;
}
if (!sp2)
{
cout << "sp2是空指针" << endl;
}
// 方式2:与nullptr比较
if (sp1 != nullptr)
{
cout << "sp1不是空指针" << endl;
}
return 0;
}
防止隐式转换
cpp
int main()
{
// 错误!构造函数被explicit修饰
// shared_ptr<Date> sp1 = new Date(); // 编译错误
// 正确:显式构造
shared_ptr<Date> sp2(new Date());
auto sp3 = make_shared<Date>();
return 0;
}
4.3 获取原始指针
有时我们需要获取智能指针管理的原始指针:
cpp
void LegacyFunction(Date* p)
{
// 老代码,只接受原始指针
p->_year++;
}
int main()
{
shared_ptr<Date> sp(new Date);
// 使用get()获取原始指针
Date* p = sp.get();
LegacyFunction(p);
// 注意:不要delete原始指针!
// delete p; // 错误!会导致double free
return 0;
}
注意事项
- 不要delete get()返回的指针
- 不要用get()返回的指针构造新的智能指针
- get()主要用于与老代码交互
五、智能指针使用建议
5.1 选择合适的智能指针
决策树
bash
需要共享所有权?
├─ 是 → 使用 shared_ptr
└─ 否 → 需要转移所有权?
├─ 是 → 使用 unique_ptr(支持移动)
└─ 否 → 使用 unique_ptr
典型场景
cpp
// 场景1:函数返回 - unique_ptr
unique_ptr<Date> CreateDate()
{
return make_unique<Date>(2024, 12, 27);
}
// 场景2:容器存储独占资源 - unique_ptr
vector<unique_ptr<Date>> dates;
dates.push_back(make_unique<Date>(2024, 1, 1));
// 场景3:共享资源 - shared_ptr
shared_ptr<Image> img = make_shared<Image>("icon.png");
Button btn1(img);
Button btn2(img);
// 场景4:观察者模式 - weak_ptr(下一篇详细讲)
class Observer
{
weak_ptr<Subject> _subject; // 不增加引用计数
};
5.2 性能考虑
unique_ptr vs 原始指针
- unique_ptr几乎零开销
- 编译器优化后性能基本相同
- 建议:默认使用unique_ptr
shared_ptr的开销
- 引用计数需要额外内存
- 引用计数的增减有原子操作开销
- 建议:不需要共享时使用unique_ptr
make_shared vs new
cpp
// 方式1:两次内存分配
shared_ptr<Date> sp1(new Date());
// 方式2:一次内存分配(推荐)
auto sp2 = make_shared<Date>();
5.3 常见陷阱
陷阱1:不要混用智能指针和原始指针管理同一资源
cpp
Date* p = new Date;
shared_ptr<Date> sp1(p);
shared_ptr<Date> sp2(p); // 危险!两个shared_ptr独立管理同一资源
// 会导致double free
陷阱2:不要用get()返回的指针构造新智能指针
cpp
shared_ptr<Date> sp1(new Date);
shared_ptr<Date> sp2(sp1.get()); // 危险!
陷阱3:注意移动后的对象
cpp
unique_ptr<Date> up1(new Date);
unique_ptr<Date> up2(std::move(up1));
// up1现在是nullptr,不要再使用!
六、总结与展望
6.1 本文要点回顾
智能指针解决的问题
- 自动内存管理
- 异常安全
- 减少内存泄漏
RAII设计思想
- 资源获取即初始化
- 利用对象生命周期管理资源
- 析构函数自动释放资源
三种主要智能指针
| 智能指针 | 所有权 | 拷贝 | 移动 | 使用场景 |
|---|---|---|---|---|
| unique_ptr | 独占 | ✗ | ✓ | 单一所有权 |
| shared_ptr | 共享 | ✓ | ✓ | 多个所有者 |
| weak_ptr | 不拥有 | ✓ | ✓ | 观察shared_ptr |
最佳实践
- 优先使用make_shared/make_unique
- 默认使用unique_ptr,需要共享时用shared_ptr
- 不要混用智能指针和原始指针管理同一资源
- 使用自定义删除器管理特殊资源
6.2 下一篇预告
在下一篇文章中,我们将深入学习:
- 智能指针的实现原理:手写智能指针
- 循环引用问题:shared_ptr的陷阱
- weak_ptr详解:如何解决循环引用
- 线程安全问题:多线程环境下的智能指针
- 内存泄漏:检测与预防
通过本文的学习,我们掌握了智能指针的基本使用。智能指针是现代C++内存管理的基石,正确使用智能指针能让代码更加安全和优雅!
以上就是C++智能指针基础的全部内容,期待在下一篇文章中与你继续探讨智能指针的原理与高级话题!如有疑问,欢迎在评论区交流讨论!❤️
