C++ 高效使用智能指针的8个建议

前言:智能指针是C++11提供的新特性,它基于RAII实现,可以自动管理内存资源,避免内存泄漏的发生,但是智能指针也并不是万能的,如果不正确使用智能指针,也会导致内存泄漏的发生,因此,我们需要了解如何高效使用智能指针避免一些可能的陷阱。本文总结了8个关于智能指针的建议,希望对大家有所帮助。

1. 优先使用std::unique_ptr, 再考虑std::shared_ptr

  • shared_ptr的大小是unique_ptr的两倍,因为shared_ptr需要维护一个引用计数。
  • shared_ptr由于占据更多内存,且需要通过原子操作维护引用计数,因此效率是比较慢的。在不开启编译器优化的时候,是比new操作慢10倍,此时不应该使用make_shared、shared_ptr。开启优化后,也大概慢2-3倍。 [2]
  • unique_ptr、make_unique、带少许偏差的make_shared几乎和new、delete具有一样的性能。
  • unique_ptr自动管理内存资源,而几乎没有额外开销。因此效率和new、delete几乎一样。

2. 尽量使用std::make_uniquestd::make_shared

尽量使用std::make_shared<T>而不是shared_ptr<T>(new T)std::make_shared<T>是更异常安全的做法。std::make_shared<T>是一个函数模板,它在动态内存中分配一个对象并初始化它,返回指向此对象的std::shared_ptr<T>std::make_shared<T>的好处是它只进行一次内存分配,而std::shared_ptr<T>(new T)则进行两次内存分配,一次是为T分配内存,另一次是为std::shared_ptr<T>的控制块分配内存。因此,std::make_shared<T>是更好的选择。

例如:

std::shared_ptr<int> sp(new int(42)); // exception unsafe

当new int(42)抛出异常时,sp将不会被创建,从而对应new分配的内存也不会释放,从而导致内存泄漏。

3. 使std::shared_ptr管理的对象或资源线程安全

如果多个线程同时拷贝同一个 shared_ptr 对象,不会有问题,因为 shared_ptr 的引用计数是线程安全的。但是如果多个线程同时修改同一个 shared_ptr 对象,不是线程安全的。因此,如果多个线程同时访问同一个 shared_ptr 对象,并且有写操作,需要使用互斥量来保护。

4. 注意std::shared_ptr的循环引用问题

  • 什么是循环引用问题 ?

循环引用是指两个或多个对象之间通过shared_ptr相互引用,形成了一个环,导致它们的引用计数都不为0,从而导致内存泄漏。

在观察者模式中使用shared_ptr可能会出现循环引用,在下面的程序中,Observer对象和Subject对象相互引用,导致它们的引用计数都不为0,从而导致内存泄漏。

class IObserver {
public:
virtual void update(const string& msg) = 0;
}; class Subject {
public:
void attach(const std::shared_ptr<IObserver>& observer) {
observers_.emplace_back(observer);
}
void detach(const std::shared_ptr<IObserver>& observer) {
observers_.erase(std::remove(observers_.begin(), observers_.end(), observer), observers_.end());
}
void notify(const string& msg) {
for (auto& observer : observers_) {
observer->update(msg);
}
}
private:
std::vector<std::shared_ptr<IObserver>> observers_;
}; class ConcreteObserver : public IObserver {
public:
ConcreteObserver(const std::shared_ptr<Subject>& subject) : subject_(subject) {}
void update(const string& msg) override {
std::cout << "ConcreteObserver " << msg<< std::endl;
}
private:
std::shared_ptr<Subject> subject_;
}; int main() {
std::shared_ptr<Subject> subject = std::make_shared<Subject>();
std::shared_ptr<IObserver> observer = std::make_shared<ConcreteObserver>(subject);
subject->attach(observer);
subject->notify("update");
return 0;
}
  • 避免循环引用的方法

将Observer类中的subject_成员变量改为weak_ptr,这样就不会导致内存无法正确释放了。

5. 避免使用裸指针创建智能指针

不要用同一个raw pointer初始化多个shared_ptr

因为多个shared_ptr由同一个raw pointer创建时会导致生成两个独立的引用计数控制块,从以下程序可见sp1、sp2的引用计数都为1。

int* p = new int(0);
std::shared_ptr<int> sp1(p);
std::shared_ptr<int> sp2(p);
std::cout<<sp1.use_count()<<std::endl; // 1
std::cout<<sp2.use_count()<<std::endl; // 1

当sp1、sp2销毁时会产生未定义行为,因为shared_ptr的析构函数会释放它所管理的对象,当sp1析构时,会释放p指向的内存,当sp2析构时,会再次释放p指向的内存。

6. 用enable_shared_from_this在类内部中获得一个指向当前对象的shared_ptr

如果通过this指针创建shared_ptr时,相当于通过一个裸指针创建shared_ptr,多次创建会导致多个shared_ptr对象管理同一个内存。当shared_ptr对象销毁时,会释放this指向的内存,但是this指针可能还会被使用,导致程序崩溃。如以下程序所示:

class A {
public:
std::shared_ptr<A> get_shared_ptr() {
return std::shared_ptr<A>(this); // error
}
};

为了解决这个问题,C++11提供了std::enable_shared_from_this模板类,它可以在类内部获得一个指向当前对象的shared_ptr。

使用方法: 继承enable_shared_from_this类;通过shared_from_this()方法返回。

class A : public std::enable_shared_from_this<A> {
public:
std::shared_ptr<A> get_shared_ptr() {
return shared_from_this();
}
};

6. 避免使用std::shared_ptrget()方法

std::shared_ptrget()方法返回一个裸指针,这个裸指针指向std::shared_ptr管理的对象。如果通过delete释放了这个裸指针指向的内存,当std::shared_ptr销毁时,其管理的对象会被再次释放。

7. 使用unique_ptrrelease()方法后,不要忘记手动释放资源

std::unique_ptr调用release()方法后,会释放对资源的所有权,但是不会释放资源本身。因此当std::unique_ptr调用release()方法后,需要手动调用delete释放资源。

8. 使用std::weak_ptrstd::shared_ptr对象前,检查是否失效。

std::weak_ptr是一种弱引用,它不会增加引用计数,因此不会影响资源的释放。std::weak_ptrlock()方法可以返回一个std::shared_ptr对象,但是在使用std::shared_ptr对象前,需要检查std::shared_ptr对象是否失效。

std::weak_ptr可以作为std::shared_ptr的构造函数参数,但如果std::weak_ptr指向的对象已经被释放,那么std::shared_ptr的构造函数会抛出std::bad_weak_ptr异常。

参考

  1. Effective Modern C++: 42 Specific Ways to Improve Your Use of C++11 and C++14. Scott Meyers. O'Reilly Media. 2014.

  2. memory-and-performance-overhead-of-smart-pointer


你好,我是七昂,计算机科学爱好者,致力于分享C/C++、操作系统、软件架构等计算机基础知识。如果你有任何问题或者建议,欢迎随时与我交流。如果这篇内容对您有帮助,请点赞关注,之后将会持续分享更多技术干货。希望我们能一起探索程序员修炼之道。感谢你的阅读!

C++ 高效使用智能指针的8个建议的更多相关文章

  1. 智能指针(1)-std::unique_ptr

    std::unique_ptr std::unique_ptr是一种几乎和原始指针一样高效的智能指针,对所管理的指针资源拥有独占权.由C++11标准引入,用于替代C++98中过时的std::auto_ ...

  2. c/c++ 智能指针 shared_ptr 和 new结合使用

    智能指针 shared_ptr 和 new结合使用 用make_shared函数初始化shared_ptr是最推荐的,但有的时候还是需要用new关键字来初始化shared_ptr. 一,先来个表格,唠 ...

  3. C++11智能指针读书笔记;

    智能指针是一个类对象,而非一个指针对象. 原始指针:通过new建立的*指针 智能指针:通过智能指针关键字(unique_ptr, shared_ptr ,weak_ptr)建立的指针 它的一种通用实现 ...

  4. 「C++」理解智能指针

    维基百科上面对于「智能指针」是这样描述的: 智能指针(英语:Smart pointer)是一种抽象的数据类型.在程序设计中,它通常是经由类型模板(class template)来实做,借由模板(tem ...

  5. shared_ptr智能指针源码剖析

    (shared_ptr)的引用计数本身是安全且无锁的,但对象的读写则不是,因为 shared_ptr 有两个数据成员,读写操作不能原子化.根据文档 (http://www.boost.org/doc/ ...

  6. 读书笔记 effective c++ Item 17 使用单独语句将new出来的对象放入智能指针

    1. 可能会出现资源泄漏的一种用法 假设我们有一个获取进程优先权的函数,还有一个在动态分类的Widget对象上根据进程优先权进行一些操作的函数: int priority(); void proces ...

  7. 智能指针剖析(上)std::auto_ptr与boost::scoped_ptr

    1. 引入 C++语言中的动态内存分配没有自动回收机制,动态开辟的空间需要用户自己来维护,在出函数作用域或者程序正常退出前必须释放掉. 即程序员每次 new 出来的内存都要手动 delete,否则会造 ...

  8. C++智能指针剖析(上)std::auto_ptr与boost::scoped_ptr

    1. 引入 C++语言中的动态内存分配没有自动回收机制,动态开辟的空间需要用户自己来维护,在出函数作用域或者程序正常退出前必须释放掉. 即程序员每次 new 出来的内存都要手动 delete,否则会造 ...

  9. C++解析(27):数组、智能指针与单例类模板

    0.目录 1.数组类模板 1.1 类模板高效率求和 1.2 数组类模板 1.3 堆数组类模板 2.智能指针类模板 2.1 使用智能指针 2.2 智能指针类模板 3.单例类模板 3.1 实现单例模式 3 ...

  10. C++解析(20):智能指针与类型转换函数

    0.目录 1.智能指针 2.转换构造函数 3.类型转换函数 4.小结 1.智能指针 内存泄漏(臭名昭著的Bug): 动态申请堆空间,用完后不归还 C++语言中没有垃圾回收机制 指针无法控制所指堆空间的 ...

随机推荐

  1. MySql 数据库、数据表操作

    数据库操作 创建数据库 语法 语法一:create database 数据库名 语法二:create database 数据库名 character set 字符集; 查看数据库 语法 查看数据库服务 ...

  2. Golang 切片作为函数参数传递的陷阱与解答

    作者:林冠宏 / 指尖下的幽灵.转载者,请: 务必标明出处. GitHub : https://github.com/af913337456/ 出版的书籍: <1.0-区块链DApp开发实战&g ...

  3. 基于Docker安装项目管理工具禅道

    禅道是通用的项目管理软件 完整支持敏捷项目模型.瀑布项目模型.看板模型 内置项目集.产品.项目和执行四个管理框架 支持CMMI标准的落地实施 下载镜像 docker pull easysoft/zen ...

  4. Swift开发基础06-闭包

    Swift的闭包(Closures)是一种将功能块和上下文整合并演示在代码中的一种手段.闭包可以捕获并存储其上下文中的变量和常量.与普遍存在于其他语言的匿名函数(如Python的lambda.Java ...

  5. VS Code 开发统一代码格式化配置

    eslint: 是用来做代码风格检查的,比较关注代码质量,并且会提示不符合风格规范的代码,也有一部分代码格式化的功能.不是消除空行. "editor.formatOnSave": ...

  6. 第一章 FFmpeg初体验:在Centos7.9下编译FFmpeg!

    FFmpeg 官方网站:https://ffmpeg.org//download.html#build-linux 1.下载源码 1.1 第一种方式,官网上面下载源码包: 截至目前最新的版本是7.0. ...

  7. AT_arc113_c 题解

    洛谷链接&Atcoder 链接 本篇题解为此题较简单做法及较少码量,并且码风优良,请放心阅读. 题目简述 现在有一个字符串 \(S\),每一次你可以选择一个 \(i(1 \le i \le | ...

  8. 学习笔记--Java方法重载

    Java方法重载 感受 以下代码不使用"方法重载",不使用overload,分析程序存在的缺点 public class OverloadTest01 { // 入口 public ...

  9. Python 基于pymongo操作Mongodb学习总结

    实践环境 Python 3.6.4 pymongo 4.1.1 pymongo-3.12.3-cp36-cp36m-win_amd64.whl 下载地址:https://pypi.org/simple ...

  10. 配置Sprig security后Post请求无法使用

    在学习过程中发现在配置完Spring security后,Post请求失效,无法增删改数据,这里可以通过在Spring Security 的Config类中增加 也可以自定义csrf,不过目前还不是很 ...