CppGuide社区 CppGuide社区
首页
  • 🔥最新谷歌C++风格指南(含C++17/20)
  • 🔥C++17详解
  • 🔥C++20完全指南
  • 🔥C++23快速入门
🔥C++面试
  • 第1章 C++ 惯用法与Modern C++篇
  • 第2章 C++开发工具与调试进阶
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 网络通信协议设计
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 服务其他模块设计
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 🔥C++游戏编程入门(零基础学C++)
  • 🔥使用C++17从零开发一个调试器 (opens new window)
  • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
  • 🔥交易系统开发岗位求职与面试指南统 (opens new window)
  • 🔥使用C++从零写一个C语言编译器 (opens new window)
  • 🔥从零用C语言写一个Redis
  • leveldb源码分析
  • libevent源码分析
  • Memcached源码分析
  • TeamTalk源码分析
  • 优质源码分享 (opens new window)
  • 🔥远程控制软件gh0st源码分析
  • 🔥Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • Go系统接口编程
  • 高效Go并发编程
  • Go性能调优
  • Go项目架构设计
  • 🔥使用Go从零开发一个数据库
  • 🔥使用Go从零开发一个编译器 (opens new window)
  • 🔥使用Go从零开发一个解释器 (opens new window)
Rust编程指南
  • SQL零基础指南
  • MySQL开发与调试指南
GitHub (opens new window)
首页
  • 🔥最新谷歌C++风格指南(含C++17/20)
  • 🔥C++17详解
  • 🔥C++20完全指南
  • 🔥C++23快速入门
🔥C++面试
  • 第1章 C++ 惯用法与Modern C++篇
  • 第2章 C++开发工具与调试进阶
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 网络通信协议设计
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 服务其他模块设计
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 🔥C++游戏编程入门(零基础学C++)
  • 🔥使用C++17从零开发一个调试器 (opens new window)
  • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
  • 🔥交易系统开发岗位求职与面试指南统 (opens new window)
  • 🔥使用C++从零写一个C语言编译器 (opens new window)
  • 🔥从零用C语言写一个Redis
  • leveldb源码分析
  • libevent源码分析
  • Memcached源码分析
  • TeamTalk源码分析
  • 优质源码分享 (opens new window)
  • 🔥远程控制软件gh0st源码分析
  • 🔥Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • Go系统接口编程
  • 高效Go并发编程
  • Go性能调优
  • Go项目架构设计
  • 🔥使用Go从零开发一个数据库
  • 🔥使用Go从零开发一个编译器 (opens new window)
  • 🔥使用Go从零开发一个解释器 (opens new window)
Rust编程指南
  • SQL零基础指南
  • MySQL开发与调试指南
GitHub (opens new window)
  • C++语言面试问题集锦 目录与说明
  • 第一章 auto与类型推导
  • 第二章 关键字static及其不同用法
  • 第三章 多态、继承和虚函数
  • 第四章 Lambda函数
  • 第五章 C++中如何使用 const 限定符
  • 第六章 Modern C++的一些最佳实践
  • 第七章 智能指针
    • 问题51:解释资源获取即初始化(RAII,Resource Acquisition Is Initialization)习惯用法
    • 问题52:我们应该在什么时候使用 unique_ptr ?
    • 问题53:使用共享指针(shared pointers)的原因有哪些?
    • 问题54:何时使用 weak_ptr ?
    • 问题55:相较于 new 操作符,std::makeshared 和std::makeunique 有哪些优势?
    • 问题56:是否应该始终使用智能指针(smart pointers)而不是原始指针?
    • 问题57:何时以及为何要将指针初始化为 nullptr ?
  • 第八章 引用、万能引用等
  • 第九章 C++20相关问题
  • 第十章 特殊函数及数量规则
  • 第十一章 C++面向对象设计
  • 第十二章 程序质量
  • 第十三章 标准模板库
  • 第十四章 杂项
  • cppinterviewmostaskedquestions
zhangxf
2025-03-27
目录

第七章 智能指针

# 第七章 智能指针

在接下来的几个问题中,我们将重点讨论智能指针是什么、何时以及为何要使用它们。

但首先,让我们讨论一个与之密切相关的编程习惯用法。

# 问题51:解释资源获取即初始化(RAII,Resource Acquisition Is Initialization)习惯用法

RAII是C++的基础习惯用法。它指出,在系统中供应有限的任何资源,都必须在使用前先获取。

这里所说的资源包括:

  • 分配的堆内存;
  • 执行线程;
  • 打开的套接字;
  • 文件;
  • 锁定的互斥锁;
  • 磁盘空间;
  • 数据库连接。

另一方面,那些在使用前无需获取的资源不属于RAII的范畴,例如:

  • CPU核心和时间;
  • 缓存容量;
  • 网络带宽;
  • 电力消耗;
  • 甚至栈内存。

但RAII不仅关乎资源获取,还涉及资源释放。RAII还确保在控制对象的生命周期结束时,所有资源都会以获取的相反顺序被释放。同样,当一个对象的资源获取失败时,该对象或其任何成员已经成功获取的所有资源都必须以相反顺序释放。

这可能足以说明这个习惯用法的名字有多糟糕,语言创造者比雅尼·斯特劳斯特鲁普(Bjarne Stroustrup)也对此感到遗憾,或许“作用域绑定资源管理(Scope Bound Resource Management)”是个更好的名字,但大多数人仍然称其为RAII。

在实际应用中,一个遵循RAII的类会在构造时获取所有资源,并在析构时释放所有资源。你无需调用诸如init()/open()或destroy/close这样的方法。

在标准库中,std::string、std::vector或std::thread都是遵循RAII的类。

另一方面,如果你考虑原始指针,它们并不遵循RAII概念。当一个指针超出作用域时,它不会自动被销毁,你必须在它丢失并造成内存泄漏之前手动删除它。而标准库中的智能指针(std::unique_ptr、std::shared_ptr)提供了这样一种封装机制。

SomeLimitedResource* resource = new  SomeLimitedResource();
resouce->doIt(); 
delete  resource; 
1
2
3

使用智能指针应用RAII,情况就会好很多:

std::unique_ptr<SomeLimitedResource> resource =
    std::make_unique<SomeLimitedResource>();
resouce->doIt(); 
1
2
3

你也可以编写自己的RAII处理程序:

class  SomeLimitedResourceHandler  {
public:
    SomeLimitedResourceHandler(SomeLimitedResource* resourc\
    e) :
        m_resource(resource) {}

    ~SomeLimitedResourceHandler() { delete  m_resource; }

    void  doit() {
        m_resource->doit();
    }

private:
    SomeLimitedResource* m_resource;
};

SomeLimitedResourceHandler resourceHandler(new  SomeLimitedResource());
resourceHandler.doit();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 问题52:我们应该在什么时候使用 unique_ptr ?

如果你需要使用智能指针,默认情况下,你应该选择std::unique_ptr。它是一种小巧、快速且仅支持移动操作的智能指针,用于管理具有独占所有权语义的资源。

它的大小与原始指针相同,并且在大多数操作中,所需的指令数量也相同。

如前所述,它用于表示独占所有权。无论它指向什么,它都拥有该对象。std::unique_ptr是一种仅支持移动的类型,不允许复制,只能有一个所有者。它没有引用计数,这使得它比std::shared_ptr更小、更快。

默认情况下,资源的销毁是通过delete操作完成的,但也可以指定自定义的删除器。有状态的删除器和作为删除器的函数指针会增加std::unique_ptr对象的大小。一旦指针超出作用域,资源就会被销毁。

在C++11中,可以这样创建一个unique_ptr:

std::unique_ptr<T> ptr (new  T());
// or;
T* t = new  T();
std::unique_ptr<T> ptr2 (t);
1
2
3
4

C++14引入了std::make_unique来简化创建过程:

std::unique_ptr<T> ptr = std::make_unique<T>();
1

新的指针创建方式更安全,因为在之前,你可能会不小心将同一个原始指针传递给两个新的unique_ptr,就像这样:

T* t = new  T();
std::unique_ptr<T> ptr (t);
std::unique_ptr<T> ptr2 (t);
1
2
3

std::unique_ptr的一个常见用法是作为对象层次结构中工厂函数的返回类型。如果后来发现使用shared_ptr更合适,转换也非常简单:

std::unique_ptr<T> unique = std::make_unique<T>();
std::shared_ptr<T> shared = std::move(unique);
1
2

# 问题53:使用共享指针(shared pointers)的原因有哪些?

共享指针为C++开发者带来了双重优势。它提供自动清理功能(也称为垃圾回收),适用于所有带有析构函数的类型,而且这种清理是可预测的,不像其他语言中的垃圾回收器那样。

与std::unique_ptr或原始指针相比,std::shared_ptr对象通常要大两倍,因为它们不仅包含一个原始指针,还包含另一个指向动态分配内存区域的原始指针,在这个区域进行引用计数。

默认情况下,对象通过delete进行销毁,但和std::unique_ptr一样,也可以传递自定义删除器。值得注意的是,删除器的类型不会影响std::shared_ptr的类型。

一旦指针超出作用域,资源就会被销毁。

共享指针自C++11起可用,有两种初始化共享指针的方式:

std::shared_ptr<T> ptr (new  T());
std::shared_ptr<T> ptr2 = std::make_shared<T>();
1
2

第二种创建指针的方式,即通过std::make_shared,更加安全,因为这样可以避免意外地传入同一个原始指针两次,而且还能避免为引用计数内存进行动态分配的开销。

避免从原始指针类型的变量创建std::shared_ptr,因为这样难以维护,也很难判断所指向的对象何时会被销毁。

使用std::shared_ptr进行共享所有权的资源管理。

# 问题54:何时使用 weak_ptr ?

弱指针std::weak_ptr是一种智能指针,它不会影响对象的引用计数,因此它所指向的对象可能已经被销毁。

std::weak_ptr由shared_ptr创建,如果所指向的对象被销毁,弱指针就会过期。

auto* sp = std::make_shared<T>();
std::weak_ptr<T> wp(sp);

// . . .
sp = nullptr ;

if  (wp.expired()) {
    std::cout << "wp doesn't point to a valid object anymore" << '\n';
}
1
2
3
4
5
6
7
8
9

如果你想使用弱指针,可以调用它的lock()函数,该函数会返回一个std::shared_ptr,如果指针已过期则返回nullptr;或者你也可以直接将弱指针传递给shared_ptr的构造函数。

std::shared_ptr<T> sp = wp.lock();
std::shared_ptr<T> sp2(wp);
1
2

那么它有什么用呢?

它在处理循环引用时很有用,可以打破循环。假设你有一个Keyboard(键盘)、一个Logger(日志记录器)和一个Screen(屏幕)对象的实例。Screen和Keyboard都有一个指向Logger的共享指针,而Logger应该有一个指向Screen的指针。

Keyboard -> Logger <–> Screen

Logger应该使用什么指针呢?如果使用原始指针,当Screen被销毁时,Logger仍然存在,Keyboard对Logger仍有共享所有权,此时Logger中指向Screen的指针就成了悬空指针。

如果使用共享指针,它们之间会形成循环依赖,导致无法被销毁,进而产生资源泄漏。

这时std::weak_ptr就派上用场了。当Screen被销毁时,Logger中指向Screen的指针仍然是悬空指针,但正如我们上面看到的,这种情况很容易被检测到。

此外,它在缓存和观察者模式中也很有用。想了解更多细节,可以查阅参考书籍。

# 问题55:相较于 new 操作符,std::make_shared 和std::make_unique 有哪些优势?

首先回顾一下。std::make_shared在C++11中被添加到标准模板库(STL),而std::make_unique直到C++14才被添加。

与直接使用new相比,make函数消除了源代码的重复,提高了异常安全性,并且std::make_shared生成的代码更简洁、运行速度更快。

auto  ptr = std::make_shared<T>(); // 优先使用这种方式
std::shared_ptr<T> ptr2(new  T);
1
2

如你所见,使用make函数时,我们只需要输入一次类型名,无需重复输入。

当使用new时,如果在构造过程中抛出异常,在某些情况下可能会导致资源泄漏,因为此时指针还没有被make函数“处理”。

std::make_shared也比直接使用new更快,因为它只分配一次内存来存储对象和用于引用计数的控制块。而使用new则需要进行两次内存分配。

遗憾的是,如果要指定自定义删除器,上述make函数通常就无法使用了,至少不常使用。

# 问题56:是否应该始终使用智能指针(smart pointers)而不是原始指针?

不是的。尽管在很多情况下原始指针被认为很危险,但它仍然有其用武之地。

std::unique_ptr用于转移所有权,std::shared_ptr用于共享所有权。如果一个函数与所有权无关,那么它就没有必要使用智能指针作为指针参数。

在这种情况下使用智能指针只会使函数的应用程序编程接口(API)更受限制,运行时成本也更高。

同时,我们应该尽量避免使用new关键字。使用new通常意味着在其他地方需要使用delete。除非在C++11中,你要创建一个std::unique_ptr,而此时std::make_unique还不可用。

总之,尽量避免使用new。涉及所有权时,使用智能指针和相关的工厂函数——除非有特殊情况(查看《Effective Modern C++》第21条内容)。否则,如果你不想共享或转移所有权,就使用原始指针。这样可以使API更友好,运行时性能也更快。

# 问题57:何时以及为何要将指针初始化为 nullptr ?

如果确定指针会被初始化,那么用nullptr进行预初始化就没有意义。从另一个角度来看,如果不确定是否会初始化指针,那么就应该将其初始化为nullptr。

例如,根据你使用的编译器及其设置,下面这段代码可能会发出警告。希望你把警告当作错误来对待:

T* fun() {
    T* t;
    if  (someCondition()) {
        t = getT();
    }
    return  t;
}
1
2
3
4
5
6
7
// Clang发出的警告:
prog.cc:22:6:  warning:  variable  't'  is used uninitialized whenever  'if'  condition  is  false  [-Wsometimes-uninitialized]
        if  (someCondition())  {
             ^~~~~~~~~~~~~~~
prog .cc:25:9: note: uninitialized use occurs here
        return  t;
                   ^
prog.cc:22:2: note: remove  the  'if'  if  its  condition  is always  true
        if  (someCondition())  {
         ^~~~~~~~~~~~~~~~~~~~~
prog.cc:21:6: note:  initialize  the  variable  't'  to silence  this  warning
        T*  t;
            ^
             = nullptr
1
2
3
4
5
6
7
8
9
10
11
12
13
14

但为什么这是个问题呢?会出什么错呢?

当你没有初始化指针就尝试使用它时,可能会出现3个问题:

  • 指针可能指向你无法访问的内存位置,这种情况下会导致段错误,使程序崩溃。
  • 指针可能会意外地指向一些实际数据,如果你不知道它指向什么,可能会对数据造成不可预测(而且很难调试)的更改。
  • 你无法确定指针是否已经被初始化。你怎么区分有效地址和声明指针时碰巧存在的地址呢?

如果你在声明指针时无法初始化它,将其初始化为nullptr可以大大减少或消除这些问题:

  • 虽然使用这样的指针仍然会导致段错误,但至少我们可以可靠地测试它是否为nullptr,并据此采取相应措施。如果是一个随机值,在程序崩溃之前我们一无所知。
  • 如果将指针初始化为nullptr,它就永远不会指向任何数据,因此不会意外地修改任何内容,只会按预期进行操作。
  • 因此,我们可以确定地判断指针是否已被初始化,并据此做出决策。
T* fun() {
    T* t = nullptr ;
    if  (someCondition()) {
        t = getT();
    }
    return  t; // 不再有警告!
}
1
2
3
4
5
6
7

这是一个风格问题,但就我个人而言,我更倾向于避免将指针初始化为nullptr。在这种情况下,我宁愿直接返回nullptr,否则就在声明指针时用正确的值进行初始化。

T* fun() {
    if  (!someCondition()) {
        return  nullptr ;
    }
    return  getT();
}
1
2
3
4
5
6

另外,最好避免需要返回nullptr的情况。

上次更新: 2025/03/27, 20:29:48
第六章 Modern C++的一些最佳实践
第八章 引用、万能引用等

← 第六章 Modern C++的一些最佳实践 第八章 引用、万能引用等→

最近更新
01
第一章 auto与类型推导
03-27
02
第二章 关键字static及其不同用法
03-27
03
C++语言面试问题集锦 目录与说明
03-27
更多文章>
Copyright © 2024-2025 沪ICP备2023015129号 张小方 版权所有
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式