简单聊一下C++里的RAII与智能指针

文章介绍了RAII(ResourceAcquisitionIsInitialization)编程思想,如何通过封装资源管理(如文件、内存、指针等)在对象生命周期结束时自动释放,避免手动清理带来的问题,以及std::shared_ptr和std::weak_ptr在处理共享资源和循环引用中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

RAII这个词看着很吓人,但其实很easy,这不是一个特性,也不是一个技术,它是一种编程思想(或者说习惯),类似"多写注释有助于以后重新熟悉代码"。
为了解释RAII的优越性,我们先看一段没有使用RAII的代码。

  1. // 检查文件是否符合某种规范
  2. bool CheckFile(const std::string file_path) {
  3. std::string file_content;
  4. ifstream ifs;
  5. ifs.open(file_path, std::ifstream::binary); // 打开文件
  6. // 检查文件是否能打开成功
  7. if (!ifs.is_open())
  8. return false;
  9. // 检查文件大小
  10. int64 file_size = ifs.seekg(0, ifs.end).tellg();
  11. if (file_size > 1024) {
  12. ifs.close();
  13. return false;
  14. }
  15. ifs.seekg(0, ifs.beg);
  16. // 获取文件第一行内容
  17. if (!getline(ifs, file_content)) {
  18. ifs.close();
  19. return false;
  20. }
  21. // 检查文件内容
  22. if (file_content != "this file is legal") {
  23. ifs.close();
  24. return false;
  25. }
  26. ifs.close(); // 关闭文件
  27. return true;
  28. }

注意这段代码是没有bug的,可以正常运行,但看起来非常拧巴,就这几个return前的close,写的罗嗦不说,以后再改代码还容易出bug,随便哪次return时忘了close文件就可能出问题。

为此有很多方式可以将多个return合并为一个,比如:

  1. bool CheckFile(const std::string file_path) {
  2. std::string file_content;
  3. ifstream ifs;
  4. ifs.open(file_path, std::ifstream::binary); // 打开文件
  5. bool is_check_ok = true;
  6. // 检查文件是否能打开成功
  7. if (is_check_ok)
  8. is_check_ok = ifs.is_open();
  9. // 检查文件大小
  10. if (is_check_ok) {
  11. int64 file_size = ifs.seekg(0, ifs.end).tellg();
  12. ifs.seekg(0, ifs.beg);
  13. is_check_ok = file_size <= 1024;
  14. }
  15. // 获取文件第一行内容
  16. if (is_check_ok)
  17. is_check_ok = (bool)getline(ifs, file_content);
  18. // 检查文件内容
  19. if (is_check_ok)
  20. is_check_ok = file_content == "this file is legal";
  21. ifs.close(); // 关闭文件
  22. return is_check_ok;
  23. }
  1. bool CheckFile(const std::string file_path) {
  2. std::string file_content;
  3. ifstream ifs;
  4. ifs.open(file_path, std::ifstream::binary); // 打开文件
  5. bool is_check_ok = false;
  6. do {
  7. // 检查文件是否能打开成功
  8. if (!ifs.is_open())
  9. break;
  10. // 检查文件大小
  11. int64 file_size = ifs.seekg(0, ifs.end).tellg();
  12. ifs.seekg(0, ifs.beg);
  13. if (file_size > 1024)
  14. break;
  15. // 获取文件第一行内容
  16. if (!getline(ifs, file_content))
  17. break;
  18. // 检查文件内容
  19. if (file_content != "this file is legal")
  20. break;
  21. is_check_ok = true;
  22. } while (false);
  23. ifs.close(); // 关闭文件
  24. return is_check_ok;
  25. }
  1. bool CheckFile(const std::string file_path) {
  2. std::string file_content;
  3. ifstream ifs;
  4. ifs.open(file_path, std::ifstream::binary); // 打开文件
  5. // lambda
  6. auto check_file_fun = [&]() -> bool {
  7. // 检查文件是否能打开成功
  8. if (!ifs.is_open())
  9. return false;
  10. // 检查文件大小
  11. __int64 file_size = ifs.seekg(0, ifs.end).tellg();
  12. if (file_size > 1024)
  13. return false;
  14. ifs.seekg(0, ifs.beg);
  15. // 获取文件第一行内容
  16. if (!getline(ifs, file_content))
  17. return false;
  18. // 检查文件内容
  19. if (file_content != "this file is legal")
  20. return false;
  21. return true;
  22. };
  23. bool is_check_ok = check_file_fun();
  24. ifs.close(); // 关闭文件
  25. return is_check_ok;
  26. }
  1. // 仅作演示用,实际工作中不推荐使用goto
  2. bool CheckFile(const std::string file_path) {
  3. std::string file_content;
  4. ifstream ifs;
  5. ifs.open(file_path, std::ifstream::binary); // 打开文件
  6. bool is_check_ok = false;
  7. // 检查文件是否能打开成功
  8. if (!ifs.is_open())
  9. goto Exit;
  10. // 检查文件大小
  11. int64 file_size = ifs.seekg(0, ifs.end).tellg();
  12. ifs.seekg(0, ifs.beg);
  13. if (file_size > 1024)
  14. goto Exit;
  15. // 获取文件第一行内容
  16. if (!getline(ifs, file_content))
  17. goto Exit;
  18. // 检查文件内容
  19. if (file_content != "this file is legal")
  20. goto Exit;
  21. is_check_ok = true;
  22. Exit:
  23. ifs.close(); // 关闭文件
  24. return is_check_ok;
  25. }

哈哈哈抱歉多写了几个。

但这样用起来还是很不方便,这三种方式虽然实现了功能,也杜绝了手误写bug的可能性,但这种代码写法还是不如第一种随时return的方式直观。

如果这个ifs能自动close就好了,像一个普通变量一样,超出作用域就自动释放,这样我们就不需要专门处理它的清理工作了。

那么,有办法实现这个效果吗?答案是有,思路是用一个变量把ifs包裹起来,把ifs当初普通变量使用。比如这样:

  1. // 智能文件类
  2. class SmartFile {
  3. private:
  4. ifstream ifs;
  5. public:
  6. SmartFile(const std::string file_path) {
  7. ifs.open(file_path, std::ifstream::binary); // 构造函数里加载文件
  8. }
  9. ~SmartFile() {
  10. ifs.close(); // 析构函数里释放文件
  11. }
  12. ifstream& getIfs() {
  13. return ifs; // 获取ifs
  14. }
  15. };
  16. bool CheckFile(const std::string file_path) {
  17. std::string file_content;
  18. SmartFile smartIfs(file_path); // 使用智能ifs
  19. // 检查文件是否能打开成功
  20. if (!smartIfs.getIfs().is_open())
  21. return false;
  22. // 检查文件大小
  23. int64 file_size = smartIfs.getIfs().seekg(0, smartIfs.getIfs().end).tellg();
  24. if (file_size > 1024)
  25. return false;
  26. smartIfs.getIfs().seekg(0, smartIfs.getIfs().beg);
  27. // 获取文件第一行内容
  28. if (!getline(smartIfs.getIfs(), file_content))
  29. return false;
  30. // 检查文件内容
  31. if (file_content != "this file is legal")
  32. return false;
  33. return true;
  34. }

注意看这个代码,新增一个SmartFile类专门处理ifstream,在构造的时候初始化,在析构的时候close,这样我们就可以像使用普通变量一样使用ifstream了。

无论我们在任何时候(全局变量、函数内、if内)使用SmartFile,只要他超出作用域,SmartFile必然被销毁,析构函数被调用,ifstream自动被close了,无需我们手动处理。

上面代码已经无限接近第一版代码了,非常符合我们日常的思维习惯,但getIfs还是有点绕口,重载运算符来让代码更简洁:

  1. // 智能文件类
  2. class SmartFile {
  3. private:
  4. ifstream ifs;
  5. public:
  6. SmartFile(const std::string file_path) {
  7. ifs.open(file_path, std::ifstream::binary); // 构造函数里加载文件
  8. }
  9. ~SmartFile() {
  10. ifs.close(); // 析构函数里释放文件
  11. }
  12. ifstream* operator->() { return &ifs; }
  13. operator ifstream* () { return &ifs; }
  14. };
  15. bool CheckFile(const std::string file_path) {
  16. std::string file_content;
  17. SmartFile smartIfs(file_path); // 使用智能ifs
  18. // 检查文件是否能打开成功
  19. if (!smartIfs->is_open())
  20. return false;
  21. // 检查文件大小
  22. int64 file_size = smartIfs->seekg(0, smartIfs->end).tellg();
  23. if (file_size > 1024)
  24. return false;
  25. smartIfs->seekg(0, smartIfs->beg);
  26. // 获取文件第一行内容
  27. if (!getline(*smartIfs, file_content))
  28. return false;
  29. // 检查文件内容
  30. if (file_content != "this file is legal")
  31. return false;
  32. return true;
  33. }

是不是看起来就顺眼许多啦哈哈哈。

这就是RAII,非常的优雅。

几乎所有一切需要清理资源的,或者需要成对调用的接口,我们都可以使用RAII把它封装起来。

比如CRITICAL_SECTION,使用的时候需要Enter和Leave,可以封装:

  1. class SmartCritSec {
  2. CRITICAL_SECTION* cs;
  3. public:
  4. explicit SmartCritSec(CRITICAL_SECTION* cs) : cs(cs) {
  5. EnterCriticalSection(cs);
  6. }
  7. ~SmartCritSec() {
  8. LeaveCriticalSection(cs);
  9. }
  10. };
  11. void Test() {
  12. SmartCritSec scope(&m_access); // m_access为已经初始化过的CRITICAL_SECTION
  13. // work
  14. }

比如打开文件的HANDLE,结束时需要CloseHandle,可以封装:

  1. class SmartHandle {
  2. private:
  3. HANDLE handle;
  4. public:
  5. explicit SmartHandle(HANDLE handle) : handle(handle) { }
  6. ~SmartHandle() { CloseHandle(handle); }
  7. operator HANDLE() const { return handle; }
  8. };
  9. void Test() {
  10. ScopedHandle file(CreateFile(file_path, GENERIC_READ | GENERIC_WRITE, OPEN_EXISTING));
  11. // work
  12. }

比如Windows的GDI对象HBRUSH创建完需要DeleteObject,可以封装:

  1. class SmartHBrush {
  2. HBRUSH obj;
  3. public:
  4. SmartHBrush(HBRUSH obj) : obj(obj) { }
  5. ~SmartHBrush() { DeleteObject(obj); }
  6. operator HBRUSH() const { return obj; }
  7. };
  8. void Test() {
  9. SmartHBrush brush(CreateSolidBrush(RGB(255, 0, 0)));
  10. // work
  11. }

比如使用com组件时需要CoInitialize和CoUninitialize,可以封装:

  1. class SmartCom {
  2. public:
  3. SmartCom() { (void)CoInitialize(nullptr); }
  4. ~SmartCom() { CoUninitialize(); }
  5. };
  6. void Test() {
  7. SmartCom com;
  8. // work
  9. }

再也不用惦记着return前执行关闭操作了。

这时有聪明的同学要问了,我很少用你说的这些handle、com、brush,有没有什么我们更常用的代码用RAII实现的,等等你这个Smart单词看起来很眼熟呀~

眼熟就对了,RAII最大的应用,就是处理指针。

比如我们写一个正常的裸指针代码如下:

  1. bool Check() {
  2. std::string* s = new std::string;
  3. *s = "abc";
  4. // work
  5. if (!s) {
  6. delete s;
  7. return false;
  8. }
  9. if (*s == "") {
  10. delete s;
  11. return false;
  12. }
  13. delete s;
  14. return true;
  15. }

那么我们就可以写一个RAII指针如下:

  1. class SmartString {
  2. private:
  3. std::string* obj;
  4. public:
  5. SmartString(std::string* obj) : obj(obj) {}
  6. ~SmartString() { delete obj; }
  7. operator std::string* () const { return obj; }
  8. std::string* operator->() const { return obj; }
  9. };
  10. bool Check() {
  11. SmartString s(new std::string);
  12. *s = "abc";
  13. // work
  14. if (!s)
  15. return false;
  16. if (s->length() == 0)
  17. return false;
  18. if (*s == "")
  19. return false;
  20. return true;
  21. }

是不是就顺眼很多啦~

来,我们把它变成一个模板,让它更通用:

  1. template <class T>
  2. class SmartPtr {
  3. T* obj;
  4. public:
  5. SmartPtr(T* obj) : obj(obj) {}
  6. ~SmartPtr() { delete obj; }
  7. operator T* () const { return obj; }
  8. T* operator->() const { return obj; }
  9. };
  10. bool Check() {
  11. SmartPtr<std::string> s(new std::string);
  12. *s = "abc";
  13. SmartPtr<int> i(new int);
  14. SmartPtr<Bitmap> d(new Bitmap);
  15. SmartPtr<MyClassA> c(new MyClassA);
  16. return true;
  17. }

这就是一个最简单的智能指针,其他所有一切的智能指针,核心原理都是这样的。

当然一个智能指针要在各种复杂环境下都正常工作,仅有这些是不够的,还需要处理其他的逻辑操作,包括重载更多运算符,包括获取裸指针等,有兴趣的小伙伴请研究一下主流智能指针的实现源码。


在智能指针的基础上再多谈一点。

我上面实现的这个智能指针,再丰富一下操作接口,就是std::unique_ptr。

假如现在我在A线程有一个智能指针,指向一个对象(即一块内存),那如果我把这个指针传递给B线程时,我需要复制这个指针,也需要复制对象本身,这样两个线程各自持有各自的智能指针,也指向各自的对象。如果不这么操作,两个线程的指针指向同一对象,那当其中一个线程退出时,智能指针将对象销毁,第二个线程访问对象必然出错。

那么,有什么办法来解决这个问题呢,答案是std::shared_ptr,智能指针内部有一个计数器,初始化时为1,每复制一次指针就将计数器+1,而销毁指针时,并不直接销毁对象,而是将计数器-1,当计数器为0时才真正销毁对象。这样不管我复制多少次智能指针,销毁多少次智能指针,只有最后一个智能指针销毁时才真的销毁对象,非常的安全,高效,更优雅了。

那么,shared_ptr有什么缺点吗,当然有的,它的缺点叫循环引用,看下面这段代码(不再详细讲shared_ptr的实现逻辑了):

  1. struct Node {
  2. std::shared_ptr<Node> child_node;
  3. };
  4. void Test() {
  5. std::shared_ptr<Node> item = std::make_shared<Node>();
  6. item->child_node= item;
  7. }

运行一下就会出现内存泄漏,详细解释一下为什么会出问题:

  • make_shared时new了Node对象,此时计数器为1
  • item->_node = item时将智能指针赋给了child_node,此时既不new也不delete对象,只是计数器变成2
  • 当Test执行完成后,智能指针item销毁,然后shared_ptr本来计数器为2,现在item销毁了,计数器就变成1了,shared_ptr一看计数器没有变成0,也就不会真的执行对象的销毁操作,那么第1步new出来的对象就永远存在了。

硬要解释的话就是本来new出来的对象有2个入口,销毁了一个,第二个入口还在,但我们再也拿不到这个入口了,因此造成内存泄露。

那么,有什么办法来解决shared_ptr循环引用的问题吗,当然有的,那就是weak_ptr。

weak_ptr,看名字就知道,它是一个弱引用智能指针,就是为了解决shared_ptr循环引用问题的,请看代码

  1. struct Node {
  2. std::weak_ptr<Node> _node;
  3. };
  4. void Test() {
  5. std::shared_ptr<Node> item = std::make_shared<Node>();
  6. item->_node = item;
  7. }

如此,便不会内存泄露了。

至于shared_ptr和weak_ptr的详细用法,大家还请继续研究。

看上面智能指针的逻辑体系,其实非常的直白,符合我们的日常思维直觉,一点都不高深,哪怕让我们自己设计一套智能指针系统,我们最终写出来的,肯定也是shared_ptr与weak_ptr这一套。

现在C++已经不推荐使用裸指针了,除非万不得已,不然还是老老实实的使用智能指针,能极大的降低bug概率和程序员的心智负担。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值

举报

选择你想要举报的内容(必选)
  • 内容涉黄
  • 政治相关
  • 内容抄袭
  • 涉嫌广告
  • 内容侵权
  • 侮辱谩骂
  • 样式问题
  • 其他
点击体验
DeepSeekR1满血版
程序员都在用的中文IT技术交流社区

程序员都在用的中文IT技术交流社区

专业的中文 IT 技术社区,与千万技术人共成长

专业的中文 IT 技术社区,与千万技术人共成长

关注【CSDN】视频号,行业资讯、技术分享精彩不断,直播好礼送不停!

关注【CSDN】视频号,行业资讯、技术分享精彩不断,直播好礼送不停!

客服 返回顶部

登录后您可以享受以下权益:

×