C++ 秘籍:问题解决方法(一)
一、开始 C++
C++ 编程语言是一种强大的低级语言,它允许你编写编译成机器指令的程序,以便在计算机的处理器上执行。这使得 C++ 不同于 C# 和 Java 等较新的语言。这些语言是解释型语言。这意味着它们不是直接在处理器上执行,而是被发送到另一个负责操作计算机的程序。Java 程序是使用 Java 虚拟机(JVM)执行的,C# 程序是由公共语言运行时(CLR)执行的。
由于 C++ 是一种提前编译的语言,它仍然在绝对性能至关重要的领域得到广泛应用。C++ 仍然是最主要使用的编程语言的最明显的领域是视频游戏行业。C++ 允许程序员编写充分利用底层系统架构的应用程序。在从事 C++ 程序员的职业生涯中,您可能会熟悉诸如缓存一致性这样的短语。没有多少其他语言可以让你优化你的应用程序,以适应你的程序运行的处理器。这本书向您介绍了一些在不同时期会影响应用程序性能的陷阱,并向您展示了一些解决这些问题的技术。
现代 C++ 正处于一个语言功能不断更新的时期。情况并非总是如此。尽管从 20 世纪 80 年代早期就出现了,C++ 编程语言直到 1998 年才被标准化。2003 年发布了该标准的一个小的更新和澄清,称为 C++03。2003 年的更新并没有给这种语言增加任何新的特性,但是它澄清了一些已经被忽略的现有特性。其中之一是对 STL vector 模板标准的更新,指定 vector 的成员应该连续存储在内存中。C++11 标准于 2011 年发布,对 C++ 编程语言进行了大规模更新。C++ 获得了模板、lambda 和闭包支持之外的通用类型演绎系统的特性,一个内置的并发库和更多的特性。C++14 对该语言进行了较小的更新,通常建立在 C++14 已经提供的特性之上。诸如从函数中自动返回类型推导这样的特性已经被清理,lambdas 已经更新了新的特性,并且有一些新的方法来定义正确类型的文字值。
本书致力于编写可移植的、符合标准的 C++14 代码。在写作的时候,只要你使用一个提供所有语言特性的编译器,在 Windows、Linux 和 OS X 机器上写 C++14 代码是可能的。为此,本书将在 Windows 和 Ubuntu 上使用 Clang 作为编译器,并将在 OS X 上使用 Xcode。本章的其余部分将重点介绍用 C++ 编写程序所需的软件,然后向您展示如何获得一些可用于 Windows、OS X 和 Linux 操作系统的更常见的选项。
配方 1-1。查找文本编辑器
问题
C++ 程序由许多不同的源文件构成,这些文件必须由一个或多个程序员创建和编辑。源文件是简单的文本文件,通常有两种不同的类型:头文件和源文件。头文件用于在不同文件之间共享有关类型和类的信息,源文件通常用于包含方法和组成程序的实际可执行代码。
解决办法
文本编辑器成为你开始写 C++ 程序所需要的第一个主要软件。在不同的平台上有许多优秀的文本编辑器可供选择。目前我最好的两个选择是免费的 Windows 版 Notepad++ 和 Sublime Text 2,尽管它们不是免费的,但可以在所有主要的操作系统上使用。图 1-1 显示了来自崇高文本 2 的截图。Vim 和 gvim 也是非常好的选项,适用于所有三种操作系统。这些编辑器提供了许多强大的功能,对于愿意学习的人来说是极好的选择。
图 1-1 。来自崇高文本 2 编辑器的截图
注意不要觉得有直接抓取文本编辑器的冲动。本章后面的一些方法涵盖了集成开发环境(ide ),集成开发环境包含了编写、构建和调试 C++ 应用程序所需的所有软件。
图 1-1 显示了一个好的文本编辑器最重要的特征之一:它应该能够在你的源代码中突出不同类型的关键字。你可以在图 1-1 的简单 Hello World 程序中看到,Sublime Text 2 能够突出显示 C++ 关键字include、int和return。它还为主功能name和琴弦<iostream>和"Hello World!"添加了不同颜色的高光。一旦你有了用你选择的文本编辑器编写代码的经验,你将会熟练地扫描你的源文件来聚焦你感兴趣的代码区域,并且语法高亮将会是这个过程中的一个主要因素。
配方 1-2。在 Ubuntu 上安装 Clang
问题
您希望在运行 Ubuntu 的计算机系统上构建支持最新 C++14 语言特性的 C++ 程序。
解决办法
Clang 编译器支持所有最新的 C++14 语言特性,libstdc++ 库支持所有 C++14 STL 特性。
它是如何工作的
Ubuntu 操作系统 配置了软件包库,让你安装 Clang 没有太大困难。您可以在终端窗口中使用apt-get命令来实现这一点。图 1-2 显示了安装 Clang 应该输入的命令。
图 1-2 。Ubuntu 终端窗口显示安装 Clang 所需的命令
要安装 Clang,你可以在命令行输入下面的命令sudo apt-get install clang。运行这个命令将导致 Ubuntu 查询它的存储库,并找出安装 Clang 所需的所有依赖项。这个过程完成后,系统会提示您确认是否要安装 Clang 及其依赖项。在图 1-3 中可以看到这个提示。
图 1-3 。apt-get 依赖关系确认提示
此时,您可以按 enter 键继续,因为默认选项是 yes。然后 Ubuntu 会下载并安装你在电脑上安装 Clang 所需的所有软件。您可以通过运行clang命令来确认这已经成功。图 1-4 显示了如果一切顺利的话应该是什么样子。
图 1-4 。Ubuntu 中一次成功的 Clang 安装
配方 1-3。在窗户上安装金属撞击声
问题
您希望在 Windows 操作系统 上构建基于 C++14 的程序。
解决办法
可以使用 Cygwin for Windows 安装 Clang,构建应用程序。
它是如何工作的
Cygwin 为 Windows 计算机提供了一个类似 Unix 的命令行环境。这非常适合使用 Clang 构建程序,因为安装的 Cygwin 预配置了包存储库,其中包含了在 Windows 计算机上安装和使用 Clang 所需的一切。
您可以从 Cygwin 网站的http://www.cygwin.com获得 Cygwin 安装程序可执行文件。请务必下载 32 位版本的 Cygwin 安装程序,因为 Cygwin 提供的默认软件包目前仅适用于 32 位环境。
一旦你下载了安装程序,你应该运行它并点击直到你看到要安装的软件包列表。此时,您需要选择 Clang、make 和 libstdc++ 包。图 1-5 显示了选中 Clang 包的 Cygwin 安装程序。
图 1-5 。在 Cygwin 安装程序中过滤 Clang 包
通过单击软件包所在行的跳过区域,可以在安装程序中将软件包标记为要安装。单击一次“跳过”会将包版本移动到最新版本。你应该为 Clang,make 和 libstdc++ 选择最新的包。一旦您选择了所有 3 个包,您可以单击 Next 进入一个窗口,要求您确认这三个包所需的依赖项的安装。
一旦您成功下载并安装了运行 Clang 所需的所有包,您可以通过打开 Cygwin 终端并键入clang命令来检查它是否成功。你可以在图 1-6 中看到这个输出的结果。
图 1-6 。在 Windows 的 Cywgin 环境下成功运行 Clang
配方 1-4。在 OS X 上安装金属撞击声
问题
您希望在运行 OS X 的计算机上构建基于 C++14 的程序
解决办法
苹果的 Xcode IDE 自带 Clang 作为默认编译器。从 OS X 应用商店安装 Xcode 也会安装 Clang。
它是如何工作的
从 OS X 电脑上的 App Store 安装最新版本的 Xcode。一旦你安装了 Xcode,你可以使用 Spotlight 打开一个终端窗口,然后输入 clang 来查看编译器是否已经安装。图 1-7 显示了这应该是什么样子。
图 1-7 。安装 Xcode 后在 OS X 上运行 Clang
配方 1-5。构建您的第一个 C++ 程序
问题
您希望使用您的计算机从您编写的 C++ 源代码中生成可执行的应用程序。
解决办法
从 C++ 源文件生成可执行文件包括两个步骤:编译和链接。根据您的操作系统,配方 1-2、配方 1-3 或配方 1-4 中的步骤将使您拥有从 C++14 源文件构建应用程序所需的所有软件。您现在已经准备好构建您的第一个 C++14 程序了。创建一个包含您的项目的文件夹,并添加一个名为 HelloWorld.cpp 的文本文件。将清单 1-1 中的代码输入到文件中并保存。
清单 1-1 。你的第一个 C++14 程序
#include <iostream>
#include <string>
int main(void)
{
using namespace std::string_literals;
auto output = "Hello World!"s;
std::cout << output << std::endl;
return 0;
}
清单 1-1 中的代码是一个 C++ 程序,只有在使用 C++14 兼容编译器时才能编译。本章的方法 2-4 包含了如何获得一个编译器的说明,该编译器可用于编译 Windows、Ubuntu 和 OS X 的 C++14 代码。一旦你创建了一个文件夹和包含清单 1-1 中代码的源文件,你就可以构建一个工作应用程序。您可以使用 makefile 来实现这一点。在 HelloWorld.cpp 文件旁边的文件夹中创建一个名为 makefile 的文件。makefile 不应该有文件扩展名,这对于习惯于 Windows 操作系统的开发人员来说可能有点奇怪,但是对于基于 Unix 的操作系统,例如 Linux 和 OS X,这是完全正常的。将清单 1-2 中的代码输入到 makefile 中。
清单 1-2 。构建清单 1-1 中的代码所需的 makefile 文件
HelloWorld: HelloWorld.cpp
clang++ -g -std=c++1y HelloWorld.cpp -o HelloWorld
注意清单 1-2 中
clang++命令前的空格是一个制表符。您不能用空格替换制表符,因为make将无法构建。确保 makefile 中的配方总是以制表符开始。
清单 1-2 中的文本由从 HelloWorld.cpp 源文件构建应用程序所需的指令组成。第一行的第一个单词是 makefile 目标的名称。这是构建过程完成后应用程序可执行文件的名称。在这种情况下,我们将构建一个名为 HelloWorld 的可执行文件。接下来是构建程序所需的先决条件。这里您将 HelloWorld.cpp 列为唯一的先决条件,因为它是用于构建可执行文件的唯一源文件。
然后,目标和先决条件后面是为了构建您的应用程序而执行的一系列方法。在这个小示例中,有一行代码调用 clang++ 编译器从 HelloWorld.cpp 文件生成可执行代码。使用–std=c++1y传递给clang++的参数要求 Clang 使用 C++14 语言标准进行编译,而–o开关指定编译过程生成的对象输出文件的名称。
使用命令 shell(例如 Windows 上的 cmd 或 Linux 或 OS X 上的 Terminal)浏览到您创建的用于存储源文件和 makefile 的文件夹,然后键入 make。这将调用 GNU make 程序,并自动读取和执行 makefile。这将把一个可执行文件输出到您可以从命令行运行的同一文件夹中。您现在应该能够做到这一点,并看到在命令行上输出了文本 Hello World。图 1-8 显示了它在 Ubuntu 终端窗口中的样子。
图 1-8 。Ubuntu 终端中 Runnung HelloWorld 生成的输出
配方 1-6。在 Cygwin 或 Linux 中使用 GDB 调试 C++ 程序
问题
您正在编写一个 C++14 程序,并且希望能够从命令行调试应用程序。
解决办法
Cygwin for Windows 和基于 Linux 的操作系统(如 Ubuntu)都可以安装和使用 C++ 应用程序的 GDB 命令行调试器。
它是如何工作的
您可以使用 Cygwin Windows 安装程序或随您喜欢的 Linux 发行版一起安装的软件包管理器来安装 GDB 调试器。这将为您提供一个命令行 C++ 调试器,可用于检查 C++ 程序的功能。您可以使用作为配方 1-5 的一部分生成的源代码、makefile 和应用程序来练习。要为你的程序生成调试信息,你应该更新 makefile 来包含清单 1-3 的内容,并运行 make 来生成一个可调试的可执行文件。
清单 1-3 。生成可调试程序的 makefile
HelloWorld: HelloWorld.cpp
clang++ -g -std=c++1y HelloWorld.cpp -o HelloWorld
一旦你遵循了配方 1-5,更新了 makefile 以包含清单 1-5 中的内容,并生成了一个可执行文件,你就可以在你的应用程序上运行 GDB 了,方法是在你的命令行上浏览文件夹并键入gdb HelloWorld。在来自清单 1-3 的 makefile 中,传递给 Clang 的新的–g开关要求编译器在应用程序中生成附加信息,以帮助调试器在程序在调试器中执行时为您提供关于程序的准确信息。
注意你可能会看到一个通知,告诉你如果你以前编译过,你的程序已经是最新的了。如果发生这种情况,只需删除现有的可执行文件。
在 HelloWorld 中运行 GDB 应该会导致您的命令行运行 GDB 并提供如图图 1-9 所示的输出。
图 1-9 。GDB 的一个实例
现在您有了一个正在运行的调试器,可以用来在程序执行时检查它。当 GDB 第一次启动时,程序还没有开始,这允许你在开始之前配置一些断点。要设置断点,您可以使用break命令或同一命令的简写b。在 GDB 命令提示符下输入break main,然后回车。这应该会导致 GDB 将命令以及设置断点的程序的地址和它为所提供的函数检测到的文件名和行号回显给您。现在,您可以在窗口中键入 run 来执行程序,并让 GDB 在断点处停止。输出应类似于图 1-10 所示。
图 1-10 。GDB 在main中设置的断点处停止时的输出
此时,您有几个选项可以让您继续执行程序。您可以在下面看到最常用命令的列表。
-
stepstep命令用于单步执行将在当前行调用的函数。 -
nextnext命令用于跳过当前行,并在同一功能的下一行停止。 -
finishfinish命令用于执行当前函数中剩余的所有代码,并在调用当前函数的函数的下一行停止。 -
print <name>后跟变量名的
print命令可用于打印程序中变量的值。 -
breakbreak命令可与行号、函数名或源文件和行号一起使用,在程序源代码中设置断点。 -
continuecontinue命令用于在断点处暂停后恢复代码执行。 -
untiluntil命令可以从循环中继续执行,并在循环执行完成后立即停止在第一行。 -
infoinfo命令可以与locals命令或stack命令一起使用,以显示关于程序中当前局部变量或堆栈状态的信息。 -
help你可以键入
help后接任何命令,让 GDB 告诉你一个给定命令的所有不同用法。
GDB 调试器也可以用命令–tui运行。这将使您在窗口顶部看到当前正在调试的源文件。你可以在图 1-11 中看到它的样子。
图 1-11 。带有源窗口的 GDB
配方 1-7。在 OS X 上调试你的 C++ 程序
问题
OS X 操作系统没有提供任何安装和使用 GDB 的简单方法。
解决办法
Xcode 附带了 LLDB 调试器,可以代替 GDB 在命令行上使用。
它是如何工作的
LLDB 调试器本质上非常类似于配方 1-6 中使用的 GDB 调试器。在 GDB 和 LLDB 之间转换只是学习如何通过使用各自提供的命令来执行相同的任务。
通过在终端中浏览到包含 HelloWorld 的目录并键入lldb HelloWorld,可以在 HelloWorld 可执行文件上执行 LLDB。这将给你类似于图 1-12 的输出。
图 1-12 。运行在 OS X 终端上的 LLDB 调试器
注意你需要使用
–g开关来编译你的程序。如果你不确定的话,看一下清单 1-3 来看看这是怎么回事。
一旦 LLDB 如清单 1-12 所示运行,就可以在 main 的第一行设置一个断点,只需键入breakpoint set –f HelloWorld.cpp –l 8或b main即可。您可以使用run命令开始执行,并让它在您刚刚设置的断点处暂停。当程序停止时,您可以使用next命令跳过当前行并停在下一行。您可以使用step命令单步执行当前行的函数,并在函数的第一行停止。finish命令将退出当前功能。
您可以通过键入q并按 enter 键来退出 LLDB。重启 LLDB 并键入breakpoint set –f HelloWorld.cpp –l 9。在此之后使用run命令,LLDB 应该在应用程序停止的那一行打印源代码。现在可以输入print output来查看输出变量存储的值。你也可以使用frame variable命令来查看当前堆栈帧中的所有局部变量。
这些简单的命令将允许您在使用随本书提供的示例时充分使用 LLDB 调试器。使用 LLDB 时,下面的列表可以作为一个方便的备忘单。
-
stepstep命令用于进入当前行要调用的函数。 -
nextnext命令用于跳过当前行,并停在同一功能的下一行。 -
finishfinish命令用于执行当前函数中剩余的所有代码,并在调用当前函数的函数的下一行停止。 -
print <name>后跟变量名的
print命令可用于打印程序中变量的值。 -
breakpoint set –-name <name> -
breakpoint set –file <name> --line <number>breakpoint命令可以与行号、函数名或源文件和行号一起使用,在程序源代码中设置断点。 -
help你可以键入
help后接任何命令,让 GDB 告诉你一个给定命令的所有不同用法。
配方 1-8。切换 C++ 编译模式
问题
在编译程序之前,您希望能够在不同的 C++ 标准之间切换。
解决方案
Clang 提供了std开关,以便您可以指定编译时要使用的 C++ 标准。
它是如何工作的
Clang 默认使用 C++98 标准构建。您可以在 Clang++ 中使用 std 参数来告诉编译器使用非默认标准。清单 1-4 显示了一个 makefile,它被配置成使用 C++14 标准构建一个程序。
清单 1-4 。用 C++14 构建
HelloWorld: HelloWorld.cpp
clang++ -std=c++1y HelloWorld.cpp -o HelloWorld
清单 1-4 中的 makefile 展示了如何指定 Clang 应该使用 C++14 构建你的源文件。这个例子是用 Clang 3.5 编写的,它使用c++1y命令来表示 C++14。
清单 1-5 展示了如何使用 C++11 来构建一个程序。
清单 1-5 。用 C++11 构建
HelloWorld: HelloWorld.cpp
clang++ -std=c++11 HelloWorld.cpp -o HelloWorld
在清单 1-5 中,你想使用带有std开关的c++11选项来构建 C++11。最后,清单 1-6 展示了如何配置 Clang 来用 C++98 显式构建。
清单 1-6 。用 C++98 构建
HelloWorld: HelloWorld.cpp
clang++ -std=c++98 HelloWorld.cpp -o HelloWorld
清单 1-6 中的 makefile 可以用来用 C++98 显式构建。您可以通过完全省略std命令来获得相同的结果,Clang 将默认使用 C++98 构建。
注意不能保证每个编译器默认使用 C++98。如果不确定哪个标准是默认标准,请查阅编译器文档。您还可以尝试使用 Clang,并使用
c++1z选项启用其实验性的 C++17 支持!
配方 1-9。使用 Boost 库构建
问题
你想用 Boost 库写一个程序。
解决办法
Boost 作为源代码提供,可以包含在应用程序中并编译到应用程序中。
它是如何工作的
Boost 是一个大型 C++ 库,包含了各种强大的功能。涵盖整个库超出了本书的范围;但是,将使用字符串格式库。您可以在http://www.boost.org/从 Boost 网站获取 Boost 库。
您可以从 Boost 网站获得包含最新版本的 Boost 库的压缩文件夹。您绝对需要能够包含基本 boost 功能的唯一文件夹是 boost 文件夹本身。我已经下载了 Boost 1.55,因此我在我的项目文件夹中创建了一个名为boost_1_55_0的文件夹,并将 Boost 文件夹从下载的版本复制到这个位置。
一旦用 Boost 的下载副本建立了项目文件夹,就可以将 Boost 头文件包含到源代码中。清单 1-7 显示了一个使用boost::format函数 的程序。
清单 1-7 。使用boost::format
#include <iostream>
#include "boost/format.hpp"
using namespace std;
int main()
{
std::cout << "Enter your first name: " << std::endl;
std::string firstName;
std::cin >> firstName;
std::cout << "Enter your surname: " << std::endl;
std::string surname;
std::cin >> surname;
auto formattedName = str( boost::format("%1% %2%"s) % firstName % surname );
std::cout << "You said your name is: " << formattedName << std::endl;
return 0;
}
清单 1-7 中的代码展示了如何在源文件中包含一个 Boost 头文件,以及如何在你的程序中使用该文件的函数。
注意不要担心
format函数如何工作,如果还不清楚的话,会在第三章中介绍。
您还必须告诉编译器在 makefile 中何处查找 Boost 头文件,否则您的程序将无法编译。清单 1-8 显示了可以用来构建这个程序的 makefile 文件的内容。
清单 1-8 。用 Boost 构建的 makefile
main: main.cpp
clang++ -g -std=c++1y -Iboost_1_55_0 main.cpp -o main
清单 1-8 中的 makefile 将–I选项传递给 Clang++。该选项用于告诉 Clang,当使用#include指令包含文件时,您希望在搜索路径中包含给定的文件夹。正如你所看到的,我已经通过了我在项目文件夹中创建的boost_1_55_0文件夹。该文件夹包含 boost 文件夹,您可以看到在清单 1-7 中包含一个 Boost 头时使用的文件夹。
注意如果您在运行这个示例时遇到问题,并且不确定应该将 Boost 头文件放在哪里,您可以从
www.apress.com/9781484201589下载本书附带的示例。
二、现代 C++
C++ 编程语言的开发始于 1979 年,当时称为带类的 C 语言。C++ 这个名字在 1983 年被正式采用,在没有采用正式语言标准的情况下,这种语言的发展一直持续到 20 世纪 80 年代和 90 年代。这一切在 1998 年改变了,当时采用了 C++ 编程语言的第一个 ISO 标准。自那时以来,该标准已经发布了三次更新,一次在 2003 年,再次在 2011 年,最近一次在 2014 年。
注意2003 年发布的标准是对 1998 年标准的微小更新,没有引入太多新功能。由于这个原因,在本书中不会详细讨论。
这本书主要关注最新的 C++ 编程标准,C++14。每当我提到 C++ 编程语言时,你可以放心,我说的是当前 ISO 标准所描述的语言。如果我讨论的是 2011 年引入的特性,那么我会明确地将该语言称为 C++11,而对于 2011 年之前引入的任何特性,我将使用 C++98 这个名称。
本章将着眼于最新标准和 C++11 中添加到语言中的编程特性。C++ 的许多现代特性都是在 C++11 标准中添加的,并在 C++14 标准中进行了扩展,因此,在使用支持非最新标准的编译器时,能够识别出它们之间的差异是非常重要的。
食谱 2-1。初始化变量
问题
您希望能够以标准方式初始化所有变量。
解决办法
统一初始化是在 C++11 中引入的,可以用来初始化任何类型的变量。
它是如何工作的
有必要了解 C++98 中变量初始化的缺陷,以理解为什么统一初始化是 C++11 中一个重要的语言特性。清单 2-1 显示了一个包含单个类MyClass的程序。
清单 2-1 。c++ 最令人烦恼的解析问题
class MyClass
{
private:
int m_Member;
public:
MyClass() = default;
MyClass(const MyClass& rhs) = default;
};
int main()
{
MyClass objectA;
MyClass objectB(MyClass());
return 0;
}
清单 2-1 中的代码会在 C++ 程序中产生一个编译错误。问题出在objectB的定义上。C++ 编译器不会认为这一行定义了一个名为objectB的类型为MyClass的变量,该变量调用一个构造函数,该构造函数接受通过调用MyClass构造函数构造的对象。这是您可能期望编译器看到的,然而它实际上看到的是一个函数声明。编译器认为这一行声明了一个名为objectB的函数,它返回一个MyClass对象,并且有一个单独的、未命名的函数指针指向一个返回一个MyClass对象的函数,并且没有传递任何参数。
编译清单 2-1 中显示的程序会导致 Clang 生成以下警告:
main.cpp:14:20: warning: parentheses were disambiguated as a function
declaration [-Wvexing-parse]
MyClass objectB(MyClass());
^~~~~~~~~~~
main.cpp:14:21: note: add a pair of parentheses to declare a variable
MyClass objectB(MyClass());
^
( )
Clang 编译器已经正确地识别出在清单 2-1 中输入的代码包含一个令人烦恼的解析问题,甚至建议将作为参数传递的MyClass构造函数包装在另一对括号中来解决这个问题。C++11 在统一初始化方面提供了另一种解决方案。你可以在的清单 2-2 中看到这一点。
清单 2-2 。使用统一初始化解决令人烦恼的解析问题
class MyClass
{
private:
int m_Member;
public:
MyClass() = default;
MyClass(const MyClass& rhs) = default;
};
int main()
{
MyClass objectA;
MyClass objectB{MyClass{}};
return 0;
}
你可以在清单 2-2 中看到,统一初始化用大括号代替了圆括号。这一语法变化通知编译器您希望使用统一初始化来初始化您的变量。统一初始化可以用来初始化几乎所有类型的变量。
注上一段提到可以用统一初始化来初始化几乎所有变量。在初始化聚集或普通的旧数据类型时可能会有问题,但是你现在不需要担心这些。
防止收缩转换的能力是使用统一初始化的另一个好处。当使用统一初始化时,清单 2-3 中的代码将无法编译。
清单 2-3 。使用统一初始化防止收缩转换
int main()
{
int number{ 0 };
char another{ 512 };
double bigNumber{ 1.0 };
float littleNumber{ bigNumber };
return 0;
}
编译清单 2-3 中的代码时,编译器会抛出错误,因为源代码中有两个收缩转换。第一种情况发生在尝试用文字值 512 定义 char 变量时。char 类型可以存储最大值 255,因此值 512 将缩小到此数据类型。由于这个错误,C++11 或更新的编译器将不会编译这个代码。从 double 类型初始化 float 也是一种收缩转换。当数据从一种类型传输到另一种类型时,如果目标类型无法存储源类型表示的所有值,就会发生收缩转换。在 double 转换为 float 的情况下,精度会丢失,因此编译器不会正确地按原样构建此代码。清单 2-4 中的代码使用一个static_cast来通知编译器收缩转换是有意的并编译代码。
清单 2-4 。使用 static_cast 编译收缩转换
int main()
{
int number{ 0 };
char another{ static_cast<char>(512) };
double bigNumber{ 1.0 };
float littleNumber{ static_cast<float>(bigNumber) };
return 0;
}
食谱 2-2。用初始化列表初始化对象
问题
您希望从给定类型的多个对象中构造对象。
解决办法
现代 C++ 提供了初始化列表,可用于向构造函数提供许多相同类型的对象。
它是如何工作的
C++11 中的初始化列表建立在统一初始化的基础上,允许你轻松初始化复杂类型。难以用数据初始化的复杂类型的一个常见例子是向量。清单 2-5 显示了对一个标准向量构造器的两个不同调用。
清单 2-5 。构造矢量对象
#include <iostream>
#include <vector>
using namespace std;
int main()
{
using MyVector = vector<int>;
MyVector vectorA( 1 );
cout << vectorA.size() << " " << vectorA[0] << endl;
MyVector vectorB( 1, 10 );
cout << vectorB.size() << " " << vectorB[0] << endl;
return 0;
}
清单 2-5 中的代码可能不会像你第一眼看到的那样。将使用包含 0 的单个int来初始化vectorA变量。您可能期望它包含一个包含 1 的整数,但这是不正确的。一个vector构造函数的第一个参数决定了初始vector将要存储多少个值,在这个例子中,我们要求它存储一个变量。你可能同样期望vectorB包含两个值,1 和 10,但是我们这里有一个包含一个值的vector,而这个值是 10。使用与vectorA相同的构造函数来构造vectorB变量,但是它指定一个值来实例化vector的成员,而不是使用默认值。
清单 2-6 中的代码使用初始化列表和统一初始化来构造一个包含两个指定值元素的向量。
清单 2-6 。使用统一初始化来构造一个vector
#include <iostream>
#include <vector>
using namespace std;
int main()
{
using MyVector = vector<int>;
MyVector vectorA( 1 );
cout << vectorA.size() << " " << vectorA[0] << endl;
MyVector vectorB( 1, 10 );
cout << vectorB.size() << " " << vectorB[0] << endl;
MyVector vectorC{ 1, 10 };
cout << vectorC.size() << " " << vectorC[0] << endl;
return 0;
}
清单 2-6 中的代码创建了三个不同的vector对象。你可以在图 2-1 的中看到这个程序生成的输出。
图 2-1 。清单 2-6 生成的输出
图 2-1 中所示的控制台输出显示了每个vector的大小以及存储在每个vector的第一个元素中的值。您可以看到第一个vector包含一个元素,其值为 0。第二个vector也包含一个元素,但是它的值是 10。第三个vector是使用统一初始化构建的,它包含两个值,第一个元素的值是 1。第二个元素的值将是 10。如果您没有特别注意确保对您的类型使用了正确的初始化类型,这可能会导致程序的行为发生重大变化。清单 2-7 中的代码显示了更加明确地使用initializer_list来构造vector。
清单 2-7 。显式初始值设定项 _ 列表用法
#include <iostream>
#include <vector>
using namespace std;
int main()
{
using MyVector = vector<int>;
MyVector vectorA( 1 );
cout << vectorA.size() << " " << vectorA[0] << endl;
MyVector vectorB( 1, 10 );
cout << vectorB.size() << " " << vectorB[0] << endl;
initializer_list<int> initList{ 1, 10 };
MyVector vectorC(initList);
cout << vectorC.size() << " " << vectorC[0] << endl;
return 0;
}
清单 2-7 中的代码包含一个显式的initializer_list,用于构造一个vector。清单 2-6 中的代码在使用统一初始化构建vector时隐式地创建了这个对象。通常很少需要显式地创建这样的初始化列表,但是当你使用统一初始化编写代码时,理解编译器在做什么是很重要的。
食谱 2-3。使用类型演绎
问题
您希望编写可移植的代码,在改变类型时维护成本不高。
解决办法
C++ 提供了 auto 关键字,可用于让编译器自动推断变量的类型。
它是如何工作的
C++98 编译器具有自动推断变量类型的能力,但是这种功能仅在您编写使用模板的代码时可用,并且您省略了类型专门化。现代 C++ 已经将这种类型的演绎支持扩展到更多的场景。清单 2-8 中的代码展示了使用auto关键字和typeid方法计算变量的类型。
清单 2-8 。使用auto关键字
#include <iostream>
#include <typeinfo>
using namespace std;
int main()
{
auto variable = 1;
cout << "Type of variable: " << typeid(variable).name() << endl;
return 0;
}
清单 2-8 中的代码展示了如何在 C++ 中创建一个具有自动推导类型的变量。编译器会自动计算出,你想用这段代码创建一个 int 变量,这就是程序输出的类型。Clang 编译器将输出其整数类型的内部表示,实际上是i。您可以将这个输出传递给一个名为c++filt的程序,将其转换成普通的 typename。图 2-2 显示了这是如何实现的。
图 2-2 。用 c++filt 从 Clang 中产生合适的类型输出
c++filt程序已经成功地将 Clang type i 转换为人类可读的 C++ 类型格式。auto 关键字也适用于类。清单 2-9 显示了这一点。
清单 2-9 。使用auto和class
#include <iostream>
#include <typeinfo>
using namespace std;
class MyClass
{
};
int main()
{
auto variable = MyClass();
cout << "Type of variable: " << typeid(variable).name() << endl;
return 0;
}
该程序将打印出名称 MyClass,如图 2-3 中的所示。
图 2-3 。对 MyClass 使用auto
不幸的是,有时候auto关键字产生的结果并不理想。如果你试图将关键字和统一初始化结合起来,你肯定会失败。清单 2-10 显示了统一初始化中自动关键字的使用。
清单 2-10 。使用auto进行统一初始化
#include <iostream>
#include <typeinfo>
using namespace std;
class MyClass
{
};
int main()
{
auto variable{ 1 };
cout << "Type of variable: " << typeid(variable).name() << endl;
auto variable2{ MyClass{} };
cout << "Type of variable: " << typeid(variable2).name() << endl;
return 0;
}
你可能认为清单 2-10 中的代码会产生一个 int 类型的变量和一个 MyClass 类型的变量,但事实并非如此。图 2-4 显示了程序生成的输出。
图 2-4 。使用 auto 和统一初始化时生成的输出
快速浏览一下图 2-4 显示了使用auto关键字和统一初始化时遇到的直接问题。C++ 统一初始化特性自动创建一个initializer_list变量,它包含我们想要的类型的值,而不是类型和值本身。这导致了一个相对简单的建议,当使用auto定义变量时,不要使用统一初始化。我建议不要使用auto,即使你想要的类型实际上是一个initializer_list,因为如果你不混合和匹配你的变量初始化风格,代码更容易理解,更不容易出错。记住最后一条建议,尽可能对局部变量使用 auto。不可能声明一个自动变量而不定义它,因此不可能有一个未定义的局部变量auto。你可以利用这些知识来减少程序中潜在的错误来源。
食谱 2-4。使用自动功能
问题
您希望使用类型演绎来创建更多的通用函数,以提高代码的可维护性。
解决办法
现代 C++ 允许对函数参数和返回类型使用类型演绎。
它是如何工作的
C++ 允许你在使用两种方法处理函数时使用类型演绎。通过创建一个模板函数并在没有显式特化器的情况下调用该函数,可以推导出函数参数的类型。使用 auto 关键字代替函数的返回类型,可以推导出函数的返回类型。清单 2-11 展示了使用 auto 来推导函数的返回类型。
清单 2-11 。使用auto推断函数的返回类型
#include <iostream>
using namespace std;
auto AutoFunctionFromReturn(int parameter)
{
return parameter;
}
int main()
{
auto value = AutoFunctionFromReturn(1);
cout << value << endl;
return 0;
}
清单 2-11 中函数的返回类型是自动推导出来的。编译器检查从函数返回的变量的类型,并使用它来推断要返回的类型。这一切都可以正常工作,因为编译器在函数中有了推导类型所需的一切。正在返回parameter变量,因此编译器可以使用它的类型作为函数的返回类型。
当你需要用 C++11 编译器编译时,事情变得有点复杂。使用 C++11 构建清单 2-11 会导致以下错误。
main.cpp:5:1: error: 'auto' return without trailing return type
auto AutoFunctionFromReturn(int parameter)
清单 2-12 包括一个在 C++11 中工作的自动返回类型演绎的函数。
清单 2-12 。C++11 中的返回类型演绎
#include <iostream>
using namespace std;
auto AutoFunctionFromReturn(int parameter) -> int
{
return parameter;
}
int main()
{
auto value = AutoFunctionFromReturn(1);
cout << value << endl;
return 0;
}
当你看到清单 2-12 中的代码时,你可能会奇怪为什么要这么做。当你总是指定函数的返回类型是一个 int 类型,而你是对的,那么推导函数的返回类型就没什么用了。返回类型推导在那些没有在签名中声明参数类型的函数中更有用。清单 2-13 展示了模板函数的类型演绎。
清单 2-13 。推导 C++11 模板函数的返回类型
#include <iostream>
using namespace std;
template <typename T>
auto AutoFunctionFromParameter(T parameter) -> decltype(parameter)
{
return parameter;
}
int main()
{
auto value = AutoFunctionFromParameter(2);
cout << value << endl;
return 0;
}
清单 2-13 展示了返回类型演绎的一个有用的应用。这一次,函数被指定为模板,因此编译器无法使用参数类型计算出返回类型。C++11 引入了decltype关键字来恭维auto关键字。decltype用来告诉编译器使用给定表达式的类型。表达式可以是一个变量名,但是你也可以在这里给一个函数,decltype 会推导出从函数返回的类型。
此时,代码又回到了起点。C++11 标准允许在函数上使用auto来推导返回类型,但是要求该类型仍然被指定为尾随返回类型。使用decltype可以推导出尾随的返回类型,但是这会导致过于冗长的代码。C++14 纠正了这种情况,允许 auto 用于没有尾随返回类型的函数,即使是和模板一起使用,正如你在清单 2-14 中看到的。
清单 2-14 。使用auto推断模板函数的返回类型
#include <iostream>
using namespace std;
template <typename T>
auto AutoFunctionFromParameter(T parameter)
{
return parameter;
}
int main()
{
auto value = AutoFunctionFromParameter(2);
cout << value << endl;
return 0;
}
食谱 2-5。使用编译时间常数
问题
您希望使用编译时间常数来优化程序的运行时操作。
解决办法
C++ 提供了constexpr关键字,可以用来保证表达式可以在编译时被求值。
它是如何工作的
constexpr关键字可以用来创建变量和函数,保证它们的求值可以在编译时进行。如果您向它们添加任何阻止编译时计算的代码,您的编译器将抛出一个错误。清单 2-15 显示了使用一个constexpr变量来定义一个array大小的程序。
清单 2-15 。使用constexpr定义一个array的大小
#include <array>
#include <cstdint>
#include <iostream>
int main()
{
constexpr uint32_t ARRAY_SIZE{ 5 };
std::array<uint32_t, ARRAY_SIZE> myArray{ 1, 2, 3, 4, 5 };
for (auto&& number : myArray)
{
std::cout << number << std::endl;
}
return 0;
}
清单 2-15 中的变量constexpr保证了该值可以在编译时被计算。这在这里是必要的,因为array的大小是在编译程序时必须确定的。清单 2-16 展示了如何扩展这个例子来包含一个constexpr函数。
清单 2-16 。一个constexpr功能
#include <array>
#include <cstdint>
#include <iostream>
constexpr uint32_t ArraySizeFunction(int parameter)
{
return parameter;
}
int main()
{
constexpr uint32_t ARRAY_SIZE{ ArraySizeFunction(5) };
std::array<uint32_t, ARRAY_SIZE> myArray{ 1, 2, 3, 4, 5 };
for (auto&& number : myArray)
{
std::cout << number << std::endl;
}
return 0;
}
你可以比清单 2-16 中的代码更进一步,用constexpr构造器创建一个类。这显示在清单 2-17 中。
清单 2-17 。创建 constexpr 类构造函数
#include <array>
#include <cstdint>
#include <iostream>
class MyClass
{
private:
uint32_t m_Member;
public:
constexpr MyClass(uint32_t parameter)
: m_Member{parameter}
{
}
constexpr uint32_t GetValue() const
{
return m_Member;
}
};
int main()
{
constexpr uint32_t ARRAY_SIZE{ MyClass{ 5 }.GetValue() };
std::array<uint32_t, ARRAY_SIZE> myArray{ 1, 2, 3, 4, 5 };
for (auto&& number : myArray)
{
std::cout << number << std::endl;
}
return 0;
}
清单 2-17 中的代码能够在constexpr语句中创建一个对象并调用一个方法。这是可能的,因为MyClass的构造函数被声明为constexpr构造函数。到目前为止,constexpr的代码已经与 C++11 编译器兼容。C++14 标准放宽了 C++11 中存在的许多限制。C++11 constexpr语句不允许做许多普通 C++ 代码可以做的事情。这些事情的例子有创建变量和使用if语句。清单 2-18 中的代码显示了一个 C++14 constexpr函数,它可以用来限制一个array的最大大小。
清单 2-18 。使用 C++14 constexpr函数
#include <array>
#include <cstdint>
#include <iostream>
constexpr uint32_t ArraySizeFunction(uint32_t parameter)
{
uint32_t value{ parameter };
if (value > 10 )
{
value = 10;
}
return value;
}
int main()
{
constexpr uint32_t ARRAY_SIZE{ ArraySizeFunction(15) };
std::array<uint32_t, ARRAY_SIZE> myArray{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
for (auto&& number : myArray)
{
std::cout << number << std::endl;
}
return 0;
}
清单 2-18 中的代码扩展了清单 2-16 中的 C++11 兼容代码,增加了一个声明变量并使用if语句的函数。用 C++11 编译器编译这段代码会导致下面的错误。
main.cpp:7:14: warning: variable declaration in a constexpr function is a C++1y extension [-Wc++1y-extensions]
uint32_t value{ parameter };
^
main.cpp:8:5: warning: use of this statement in a constexpr function is a C++1y extension [-Wc++1y-extensions]
if (value > 10 )
^
main.cpp:17:24: error: constexpr variable 'ARRAY_SIZE' must be initialized by a constant expression
constexpr uint32_t ARRAY_SIZE{ ArraySizeFunction(15) };
出现两个警告,表明constexpr函数不能在constexpr上下文中使用。这不是编译错误,因为该函数仍然可以在非constexpr上下文中使用。当函数被用来初始化一个constexpr变量时,实际的错误被抛出。
配方 2-6。使用 Lambdas
问题
你想编写利用未命名函数对象的程序。
解决办法
C++ 提供了 lambdas ,可以用来创建闭包,并且可以在代码中传递。
它是如何工作的
C++11 中引入的 lambda 语法一开始可能会有点混乱。清单 2-19 显示了一个简单的程序例子,它使用 lambda 来打印出一个数组中的所有值。
清单 2-19 。使用λ来打印array值
#include <algorithm>
#include <array>
#include <cstdint>
#include <iostream>
int main()
{
using MyArray = std::array<uint32_t, 5>;
MyArray myArray{ 1, 2, 3, 4, 5 };
std::for_each(myArray.cbegin(),
myArray.cend(),
[](auto&& number) {
std::cout << number << std::endl;
});
return 0;
}
这段代码展示了 lambda 是如何在 C++ 源代码中定义的。lambda 的语法如下:
[] () {};
大括号表示捕获块。lambda 使用一个捕获块来捕获要在 lambda 中使用的现有变量。清单 2-19 中的代码不需要捕获任何变量,因此它是空的。括号表示参数块,就像在普通函数中一样。清单 2-19 中的 lambda 有一个类型为auto&&的单一参数。std::for_each算法将给定的函数应用于序列中的每个元素。这里的函数恰好是编译器在遇到 lambda 语法并将其传递给for_each函数时创建的闭包。这里有一个微妙的术语差异,您应该熟悉一下。lambda 是定义匿名或未命名函数的源代码结构。编译器使用这个语法从 lambda 创建一个闭包对象。
闭包可以被一个变量引用,如清单 2-20 中的所示。
清单 2-20 。引用变量中的闭包
#include <algorithm>
#include <array>
#include <cstdint>
#include <iostream>
#include <typeinfo>
int main()
{
using MyArray = std::array<uint32_t, 5>;
MyArray myArray{ 1, 2, 3, 4, 5 };
auto myClosure = [](auto&& number) {
std::cout << number << std::endl;
};
std::cout << typeid(myClosure).name() << std::endl;
std::for_each(myArray.begin(),
myArray.end(),
myClosure);
return 0;
}
清单 2-20 中的例子将 lambda 捕获到一个自动类型的变量中。图 2-5 显示了由此产生的输出。
图 2-5 。该类型由typeid输出时传递一个闭包
图 2-5 显示了由清单 2-20 中的myClosure变量存储的闭包的类型。这里自动生成的类型并不特别有用,但是 C++ 确实提供了一种方法来传递任何类型的对象,这些对象可以像函数一样被调用。function模板在功能头中提供,是 STL 的一部分。这个模板接受对象所代表的函数的签名。你可以在清单 2-21 中看到这段代码。
清单 2-21 。将闭包传递给Function
#include <algorithm>
#include <array>
#include <cstdint>
#include <functional>
#include <iostream>
#include <typeinfo>
using MyArray = std::array<uint32_t, 5>;
void PrintArray(const std::function<void(MyArray::value_type)>& myFunction)
{
MyArray myArray{ 1, 2, 3, 4, 5 };
std::for_each(myArray.begin(),
myArray.end(),
myFunction);
}
int main()
{
auto myClosure = [](auto&& number) {
std::cout << number << std::endl;
};
std::cout << typeid(myClosure).name() << std::endl;
PrintArray(myClosure);
return 0;
}
你现在可以创建闭包并使用函数模板在你的程序中传递它们,如清单 2-21 所示。这允许你给你的程序添加一些在 C++98 中很难实现的东西。清单 2-22 展示了一种使用捕获块通过 lambda 将数组复制到vector中的方法。
清单 2-22 。使用 Lambda 捕获功能
#include <algorithm>
#include <array>
#include <cstdint>
#include <functional>
#include <iostream>
#include <typeinfo>
#include <vector>
using MyArray = std::array<uint32_t, 5>;
using MyVector = std::vector<MyArray::value_type>;
void PrintArray(const std::function<void(MyArray::value_type)>& myFunction)
{
MyArray myArray{ 1, 2, 3, 4, 5 };
std::for_each(myArray.begin(),
myArray.end(),
myFunction);
}
int main()
{
MyVector myCopy;
auto myClosure = &myCopy {
std::cout << number << std::endl;
myCopy.push_back(number);
};
std::cout << typeid(myClosure).name() << std::endl;
PrintArray(myClosure);
std::cout << std::endl << "My Copy: " << std::endl;
std::for_each(myCopy.cbegin(),
myCopy.cend(),
[](auto&& number){
std::cout << number << std::endl;
});
return 0;
}
清单 2-22 中的代码包含了一个 lambda 捕获的使用,用来在闭包中存储对对象myCopy的引用。然后可以在 lambda 中使用这个对象,并将数组的每个成员都推送到它上面。main函数通过打印由myCopy存储的所有值来结束,以表明由于引用捕获,闭包与 main 共享同一个vector。使用&操作符将捕获指定为参考捕获。如果省略的话,vector将被复制到闭包中,并且main中的myCopy vector将保持为空。
通过值而不是引用来捕获myCopy会导致另一个问题。编译器为 lambda 创建的类型将不再是与用于声明函数签名的参数兼容的参数。清单 2-23 显示了 lambda 使用值捕获来复制myCopy。
清单 2-23 。通过值捕获myCopy
#include <algorithm>
#include <array>
#include <cstdint>
#include <functional>
#include <iostream>
#include <typeinfo>
#include <vector>
using MyArray = std::array<uint32_t, 5>;
using MyVector = std::vector<MyArray::value_type>;
void PrintArray(const std::function<void(MyArray::value_type)>& myFunction)
{
MyArray myArray{ 1, 2, 3, 4, 5 };
std::for_each(myArray.begin(),
myArray.end(),
myFunction);
}
int main()
{
MyVector myCopy;
auto myClosure = myCopy {
std::cout << number << std::endl;
myCopy.push_back(number);
};
std::cout << typeid(myClosure).name() << std::endl;
PrintArray(myClosure);
std::cout << std::endl << "My Copy: " << std::endl;
std::for_each(myCopy.cbegin(),
myCopy.cend(),
[](auto&& number){
std::cout << number << std::endl;
});
return 0;
}
清单 2-23 中的代码不会被编译,你的编译器也不可能给你一个有意义或有帮助的错误信息。当试图在 Windows 上使用 CygwinT3 编译这段代码时,Clang 提供了以下错误输出。
$ make
clang++ -g -std=c++1y main.cpp -o main
main.cpp:26:13: error: no matching member function for call to 'push_back'
myCopy.push_back(number);
~~~~~~~^~~~~~~~~
/usr/lib/gcc/i686-pc-cygwin/4.9.2/include/c++/functional:2149:27: note: in instantiation of function template
specialization 'main()::<anonymous class>::operator()<unsigned int>' requested here
using _Invoke = decltype(__callable_functor(std::declval<_Functor&>())
^
/usr/lib/gcc/i686-pc-cygwin/4.9.2/include/c++/functional:2158:2: note: in instantiation of template type alias
'_Invoke' requested here
using _Callable
^
/usr/lib/gcc/i686-pc-cygwin/4.9.2/include/c++/functional:2225:30: note: in instantiation of template type alias
'_Callable' requested here
typename = _Requires<_Callable<_Functor>, void>>
^
/usr/lib/gcc/i686-pc-cygwin/4.9.2/include/c++/functional:2226:2: note: in instantiation of default argument for
'function<<lambda at main.cpp:24:22> >' required here
function(_Functor);
^~~~~~~~
/usr/lib/gcc/i686-pc-cygwin/4.9.2/include/c++/functional:2226:2: note: while substituting deduced template arguments
into function template 'function' [with _Functor = <lambda at main.cpp:24:22>, $1 = <no value>]
function(_Functor);
^
/usr/lib/gcc/i686-pc-cygwin/4.9.2/include/c++/bits/stl_vector.h:913:7: note: candidate function not viable: 'this'
argument has type 'const MyVector' (aka 'const vector<MyArray::value_type>'), but method is not marked const
push_back(const value_type& __x)
^
/usr/lib/gcc/i686-pc-cygwin/4.9.2/include/c++/bits/stl_vector.h:931:7: note: candidate function not viable: 'this'
argument has type 'const MyVector' (aka 'const vector<MyArray::value_type>'), but method is not marked const
push_back(value_type&& __x)
^
main.cpp:30:5: error: no matching function for call to 'PrintArray'
PrintArray(myClosure);
^~~~~~~~~~
main.cpp:12:6: note: candidate function not viable: no known conversion from '<lambda at main.cpp:24:22>' to 'const
std::function<void (MyArray::value_type)>' for 1st argument
void PrintArray(const std::function<void(MyArray::value_type)>& myFunction)
^
2 errors generated.
makefile:2: recipe for target 'main' failed
make: *** [main] Error 1
考虑到 Clang 输出的冗长且令人困惑的错误消息,您可能会认为代码远未处于工作状态,但是您可能会惊讶地发现这可以用一个关键字mutable来解决。清单 2-24 显示了处于正确编译状态的代码。
清单 2-24 。创建一个mutable闭包
#include <algorithm>
#include <array>
#include <cstdint>
#include <functional>
#include <iostream>
#include <typeinfo>
#include <vector>
using MyArray = std::array<uint32_t, 5>;
using MyVector = std::vector<MyArray::value_type>;
void PrintArray(const std::function<void(MyArray::value_type)>& myFunction)
{
MyArray myArray{ 1, 2, 3, 4, 5 };
std::for_each(myArray.begin(),
myArray.end(),
myFunction);
}
int main()
{
MyVector myCopy;
auto myClosure = myCopy mutable {
std::cout << number << std::endl;
myCopy.push_back(number);
};
std::cout << typeid(myClosure).name() << std::endl;
PrintArray(myClosure);
std::cout << std::endl << "My Copy: " << std::endl;
std::for_each(myCopy.cbegin(),
myCopy.cend(),
[](auto&& number){
std::cout << number << std::endl;
});
return 0;
}
清单 2-24 包含了你在上面看到的所有错误输出的解决方案。mutable 关键字用于告诉编译器 lambda 函数应该生成一个闭包,其中包含已经通过值复制的非const成员。
默认情况下,编译器在遇到 lambda 函数时创建的闭包是const。这导致编译器为闭包创建一个类型,该类型不能再隐式转换为标准函数指针。当您试图使用 lambda 函数来生成不适合您的代码的闭包时,编译器生成的错误消息可能会非常混乱,因此除了正确学习如何使用 lambda 函数并经常编译以发现您做出了编译器无法处理的更改之外,这里没有真正的解决方案。
在试图编译到目前为止在这个菜谱中看到的代码时,您可能会遇到的下一个问题是使用不支持 C++14 的 C++11 编译器进行编译。这里的问题是 c++ 11 lambda 不支持 auto 关键字作为参数。用 C++11 编译器构建清单 2-24 会产生以下输出。
clang++ -g -std=c++11 main.cpp -o main
main.cpp:24:31: error: 'auto' not allowed in lambda parameter
auto myClosure = myCopy mutable {
^~~~
main.cpp:30:5: error: no matching function for call to 'PrintArray'
PrintArray(myClosure);
^~~~~~~~~~
main.cpp:12:6: note: candidate function not viable: no known conversion from '<lambda at main.cpp:24:22>' to 'const
std::function<void (MyArray::value_type)>' for 1st argument
void PrintArray(const std::function<void(MyArray::value_type)>& myFunction)
^
main.cpp:35:5: error: 'auto' not allowed in lambda parameter
[](auto&& number){
^~~~
In file included from main.cpp:1:
In file included from /usr/lib/gcc/i686-pc-cygwin/4.9.2/include/c++/algorithm:62:
/usr/lib/gcc/i686-pc-cygwin/4.9.2/include/c++/bits/stl_algo.h:3755:2: error: no matching function for call to object
of type '<lambda at main.cpp:35:2>'
__f(*__first);
^~~
main.cpp:33:10: note: in instantiation of function template specialization
'std::for_each<__gnu_cxx::__normal_iterator<const unsigned int *, std::vector<unsigned int,
std::allocator<unsigned int> > >, <lambda at main.cpp:35:2> >' requested here
std::for_each(myCopy.cbegin(),
^
main.cpp:35:2: note: candidate template ignored: couldn't infer template argument '$auto-0-0'
[](auto&& number){
^
4 errors generated.
makefile:2: recipe for target 'main' failed
make: *** [main] Error 1
谢天谢地,这是一个比试图编译清单 2-23 时更清晰的消息,很明显 C++11 不支持 lambda 函数参数的自动类型推导。清单 2-25 显示了构建一个使用 lambda 函数将array复制到vector的工作程序所需的代码。
清单 2-25 。一个 C++11 兼容的 Lambda 函数
#include <algorithm>
#include <array>
#include <cstdint>
#include <functional>
#include <iostream>
#include <typeinfo>
#include <vector>
using MyArray = std::array<uint32_t, 5>;
using MyVector = std::vector<MyArray::value_type>;
void PrintArray(const std::function<void(MyArray::value_type)>& myFunction)
{
MyArray myArray{ 1, 2, 3, 4, 5 };
std::for_each(myArray.begin(),
myArray.end(),
myFunction);
}
int main()
{
MyVector myCopy;
auto myClosure = &myCopy {
std::cout << number << std::endl;
myCopy.push_back(number);
};
std::cout << typeid(myClosure).name() << std::endl;
PrintArray(myClosure);
std::cout << std::endl << "My Copy: " << std::endl;
std::for_each(myCopy.cbegin(),
myCopy.cend(),
[](const MyVector::value_type&number){
std::cout << number << std::endl;
});
return 0;
}
清单 2-25 中的代码在 C++11 编译器上运行良好,但是它确实导致了 lambda 函数在不同类型之间的可移植性稍差。用于打印来自myCopy的值的 lambda 函数现在只能与由MyVector::value_type定义的类型一起使用,而 C++14 版本可以与可以作为输入传递给cout的任何类型一起重用。
不用说,这些代码都不能用 C++98 编译器编译,因为 C++98 不支持 lambda 函数。
食谱 2-7。与时间一起工作
问题
你想写可移植的程序,知道当前时间或它们的执行时间。
解决办法
现代 C++ 提供了 STL 模板和类,这些模板和类提供了可移植的时间处理能力。
它是如何工作的
获取当前日期和时间
C++11 提供对给定计算机系统中不同实时时钟的访问。每个时钟的实现可能会有所不同,这取决于您运行的计算机系统本身,但是每个时钟的总体意图是相同的。您可以使用system_clock从系统范围的实时时钟中查询当前时间。这意味着当你的程序运行时,你可以使用这种类型的时钟来获取计算机的当前日期和时间。清单 2-26 展示了这是如何实现的。
清单 2-26 。获取当前日期和时间
#include <ctime>
#include <chrono>
#include <iostream>
using namespace std;
using namespace chrono;
int main()
{
auto currentTimePoint = system_clock::now();
auto currentTime = system_clock::to_time_t( currentTimePoint );
auto timeText = ctime( ¤tTime );
cout << timeText << endl;
return 0;
}
清单 2-26 中的程序展示了如何从system_clock获取当前时间。您可以使用system_clock::now方法 来完成这项工作。从 now 返回的对象是一个time_point,它包含了某个时期的时间偏移量。历元是系统用来补偿所有其他时间的参考时间。您将不必担心纪元,因为您所有的时间工作都使用同一个时钟。然而,你必须意识到,如果系统使用不同的时间,一台计算机的时间可能无法直接转移到另一台计算机。
time_point 结构不能直接打印出来,也没有方法将其转换为字符串,但是该类提供了一个方法将time_point对象转换为time_t对象。time_t 类型是一种旧的 C 类型,可以使用ctime函数转换成字符串表示。你可以在图 2-6 中看到运行该程序的结果。
图 2-6 。打印到终端的当前时间
比较时间
您还可以使用 STL 时间功能来比较时间。清单 2-27 展示了如何比较一个时间和另一个时间。
清单 2-27 。比较时间
#include <ctime>
#include <chrono>
#include <iostream>
#include <thread>
using namespace std;
using namespace chrono;
using namespace literals;
int main()
{
auto startTimePoint = system_clock::now();
this_thread::sleep_for(5s);
auto endTimePoint = system_clock::now();
auto timeTaken = duration_cast<milliseconds>(endTimePoint - startTimePoint);
cout << "Time Taken: " << timeTaken.count() << endl;
return 0;
}
清单 2-27 显示了你可以多次调用时钟上的now方法并获取不同的值。程序在startTimePoint变量中获取一个时间,然后在当前执行线程上调用sleep_for方法。这个调用导致程序休眠 5 秒钟,并在程序恢复后再次调用system_clock::now方法。此时,您有两个 time_point 对象,可用于将一个从另一个中减去。然后可以使用duration_cast将减法的结果转换成具有给定持续时间的具体时间。有效的持续时间类型有hours、minutes、seconds、milliseconds、microseconds和nanoseconds。然后在 duration 对象上使用count方法来获得调用now之间经过的实际毫秒数。
注意清单 2-27 中的代码使用了 C++14 标准用户定义的文字。传递给 sleep for 的 5s 定义了 5 秒的字面量。还有为
h(小时)min(分钟)s(秒)ms(毫秒)us(微秒)和ns(纳秒)定义的文字。这些字面值都可以应用于一个整数字面值,以通知编译器您想要创建一个具有给定时间类型的duration对象的字面值。将 s 应用于一个字符字面量,比如"A String"s,告诉编译器创建一个std::string类型的字面量。这些文字在std::literals名称空间中定义,是 C++14 独有的特性,这意味着它们不能在 C++11 或 C++98 代码中使用。
图 2-7 显示了该程序运行时产生的输出。
图 2-7 。清单 2-27 中的几次运行的输出
图 2-7 显示 sleep_for 方法并不是 100%准确,但是每次运行都相当接近 5000 毫秒。现在,您可以看到如何使用 now 方法来比较两个time_point,并且不难想象您可以创建一个if语句,该语句只在一定时间过后才执行。
食谱 2-8。理解左值和右值引用
问题
C++ 包含左值引用和右值引用的区别。您需要能够理解这些概念来编写最佳的 C++ 程序。
解决办法
现代 C++ 包含两种不同的引用操作符,&(左值)和&&(右值)。这些与移动语义一起减少了程序中复制对象所花费的时间。
它是如何工作的
移动语义是现代 C++ 编程语言的主要特征之一。它们的有用性被大大夸大了,不熟悉现代 C++ 编程的程序员可能倾向于一头扎进这个闪亮的新特性,实际上由于缺乏对何时以及为何使用右值引用而不是左值引用的理解,他们的程序变得更糟。
简而言之,右值引用应该用于移动构造或移动赋值对象,以在适当的时候代替复制操作。移动语义不应该用于替换通过常量引用向方法传递参数。移动操作可能比复制快,在最坏的情况下,它可能比复制慢,并且总是比通过常量引用传递慢。这个菜谱将向您展示左值引用、右值引用、复制和移动类构造函数和操作符之间的区别,并展示一些与它们相关的性能问题。
清单 2-28 中的代码显示了一个简单类的实现,它使用一个静态计数器值来跟踪在任何给定时间内存中对象的数量。
清单 2-28 。计算实例数量的类
#include <iostream>
using namespace std;
class MyClass
{
private:
static int s_Counter;
int* m_Member{ &s_Counter };
public:
MyClass()
{
++(*m_Member);
}
~MyClass()
{
--(*m_Member);
m_Member = nullptr;
}
int GetValue() const
{
return *m_Member;
}
};
int MyClass::s_Counter{ 0 };
int main()
{
auto object1 = MyClass();
cout << object1.GetValue() << endl;
{
auto object2 = MyClass();
cout << object2.GetValue() << endl;
}
auto object3 = MyClass();
cout << object3.GetValue() << endl;
return 0;
}
清单 2-28 中的s_Counter static成员计算在任何给定时间内存中存在的类的活动实例的数量。这是通过将static初始化为 0 并通过成员整数指针预递增MyClass构造函数中的值来实现的。在~MyClass中s_Counter值也会递减,以确保该数字不会失控。当您看到运行中的 move 构造函数时,对非常规设置的需求就变得很明显了。该程序生成的输出如图 2-8 所示。
图 2-8 。行动中的s_Counter变量
现在,您可以扩展MyClass来包含一个复制构造函数,并确定在任何给定时间它对内存中对象数量的影响。清单 2-29 显示了一个包含MyClass复制构造器的程序。
清单 2-29 。复制我的类
#include <iostream>
using namespace std;
class MyClass
{
private:
static int s_Counter;
int* m_Member{ &s_Counter };
public:
MyClass()
{
++(*m_Member);
cout << "Constructing: " << GetValue() << endl;
}
~MyClass()
{
--(*m_Member);
m_Member = nullptr;
cout << "Destructing: " << s_Counter << endl;
}
MyClass(const MyClass& rhs)
: m_Member{ rhs.m_Member }
{
++(*m_Member);
cout << "Copying: " << GetValue() << endl;
}
int GetValue() const
{
return *m_Member;
}
};
int MyClass::s_Counter{ 0 };
MyClass CopyMyClass(MyClass parameter)
{
return parameter;
}
int main()
{
auto object1 = MyClass();
{
auto object2 = MyClass();
}
auto object3 = MyClass();
auto object4 = CopyMyClass(object3);
return 0;
}
清单 2-29 中的代码添加了一个复制构造函数和一个将object3复制到object4中的函数。这需要两个副本,一个将object3复制到参数中,一个将参数复制到object4中。图 2-9 显示了两个复制操作已经发生,并且还有两个后续的析构函数被调用来销毁这些对象。
图 2-9 。复制运行中的构造函数
移动构造函数可以用来降低复制构造函数的复杂性。在运行中会有同样多的对象,但是您可以在 move 构造函数中安全地浅层复制一个对象,这要感谢它们所传递的右值引用类型。右值引用是编译器对变量引用的对象是临时对象的保证。这意味着您可以自由地分解对象,这样,与需要保留预先存在的状态相比,您可以更快地实现复制操作。清单 2-30 展示了如何添加一个移动构造函数到MyClass。
清单 2-30 。将移动构造函数添加到MyClass
#include <iostream>
using namespace std;
class MyClass
{
private:
static int s_Counter;
int* m_Member{ &s_Counter };
public:
MyClass()
{
++(*m_Member);
cout << "Constructing: " << GetValue() << endl;
}
~MyClass()
{
if (m_Member)
{
--(*m_Member);
m_Member = nullptr;
cout << "Destructing: " << s_Counter << endl;
}
else
{
cout << "Destroying a moved-from instance" << endl;
}
}
MyClass(const MyClass& rhs)
: m_Member{ rhs.m_Member }
{
++(*m_Member);
cout << "Copying: " << GetValue() << endl;
}
MyClass(MyClass&& rhs)
: m_Member{ rhs.m_Member }
{
cout << hex << showbase;
cout << "Moving: " << &rhs << " to " << this << endl;
cout << noshowbase << dec;
rhs.m_Member = nullptr;
}
int GetValue() const
{
return *m_Member;
}
};
int MyClass::s_Counter{ 0 };
MyClass CopyMyClass(MyClass parameter)
{
return parameter;
}
int main()
{
auto object1 = MyClass();
{
auto object2 = MyClass();
}
auto object3 = MyClass();
auto object4 = CopyMyClass(object3);
return 0;
}
清单 2-30 中的代码向 MyClass 添加了一个 move 构造函数。这对正在运行的代码有直接的影响。在图 2-10 中可以看到 move 构造函数正在被调用。
图 2-10 。使用移动构造函数
编译器已经意识到,在 return 语句结束后,不需要维护清单 2-30 中的参数状态。这意味着代码可以调用一个 move 构造函数来创建object4。这为代码中可能的优化创建了一个场景。这个例子很简单,因此对性能和内存的好处很小。如果这个类更复杂,那么你就可以节省同时在内存中存储两个对象所需的内存,以及从一个对象复制到另一个对象所需的时间。这样做的性能优势可以在清单 2-31 中看到。
清单 2-31 。比较复制构造函数和移动构造函数
#include <chrono>
#include <iostream>
#include <string>
#include <vector>
using namespace std;
using namespace chrono;
using namespace literals;
class MyClass
{
private:
vector<string> m_String{
"This is a pretty long string that"
" must be copy constructed into"
" copyConstructed!"s
};
int m_Value{ 1 };
public:
MyClass() = default;
MyClass(const MyClass& rhs) = default;
MyClass(MyClass&& rhs) = default;
int GetValue() const
{
return m_Value;
}
};
int main()
{
using MyVector = vector<MyClass>;
constexpr unsigned int ITERATIONS{ 1000000U };
MyVector copyConstructed(ITERATIONS);
int value{ 0 };
auto copyStartTime = high_resolution_clock::now();
for (unsigned int i=0; i < ITERATIONS; ++i)
{
MyClass myClass;
copyConstructed.push_back(myClass);
value = myClass.GetValue();
}
auto copyEndTime = high_resolution_clock::now();
MyVector moveConstructed(ITERATIONS);
auto moveStartTime = high_resolution_clock::now();
for (unsigned int i=0; i < ITERATIONS; ++i)
{
MyClass myClass;
moveConstructed.push_back(move(myClass));
value = myClass.GetValue();
}
auto moveEndTime = high_resolution_clock::now();
cout << value << endl;
auto copyDuration =
duration_cast<milliseconds>(copyEndTime - copyStartTime);
cout << "Copy lasted: " << copyDuration.count() << "ms" << endl;
auto moveDuration =
duration_cast<milliseconds>(moveEndTime - moveStartTime);
cout << "Move lasted: " << moveDuration.count() << "ms" << endl;
return 0;
}
清单 2-31 中的代码使用了default关键字来通知编译器我们想要使用这个类的默认构造函数、复制构造函数和移动构造函数。这在这里是有效的,因为MyClass不需要手动的内存管理或行为。我们只是想构造、复制或移动成员m_String和m_Value。m_Value变量用于防止编译器过度优化我们的例子并产生意想不到的结果。在图 2-11 中,你可以看到移动构造函数比复制构造函数更快。
图 2-11 。显示移动构造函数可能比复制构造函数更快
食谱 2-9。使用托管指针
问题
您希望在 C++ 程序中自动执行管理内存的任务。
解决办法
现代 C++ 提供了自动管理动态分配内存的能力。
它是如何工作的
使用唯一指针
C++ 提供了三种智能指针类型,可用于自动管理动态分配对象的生存期。清单 2-32 展示了一个unique_ ptr 的用法。
清单 2-32 。使用unique_ptr
#include <iostream>
#include <memory>
using namespace std;
class MyClass
{
private:
int m_Value{ 10 };
public:
MyClass()
{
cout << "Constructing!" << endl;
}
~MyClass()
{
cout << "Destructing!" << endl;
}
int GetValue() const
{
return m_Value;
}
};
int main()
{
unique_ptr<MyClass> uniquePointer{ make_unique<MyClass>() };
cout << uniquePointer->GetValue() << endl;
return 0;
}
清单 3-32 中的代码设法创建和销毁一个动态分配的对象,从来没有使用过new或delete。当unique_ptr实例超出范围时,make_unique模板处理调用new,而unique_ptr对象处理调用delete。不幸的是,make_unique 模板是 C++14 的一个特性,在 C++11 中不存在。清单 2-33 中的代码展示了如何纠正这一点。
清单 2-33 。创建您自己的make_unique
#include <iostream>
#include <memory>
using namespace std;
#if __cplusplus > 200400L && __cplusplus < 201200L
template <typename T, typename... Args>
unique_ptr<T> make_unique(Args... args)
{
return unique_ptr<T>{ new T(args...) };
}
#endif
class MyClass
{
private:
string m_Name;
int m_Value;
public:
MyClass(const string& name, int value)
: m_Name{ name }
, m_Value{ value }
{
cout << "Constructing!" << endl;
}
~MyClass()
{
cout << "Destructing!" << endl;
}
const string& GetName() const
{
return m_Name;
}
int GetValue() const
{
return m_Value;
}
};
int main()
{
unique_ptr<MyClass> uniquePointer{
make_unique<MyClass>("MyClass", 10) };
cout << uniquePointer->GetName() << endl;
cout << uniquePointer->GetValue() << endl;
return 0;
}
清单 2-33 中的代码使用了另一个 C++11 特性来创建一个 make_unique 模板。这个模板是一个可变的模板,它可以接受任意多的参数。这在 make unique 的调用中得到了证明,其中一个字符串和一个 int 被传递给了MyClass构造函数。__cplusplus预处理符号用于检测编译器正在编译的 C++ 版本。您可能需要确保这与您正在使用的编译器一起正常工作,因为并非所有编译器都能正确实现这一点。该代码将使用用户提供的make_unique模板在 C++11 中编译,并将使用标准提供的make_unique模板在 C++14 中编译。
唯一指针正如你所期望的那样,它们是唯一的,因此你的代码不能有一个以上的unique_ptr实例同时指向同一个对象。它通过阻止对unqiue_ptr实例的复制操作来实现这一点。然而一个unique_ptr可以被移动,这允许你在你的程序中传递一个unique_ptr。清单 2-34 展示了如何使用移动语义在你的程序中传递一个unqiue_ ptr 。
清单 2-34 。移动一个unqiue_ptr
#include <iostream>
#include <memory>
using namespace std;
class MyClass
{
private:
string m_Name;
int m_Value;
public:
MyClass(const string& name, int value)
: m_Name{ name }
, m_Value{ value }
{
cout << "Constructing!" << endl;
}
~MyClass()
{
cout << "Destructing!" << endl;
}
const string& GetName() const
{
return m_Name;
}
int GetValue() const
{
return m_Value;
}
};
using MyUniquePtr = unique_ptr<MyClass>;
auto PassUniquePtr(MyUniquePtr ptr)
{
cout << "In Function Name: " << ptr->GetName() << endl;
return ptr;
}
int main()
{
auto uniquePointer = make_unique<MyClass>("MyClass", 10);
auto newUniquePointer = PassUniquePtr(move(uniquePointer));
if (uniquePointer)
{
cout << "First Object Name: " << uniquePointer->GetName() << endl;
}
cout << "Second Object Name: " << newUniquePointer->GetName() << endl;
return 0;
}
清单 2-34 中的代码将一个unique_ptr实例移动到一个函数中。然后,该实例从函数中移回第二个unique_ptr对象。没有理由为什么相同的unique_ptr不能在 main 中使用,除非表明原来的实例在被移走后无效。这在检查指针是否有效的if调用中很明显,因为当代码被执行时,这将失败。可以以这种方式使用unique_ptr,一旦实例指向的对象超出范围而没有被移走,它将被删除。该程序的输出如图 2-12 所示。
图 2-12 。通过函数移动的有效unique_ptr实例
使用 shared_ptr 实例
一个unique_ptr可以给你一个对象的单独所有权,你可以在一个指针实例中移动,一个shared_ptr可以给你一个对象的共享所有权。这是通过让一个shared_ptr存储一个内部引用计数以及指向该对象的指针来实现的,只有当所有的值都超出范围时才删除该对象。清单 2-35 展示了一个shared_ ptr 的用法。
清单 2-35 。使用shared_ptr
#include <iostream>
#include <memory>
using namespace std;
class MyClass
{
private:
string m_Name;
int m_Value;
public:
MyClass(const string& name, int value)
: m_Name{ name }
, m_Value{ value }
{
cout << "Constructing!" << endl;
}
~MyClass()
{
cout << "Destructing!" << endl;
}
const string& GetName() const
{
return m_Name;
}
int GetValue() const
{
return m_Value;
}
};
using MySharedPtr = shared_ptr<MyClass>;
auto PassSharedPtr(MySharedPtr ptr)
{
cout << "In Function Name: " << ptr->GetName() << endl;
return ptr;
}
int main()
{
auto sharedPointer = make_shared<MyClass>("MyClass", 10);
{
auto newSharedPointer = PassSharedPtr(sharedPointer);
if (sharedPointer)
{
cout << "First Object Name: " << sharedPointer->GetName() << endl;
}
cout << "Second Object Name: " << newSharedPointer->GetName() << endl;
}
return 0;
}
清单 2-35 中的shared_ptr与您之前看到的unique_ptr有所不同。一个shared_ptr可以通过你的程序被复制,你可以有多个指针指向同一个对象。这可以在图 2-13 中看到,这里可以看到第一个对象名语句的输出。
图 2-13 。使用shared_ptr
使用弱指针
现代 C++ 也允许你持有智能指针的弱引用。只要共享对象存在,您就可以在需要时临时获取指向共享对象的指针的引用。清单 2-36 展示了如何使用weak_ ptr 来实现这一点。
清单 2-36 。使用弱指针
#include <iostream>
#include <memory>
using namespace std;
class MyClass
{
private:
string m_Name;
int m_Value;
public:
MyClass(const string& name, int value)
: m_Name{ name }
, m_Value{ value }
{
cout << "Constructing!" << endl;
}
~MyClass()
{
cout << "Destructing!" << endl;
}
const string& GetName() const
{
return m_Name;
}
int GetValue() const
{
return m_Value;
}
};
using MySharedPtr = shared_ptr<MyClass>;
using MyWeakPtr = weak_ptr<MyClass>;
auto PassSharedPtr(MySharedPtr ptr)
{
cout << "In Function Name: " << ptr->GetName() << endl;
return ptr;
}
int main()
{
MyWeakPtr weakPtr;
{
auto sharedPointer = make_shared<MyClass>("MyClass", 10);
weakPtr = sharedPointer;
{
auto newSharedPointer = PassSharedPtr(sharedPointer);
if (sharedPointer)
{
cout << "First Object Name: " << sharedPointer->GetName() << endl;
}
cout << "Second Object Name: " << newSharedPointer->GetName() << endl;
auto sharedFromWeak1 = weakPtr.lock();
if (sharedFromWeak1)
{
cout << "Name From Weak1: " << sharedFromWeak1->GetName() << endl;
}
}
}
auto sharedFromWeak2 = weakPtr.lock();
if (!sharedFromWeak2)
{
cout << "Shared Pointer Out Of Scope!" << endl;
}
return 0;
}
你可以在清单 2-36 中看到,一个weak_ptr可以被分配一个shared_ptr,但是你不能通过弱指针直接访问共享对象。相反,弱指针提供了一个lock方法。lock 方法返回一个指向您正在引用的对象的shared_ptr实例。如果 shared_ptr 最终成为指向该对象的最后一个对象,那么它会在整个作用域内保持该对象的活动状态。lock方法总是返回一个shared_ptr,但是如果对象不再存在,lock 返回的shared_ptr将无法通过if测试。你可以在删除对象后调用lock的主函数的末尾看到这一点。图 2-14 显示发生这种情况后weak_ptr无法获得有效的shared_ptr。
图 2-14 。未能lock一个被删除的对象