0%

简介

  • CPU(Central Processing Unit),即中央处理器
  • GPU(Graphics Processing Unit), 即图形处理器
  • TPU(Tensor Processing Unit), 即张量处理器(谷歌)
  • NPU(Neual network Processing Unit),即神经网络处理器

概括三者的区别

  • CPU虽然有多核,但一般也就几个,每个核都有足够大的缓存和足够多的数字和逻辑运算单元,需要很强的通用性来处理各种不同的数据类型,同时又要逻辑判断又会引入大量的分支跳转和中断的处理,并辅助有很多加速分支判断甚至更复杂的逻辑判断的硬件;
  • GPU的核数远超CPU,被称为众核(NVIDIA Fermi有512个核)。每个核拥有的缓存大小相对小,数字逻辑运算单元也少而简单(GPU初始时在浮点计算上一直弱于CPU),面对的则是类型高度统一的、相互无依赖的大规模数据和不需要被打断的纯净的计算环境
  • TPU是一款为机器学习而定制的芯片,经过了专门深度机器学习方面的训练,它有更高效能(每瓦计算能力)。大致上,相对于现在的处理器有7年的领先优势,宽容度更高,每秒在芯片中可以挤出更多的操作时间,使用更复杂和强大的机器学习模型,将之更快的部署,用户也会更加迅速地获得更智能的结果
  • 所谓NPU, 即神经网络处理器,用电路模拟人类的神经元和突触结构

CPU

  • CPU(CentralProcessing Unit)中央处理器,是一块超大规模的集成电路,主要逻辑架构包括控制单元Control,运算单元ALU和高速缓冲存储器(Cache)及实现它们之间联系的数据(Data)、控制及状态的总线(Bus)。简单说,就是计算单元、控制单元和存储单元。
  • CPU遵循的是冯·诺依曼架构,其核心是存储程序/数据、串行顺序执行。因此CPU的架构中需要大量的空间去放置存储单元(Cache)和控制单元(Control),相比之下计算单元(ALU)只占据了很小的一部分,所以CPU在进行大规模并行计算方面受到限制,相对而言更擅长于处理逻辑控制。
  • CPU无法做到大量数据并行计算的能力,但GPU可以。

GPU

  • GPU(GraphicsProcessing Unit),即图形处理器,是一种由大量运算单元组成的大规模并行计算架构,早先由CPU中分出来专门用于处理图像并行计算数据,专为同时处理多重并行计算任务而设计。
  • GPU中也包含基本的计算单元、控制单元和存储单元,但GPU的架构与CPU有很大不同。
  • 与CPU相比,CPU芯片空间的不到20%是ALU,而GPU芯片空间的80%以上是ALU。即GPU拥有更多的ALU用于数据并行处理。
  • GPU具有如下特点:
    • 多线程,提供了多核并行计算的基础结构,且核心数非常多,可以支撑大量数据的并行计算,处理神经网络数据远远高效于CPU。
    • 拥有更高的访存速度。
    • 更高的浮点运算能力。
  • 因此,GPU比CPU更适合深度学习中的大量训练数据、大量矩阵、卷积运算。
  • GPU虽然在并行计算能力上尽显优势,但并不能单独工作,需要CPU的协同处理,对于神经网络模型的构建和数据流的传递还是在CPU上进行。
  • 但是GPU也有天生缺陷,那就是功耗高,体积大,价格贵。
  • 性能越高的GPU体积越大,功耗越高,价格也昂贵,对于一些小型设备、移动设备来说将无法使用。
  • 因此,一种体积小、功耗低、计算性能高、计算效率高的ASIC专用芯片NPU诞生了。

NPU

  • NPU (NeuralNetworks Process Units)神经网络处理单元。其针对于矩阵运算进行了专门的优化设计,解决了传统芯片在神经网络运算时效率低下的问题。NPU工作原理是在电路层模拟人类神经元和突触,并且用深度学习指令集直接处理大规模的神经元和突触,一条指令完成一组神经元的处理。相比于CPU和GPU,NPU通过突出权重实现存储和计算一体化,从而提高运行效率。
  • 神经网络处理器(NPU)采用“数据驱动并行计算”的架构,特别擅长处理视频、图像类的海量多媒体数据。NPU处理器专门为物联网人工智能而设计,用于加速神经网络的运算,解决传统芯片在神经网络运算时效率低下的问题。
  • NPU是模仿生物神经网络而构建的,CPU、GPU处理器需要用数千条指令完成的神经元处理,NPU只要一条或几条就能完成,因此在深度学习的处理效率方面优势明显。
  • 神经网络中存储和处理是一体化的,都是通过突触权重来体现。 冯·诺伊曼结构中,存储和处理是分离的,分别由存储器和运算器来实现,二者之间存在巨大的差异。当用现有的基于冯·诺伊曼结构的经典计算机(如X86处理器和英伟达GPU)来跑神经网络应用时,就不可避免地受到存储和处理分离式结构的制约,因而影响效率。这也就是专门针对人工智能的专业芯片能够对传统芯片有一定先天优势的原因之一。

CPU 如何辅助NPU实现加速

  • NPU与GPU加速不同,主要体现为每层神经元计算结果不用输出到主内存,而是按照神经网络的连接传递到下层神经元继续计算,因此其在运算性能和功耗上都有很大的提升。
  • CPU将编译好的神经网络模型文件和权重文件交由专用芯片加载,完成硬件编程

关于RK1126平台项目的小思考

  • 在瑞芯微的1126芯片上有NPU,我接触的有四种检测模型:安全帽检测,区域入侵检测,反光衣检测,烟火检测。软件具体运行流程为:底层视频结构化引擎运行,后端服务运行,将模型配置文件发送给引擎,加载检测模型,将流媒体地址发送给引擎,引擎将视频流解码为一帧帧图片,将图片处理为检测模型需要的格式,这一步一般称为预处理,由检测模型返回检测结果,引擎处理成结构化数据。

  • 检测模型就是神经网络模型文件和权重文件。底层引擎通过平台提供的接口,将检测模型加载到NPU上。

简介

  • 在C++编程中, NaN(Not a number)是浮点数类型(例如float, double, long double)中的一种特殊值,用于表示无法定义的数值或者操作结果。NaN在数值计算和科学计算中十分常见,正确理解和处理NaN对于编写健壮和可靠的代码至关重要。
Read more »

linux rsync命令是什么

rsync 是一个用于在本地或远程同步文件和目录的开源实用工具。它非常强大和灵活,支持递归传输、保持文件权限、压缩传输等功能,使其成为文件同步和备份的首选工具之一。

以下是 rsync 的一些常用选项和示例:

常用选项

  • -a, --archive:归档模式,等同于 -rlptgoD,保留文件的权限、时间戳、符号链接等。
  • -v, --verbose:详细模式,显示传输过程中的详细信息。
  • -z, --compress:在传输过程中压缩文件以节省带宽。
  • -P:显示传输进度并部分保留已传输的文件(等同于 --partial --progress)。
  • --exclude:排除指定的文件或目录。
  • --delete:删除目标目录中在源目录中不存在的文件。

基本用法

本地复制

1
rsync -av /source/directory /destination/directory

/source/directory 复制到 /destination/directory

远程复制

1
rsync -av /local/directory user@remote_host:/remote/directory

将本地目录 /local/directory 复制到远程主机 remote_host/remote/directory

1
rsync -av user@remote_host:/remote/directory /local/directory

将远程主机 remote_host/remote/directory 复制到本地目录 /local/directory

使用 --exclude 排除特定目录

1
rsync -av --exclude 'exclude_directory' /source/directory user@remote_host:/remote/directory

将本地目录 /source/directory 复制到远程主机 remote_host/remote/directory,但排除 exclude_directory 目录。

示例

  1. 同步本地目录

    1
    rsync -av /home/user/data /backup

    /home/user/data 目录同步到 /backup 目录。

  2. 排除特定目录

    1
    rsync -av --exclude 'tmp' /home/user/data /backup

    /home/user/data 目录同步到 /backup 目录,但排除 tmp 目录。

  3. 同步到远程主机

    1
    rsync -avz /home/user/data user@remote_host:/backup

    将本地 /home/user/data 目录同步到远程主机 remote_host/backup 目录,并在传输过程中进行压缩。

  4. 从远程主机同步到本地

    1
    rsync -avz user@remote_host:/backup /home/user/data

    将远程主机 remote_host/backup 目录同步到本地的 /home/user/data 目录,并在传输过程中进行压缩。

  5. 显示进度

    1
    rsync -avP /home/user/data user@remote_host:/backup

    将本地 /home/user/data 目录同步到远程主机 remote_host/backup 目录,并显示传输进度。

结论

rsync 是一个非常强大且灵活的工具,适用于各种文件同步和备份需求。通过熟练使用 rsync 的各种选项,你可以高效地完成复杂的文件传输和同步任务。

linux rsync命令 详解

rsync 是一个强大的文件同步工具,常用于在本地或远程主机之间复制和同步文件和目录。它支持增量传输、保持文件权限、压缩传输等功能。以下是 rsync 的详细说明和一些示例。

基本语法

1
rsync [OPTION]... SRC [SRC]... DEST
  • SRC:源文件或目录
  • DEST:目标文件或目录

常用选项

  • -a, --archive:归档模式,相当于 -rlptgoD,保留文件属性。
    • -r, --recursive:递归处理目录。
    • -l, --links:保留符号链接。
    • -p, --perms:保留文件权限。
    • -t, --times:保持文件修改时间。
    • -g, --group:保留文件组信息。
    • -o, --owner:保留文件所有者信息。
    • -D:保留设备文件和特殊文件。
  • -v, --verbose:详细输出。
  • -z, --compress:在传输过程中压缩文件。
  • -P:显示传输进度和部分传输的文件(等同于 --partial --progress)。
    • --partial:保留部分传输的文件,以便恢复。
    • --progress:显示传输进度。
  • --exclude:指定排除的文件或目录模式。
  • --delete:删除目标目录中在源目录中不存在的文件。
  • --bwlimit=RATE:限制 I/O 带宽,单位为 KB/s。
  • -e, --rsh=COMMAND:指定远程 shell 程序。
  • --checksum:基于文件校验和进行比较,而不是文件大小和修改时间。

示例

本地复制

将本地目录 /source/directory 复制到 /destination/directory

1
rsync -av /source/directory /destination/directory

远程复制

将本地目录 /local/directory 复制到远程主机 remote_host/remote/directory

1
rsync -av /local/directory user@remote_host:/remote/directory

将远程主机 remote_host/remote/directory 复制到本地目录 /local/directory

1
rsync -av user@remote_host:/remote/directory /local/directory

排除特定目录

将本地目录 /source/directory 复制到远程主机 remote_host/remote/directory,但排除 exclude_directory

1
rsync -av --exclude 'exclude_directory' /source/directory user@remote_host:/remote/directory

显示进度

在传输过程中显示进度:

1
rsync -avP /source/directory user@remote_host:/remote/directory

同步文件并删除目标目录中多余的文件

将本地目录 /source/directory 同步到远程主机 remote_host/remote/directory,并删除目标目录中多余的文件:

1
rsync -av --delete /source/directory user@remote_host:/remote/directory

限制传输带宽

将本地目录 /source/directory 复制到远程主机 remote_host/remote/directory,并限制传输带宽为 500 KB/s:

1
rsync -avz --bwlimit=500 /source/directory user@remote_host:/remote/directory

使用 SSH 进行传输

通过 SSH 进行文件传输:

1
rsync -avz -e ssh /source/directory user@remote_host:/remote/directory

基于校验和进行比较

基于文件校验和进行比较,而不是文件大小和修改时间:

1
rsync -avz --checksum /source/directory user@remote_host:/remote/directory

结论

rsync 是一个非常强大且灵活的工具,可以满足各种文件同步和备份需求。通过掌握 rsync 的各种选项和用法,你可以高效地完成复杂的文件传输和同步任务。

linux rsync命令 拷贝目录并且忽略指定目录

在 Linux 上使用 rsync 拷贝目录并且忽略指定目录的具体步骤如下。假设你要拷贝 /source/directory 到目标目录 /destination/directory,但要忽略 ignore_directory 目录。

示例命令

1
rsync -av --exclude 'ignore_directory' /source/directory/ /destination/directory/

详细说明

  • -a, --archive:归档模式,表示递归复制目录,并保持文件属性。
  • -v, --verbose:详细模式,显示传输过程中的信息。
  • --exclude 'ignore_directory':指定要排除的目录 ignore_directory
  • /source/directory/:源目录路径,末尾的 / 表示拷贝目录内的内容。
  • /destination/directory/:目标目录路径,末尾的 / 表示将内容复制到该目录中。

更详细的例子

假设有如下目录结构:

1
2
3
4
5
6
/source/directory/
├── file1.txt
├── file2.txt
└── ignore_directory/
├── file3.txt
└── file4.txt

使用上述 rsync 命令:

1
rsync -av --exclude 'ignore_directory' /source/directory/ /destination/directory/

结果是:

1
2
3
/destination/directory/
├── file1.txt
└── file2.txt

其他选项

  • 如果你有多个目录或文件需要排除,可以使用多个 --exclude 选项:
1
rsync -av --exclude 'ignore_directory1' --exclude 'ignore_directory2' /source/directory/ /destination/directory/
  • 你也可以使用 --exclude-from 选项从文件中读取排除的目录和文件列表。假设排除列表文件 exclude-list.txt 内容如下:
1
2
3
ignore_directory1
ignore_directory2
ignore_file.txt

使用命令:

1
rsync -av --exclude-from='exclude-list.txt' /source/directory/ /destination/directory/

结论

通过以上方法,你可以使用 rsync 高效地复制目录,同时忽略指定的目录或文件。这些选项提供了灵活的排除机制,使 rsync 成为处理文件同步和备份任务的强大工具。

linux rsync命令 远程拷贝目录并且忽略指定目录

使用 rsync 命令可以实现远程拷贝目录并且忽略指定目录。以下是详细步骤和示例。

示例命令

假设你要将本地的 /source/directory 目录拷贝到远程主机 remote_host/destination/directory,但要忽略 ignore_directory 目录。

1
rsync -av --exclude 'ignore_directory' /source/directory/ user@remote_host:/destination/directory/

详细说明

  • -a, --archive:归档模式,表示递归复制目录,并保持文件属性。
  • -v, --verbose:详细模式,显示传输过程中的信息。
  • --exclude 'ignore_directory':指定要排除的目录 ignore_directory
  • /source/directory/:源目录路径,末尾的 / 表示拷贝目录内的内容。
  • user@remote_host:/destination/directory/:目标目录路径,格式为 user@host:/path

更详细的例子

假设有如下本地目录结构:

1
2
3
4
5
6
/source/directory/
├── file1.txt
├── file2.txt
└── ignore_directory/
├── file3.txt
└── file4.txt

使用上述 rsync 命令:

1
rsync -av --exclude 'ignore_directory' /source/directory/ user@remote_host:/destination/directory/

结果是远程主机 remote_host 上的 /destination/directory/ 目录结构为:

1
2
3
/destination/directory/
├── file1.txt
└── file2.txt

使用多个排除选项

如果你有多个目录或文件需要排除,可以使用多个 --exclude 选项:

1
rsync -av --exclude 'ignore_directory1' --exclude 'ignore_directory2' /source/directory/ user@remote_host:/destination/directory/

使用排除列表文件

你也可以使用 --exclude-from 选项从文件中读取排除的目录和文件列表。假设排除列表文件 exclude-list.txt 内容如下:

1
2
3
ignore_directory1
ignore_directory2
ignore_file.txt

使用命令:

1
rsync -av --exclude-from='exclude-list.txt' /source/directory/ user@remote_host:/destination/directory/

结论

通过以上方法,你可以使用 rsync 高效地实现远程目录拷贝,同时忽略指定的目录或文件。这些选项提供了灵活的排除机制,使 rsync 成为处理远程文件同步和备份任务的强大工具。

简介

  • 数据结构相关笔记

  • 数据结构(data structure)是计算机中存储,组织数据的方式

  • 数据结构是一种具有一定逻辑关系,在计算机中应用某种数据结构,并且封装了相应操作的数据元素集合。它包含三方面的内容,逻辑关系,存储关系及操作。

  • 不同种类的数据结构适合于不同种类的应用,而部分甚至专门用于特定的作业任务。例如,计算机网络依赖于路由表运作,B树高度适用于数据库的封装。

  • 数据结构研究的内容:就是如何按一定的逻辑结构,把数据组织起来,并选择适当的存储表示方法把逻辑结构组织好的数据存储到计算机的存储器里。

常见的数据结构

  • 栈(stack): 栈是一种特殊的线性表,它只能在一个表的一个固定端进行数据节点的插入和删除操作
  • 队列(queue):队列和栈类似,也是一种特殊的线性表。和栈不同的是,队列只允许在表的一端进行插入操作,而在另一端进行删除操作
  • 数组(array):数据是一种聚合数据类型,它是将具有相同相同类型的若干变量有序的组织在一起的集合。
  • 链表(linked list):链表是一种数据元素按照链式存储结构进行存储的数据结构,这种存储结构具有在物理上存在非连续的特点。
  • 树(tree):树是典型的非线性结构,它是包括,2 个结点的有穷集合 K
  • 图(graph):图是另一种非线性数据结构。在图结构中,数据结点一般称为顶点,而边是顶点的有序偶对。
  • 堆(heap):堆是一种特殊的树形数据结构,一般讨论的堆都是二叉堆。
  • 散列表(hash table):散列表源自于散列函数(Hash function),其思想是如果在结构中存在关键字和T相等的记录,那么必定在F(T)的存储位置可以找到该记录,这样就可以不用进行比较操作而直接取得所查记录。

简介

  • 数据结构研究的内容:就是如何按一定的逻辑结构,把数据组织起来,并选择适当的存储表示方法把逻辑结构组织好的数据存储到计算机的存储器里。
  • 算法研究的目的是为了更有效的处理数据,提高数据运算效率。数据的运算是定义在数据的逻辑结构上,但是运算的具体实现要在存储结构上进行。一般有以下几种常用运算
    • 检索:检索就是在数据结构里查找满足一定条件的节点。一般是给定一个某字段的值,找具有该字段的节点
    • 插入:往数据结构中增加新的节点
    • 删除:把指定的节点从数据结构中去掉
    • 更新:改变指定节点的一个或者多个字段的值
    • 排序:把节点按某种指定的顺序重新排列。例如递增或者递减

简介

  • os模块相关笔记

python3 os模块 详解

os模块是Python中用于与操作系统进行交互的标准库之一。它提供了许多函数来执行文件和目录管理,处理文件路径,以及与操作系统交互的其他功能。下面是对os模块的一些主要功能的详解:

文件和目录操作

  1. 创建目录

    • os.mkdir(path): 创建单级目录。
    • os.makedirs(path): 递归创建多级目录。
  2. 删除目录

    • os.rmdir(path): 删除指定目录。
    • os.removedirs(path): 递归删除目录,直到指定目录。
  3. 文件和目录存在性检查

    • os.path.exists(path): 检查文件或目录是否存在。
    • os.path.isfile(path): 检查给定路径是否是文件。
    • os.path.isdir(path): 检查给定路径是否是目录。
  4. 重命名和移动

    • os.rename(src, dst): 重命名文件或目录。
    • os.replace(src, dst): 替换文件或目录,如果目标已经存在。
  5. 列出目录内容

    • os.listdir(path): 返回目录中的所有文件和目录的列表。
  6. 删除文件

    • os.remove(path): 删除指定文件。

路径操作

  1. 连接路径

    • os.path.join(path1, path2, ...): 将多个路径组合成一个路径。
  2. 获取文件名和目录名

    • os.path.basename(path): 返回路径的基本名称(文件名或目录名)。
    • os.path.dirname(path): 返回路径的目录部分。
  3. 获取路径信息

    • os.path.abspath(path): 返回路径的绝对路径。
    • os.path.split(path): 将路径拆分为目录和文件名的元组。
    • os.path.splitext(path): 将路径拆分为文件名和扩展名的元组。
  4. 检查路径属性

    • os.path.isabs(path): 检查路径是否是绝对路径。
    • os.path.islink(path): 检查路径是否是符号链接。
    • os.path.realpath(path): 返回符号链接的绝对路径。

其他操作

  1. 执行系统命令

    • os.system(command): 在系统上执行命令。
  2. 获取环境变量

    • os.getenv(name): 获取环境变量的值。
    • os.putenv(name, value): 设置环境变量的值。
  3. 进程控制

    • os.getpid(): 获取当前进程的PID。
    • os.kill(pid, signal): 向指定PID的进程发送信号。
  4. 文件描述符操作

    • os.open(file, flags[, mode]): 打开文件并返回文件描述符。
    • os.close(fd): 关闭文件描述符。

这些只是os模块中的一部分功能,但覆盖了许多常见的文件和目录操作,以及与操作系统进行交互的基本操作。

VSCode 基础

vscode c_cpp_properties.json excludePath参数 详解

在Visual Studio Code中,c_cpp_properties.json文件是用于配置C/C++扩展的解析设置的文件。在该文件中,excludePath参数用于指定应该在解析期间排除的目录。以下是对excludePath参数的详细解释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"configurations": [
{
"name": "My Configuration",
"includePath": [
"${workspaceFolder}/**"
],
"defines": [],
"compilerPath": "/usr/bin/gcc",
"cStandard": "c11",
"cppStandard": "c++17",
"intelliSenseMode": "gcc-x64",
"excludePath": [
"${workspaceFolder}/path/to/excluded_directory",
"${workspaceFolder}/another/excluded_directory/**/*.cpp"
]
}
],
"version": 4
}
  1. ${workspaceFolder}: 这是一个变量,表示当前工作区的根目录。${workspaceFolder}/**表示将包括工作区中的所有文件和子目录。

  2. excludePath数组: 用于指定要在解析期间排除的目录。可以指定一个或多个目录,并使用相对于${workspaceFolder}的路径。在上面的示例中,两个目录都被排除了。

    • ${workspaceFolder}/path/to/excluded_directory: 表示要排除的第一个目录。
    • ${workspaceFolder}/another/excluded_directory/**/*.cpp: 使用通配符**表示所有子目录和其中的所有.cpp文件都应该被排除。
  3. 注意事项:

    • 在配置excludePath时,确保使用正确的相对路径,并确保路径是以${workspaceFolder}为基础的。
    • 你可以使用通配符**来匹配任意目录或文件。在示例中,/**/*.cpp表示匹配任意子目录下的所有.cpp文件。
    • 更改完成后,需要重启Visual Studio Code以使更改生效。

通过使用excludePath参数,你可以控制C/C++扩展在解析代码时忽略特定的目录,这对于排除不需要进行代码分析的目录非常有用。

解决cpptools high cpu问题

  • 在VScode菜单栏文件->首选项->设置 搜索C_Cpp.intelliSenseCacheSize,修改默认的5120为512

调试模式 设置环境变量

在Visual Studio Code (VSCode) 中,launch.json 文件用于配置调试器的启动选项。当您在VSCode中使用C++进行调试时,可以使用 launch.json 文件设置环境变量以定制调试过程。

以下是如何在 launch.json 文件中设置环境变量的步骤:

  1. 打开 VSCode,并在您的 C++ 项目文件夹中找到或创建 launch.json 文件。通常,该文件位于 .vscode 文件夹中。

  2. configurations 字段中添加一个调试配置。如果该字段为空,请复制以下 JSON 代码并粘贴到 launch.json 文件中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"version": "0.2.0",
"configurations": [
{
"name": "C++ Launch",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/<your_executable>",
"args": [],
"stopAtEntry": false,
"cwd": "${workspaceFolder}",
"environment": [],
"externalConsole": true
}
]
}
  1. 在上面的代码中,environment 字段用于设置环境变量。您可以将需要的环境变量添加到该字段中。每个环境变量都以键值对的形式表示,键和值之间使用冒号分隔。例如,如果要设置名为 MY_ENV_VAR 的环境变量,可以添加如下配置:
1
2
3
4
5
6
"environment": [
{
"name": "MY_ENV_VAR",
"value": "your_value"
}
]
  1. 保存 launch.json 文件。

  2. 在 VSCode 中打开您的 C++ 源代码文件,并在需要调试的行上设置断点。

  3. 单击 VSCode 左侧的调试图标(虫子图标),然后点击绿色的“启动调试”按钮。调试器将启动,并在设置的断点处停止。

  4. 如果有设置环境变量,它们将在调试过程中生效,您可以在调试过程中使用它们。

请注意:

  • 有些调试器可能不支持在 launch.json 文件中设置环境变量。确保您使用的调试器支持此功能。
  • launch.json 文件中设置的环境变量仅对该特定的调试配置生效。如果您有多个调试配置,每个配置可能需要不同的环境变量设置。

以上步骤是为了在 VSCode 中通过 launch.json 文件设置 C++ 调试的环境变量。如果您在调试过程中遇到问题,请确保配置正确,并查看调试器的文档以获取更多帮助。

自动换行

  • word wrap

VScode extensions xhr:failure

  • setting->details->Data&Time->Automatic Time Zone
    • open

下载

  • 首先,在官网找到需要下载的文件,点击下载,并将下载连接复制下来,例如:

    • https://az764295.vo.msecnd.net/stable/97dec172d3256f8ca4bfb2143f3f76b503ca0534/code_1.74.3-1673284829_amd64.deb
  • 然后,将az764295.vo.msecnd.net替换为vscode.cdn.azure.cn,例如:

    • https://vscode.cdn.azure.cn/stable/97dec172d3256f8ca4bfb2143f3f76b503ca0534/code_1.74.3-1673284829_amd64.deb
  • 这就是国内的镜像

使用root用户打开vscode

  • 背景:

    • 在远程调试机械臂的时候,需要使用到root权限来运行程序,所以在调试的时候需要使用到root权限
  • 方法:

    • 示例:code --no-sandbox --disable-gpu-sandbox --user-data-dir=".vscode-root"

VSCode 是什么

  • VSCode 是什么,VS Code的全称是Visual Studio Code,但这全名实在是太长了,很多用户喜欢叫它VS Code。说起VS Code,官方定义它是一个免费的、开源的跨平台编辑器。之所以强调“编辑器”,我想是因为 VS Code 并无意成为一个全尺寸的集成开发环境,也就是IDE

  • 很多人都把编辑器等同于IDE,其实从专业角度来讲并非这样。IDE 更为关注开箱即用的编程体验、对代码往往有很好的智能理解,同时侧重于工程项目,为代码调试、测试、工作流等都有图形化界面的支持,因此相对笨重,Java程序员常用的Eclipse定位就是IDE;而编辑器则相对更轻量,侧重于文件或者文件夹,语言和工作流的支持更丰富和自由,VS Code 把自己定位在编辑器这个方向上,但又不完全局限于此。

  • 要理解VS Code代码编辑器的设计思路,就需要先看看VS Code的发展轨迹。

  • 从我的角度看,不管你是学习编程语言,还是框架、编辑器,都应该先去看看它的来龙去脉,了解它们是怎么发展而来的,曾经遇到了什么问题,又是怎么解决的,这些信息都便于你从大局上提高对事情本质的认识

  • VSCode 发展历史:https://geek-docs.com/vscode/vscode-tutorials/what-is-vscode.html

VSCode的学习路线

  • 简短地了解了 VS Code 的历史后,如果你也认同它的设计哲学和使命,你肯定还想知道该如何把 VS Code 的这一套转化为自己的内力。我在第一讲 “学编辑器,到底应该‘学’什么?” 里讲过编辑器学习的通用办法,在 VS Code 身上也是适用的。你可以按照以下三个步骤来逐步掌握 VS Code。

    • 核心编辑器的使用。VS Code 有一套自己的快捷键,你可以通过快捷键的学习了解核心编辑器所支持的功能。同时, VS Code 允许自定义快捷键的映射,如果你有自己熟悉的一套快捷键操作,也可以无缝地在 VS Code 上使用。除了快捷键,VS Code 对鼠标操作、多光标、搜索都有完备的支持;在编程语言的支持上面,VS Code 也向 IDE 看齐,自动补全、代码片段等一应俱全。掌握了核心编辑器,VS Code 就能够胜任你的日常通用编辑器。
    • 工作台、工作区的使用。VS Code 中除了编辑器区域,还有很多其他的功能,像是资源管理器、跨文件搜索、插件管理等,它们一起组成了统一的界面,我们称之为工作台。这个工作台的设计,代表了 VS Code 对工作流的选择。内置的软件版本管理,终端模拟器,调试器等,掌握这些 VS Code “钦定”的工具,进一步提升工作效率。
    • VS Code 定制和插件开发。作为一个百万级别用户量的工具,很多功能的默认设置不可能满足每个人或者每个工作场景,你可以学习如何定制 VS Code 的各个部件,不能永远按部就班;对于 VS Code 没有实现的功能,还可以学习一下如何使用 JavaScript 书写插件,把自己的想法,变成工具的一部分。
  • 通过这三个步骤,你在使用 VS Code 时就能够“随心所欲”了。除此之外,我也建议你关注 VS Code 每月的发布更新日志,官方团队会详细讲解每个版本新增的功能。VS Code 的官方博客也非常值得订阅,团队成员会经常分享开发过程的心得感悟,算得上是最前沿的技术分享。

VSCode入门

  • 主要讲一下“学习”区域的三个功能:命令面板、界面概览和交互式演习场

命令面板

  • 首先来看命令面板,它是 VS Code 快捷键的主要交互界面,你可以通过 F1 或者“Cmd+Shift+P ”(Windows 上是 “Ctrl+Shift+P”) 打开。这里提醒一下,如无特殊说明,我在这个专栏里默认基于macOS平台进行讲解,但也会本着为你提供方便的原则,同时给出Windows或Linux平台下的操作说明。通过编辑器来实现高效编程的思路是一致的,这和具体的使用平台无关,所以你大可不必为此担心。
  • 你可以在命令面板中快速搜索命令并且执行。如果你的 VS Code 是简体中文版,那么你可以在命令面板里使用中文或者英文来搜索命令。VS Code 的绝大多数命令都可以在命令面板里搜到,所以熟练使用命令面板,你就可以摆脱鼠标,完全通过键盘操作来完成全部编码工作。

界面概览

  • 第二个是界面概览,它展示了 VS Code 默认界面里的不同部件的位置、名称和快捷键。VS Code 强调无鼠标操作,但是对于初学者而言快捷键的记忆是个麻烦,这个界面恰好可以帮助你渡过最初的不适应阶段。

交互式演习场

  • 第三个是交互式演习场,打开这个界面,你会看到一个全英文的初学者教程,其中通过各种交互示例给出了 VS Code 的核心功能,展示了一些高级代码编辑功能的使用,每个功能都会有一个代码片段和编辑器供你实时使用。

命令行的使用

  • 如果你是 Windows用户,安装并重启系统后,你就可以在命令行中使用 code 或者 code-insiders了,如果你希望立刻而不是等待重启后使用,可以将 VS Code 的安装目录添加到系统环境变量 PATH中, Windows 64 位下的 VS Code 安装路径是 C:\Program FIles\Microsoft VS Code下。

  • 如果你希望使用已经打开的窗口来打开文件,可以在 code 命令后添加参数 -r来进行窗口的复用。

  • 你也可以使用参数 -g <file:line[:character]> 打开文件,然后滚动到文件中某个特定的行和列,比如:

    • 输入 code -r -g package.json:128命令,你就可以打开 package.json 这个文件,然后自动跳转到 128 行。
    • 这个命令可以方便你从终端里快速地在 VS Code 里打开一个文件进行预览,一个特别常见的例子就是当我们使用脚本执行某个命令,这个命令告诉我们某个文件的某一行出现了错误,我们就能够快速定位了。
  • VS Code 也可以用来比较两个文件的内容,你只需使用 -d参数,并传入两个文件路径,比如:

    • 输入 code -r -d a.txt b.txt命令,就可以比较a.txtb.txt两个文件的内容了。
    • 有了这个命令,你就可以既使用命令行运行脚本,也可以借助 VS Code 的图形化界面进行文件内容的对比了。
  • VS Code 命令行除了支持打开磁盘上的文件以外,也接受来自管道中的数据。这样你就可以将原本在命令行中展示的内容,实时地展示在 VS Code 里,然后在编辑器中搜索和修改。比如,你可以把当前目录下所有的文件名都展示在编辑器里,此时只需使用ls | code -命令。

VSCode 键盘操作

  • VSCode 键盘操作,做到双手不离键盘,今天先来谈一谈核心的键盘操作:光标的移动、文本的选择、文本的删除,以及如何为编辑器命令绑定快捷键。

光标移动

  • 移动光标最常用的就是方向键,但是方向键每次只能把光标移动一个位置,可以说是一种相对低效的方式。

  • 首先是针对单词的光标移动。这个你应该比较熟悉,绝大多数原生的编辑应用和文本框都支持。这也是我自己最常用的一组快捷键。

    • 下面这张图显示,第一行代码中的第一个单词是 function,一共8个字符,光标的位置在第五个字符 t 的后面。当你想把光标直接移动到整个单词,也就是 function 的前面,你只需按下 Option(Windows 上是 Ctrl 键)和左方向键。相反,如果要把光标移动到单词的末尾,只需要按下 Option 和右方向键就好了。
    • 我们都知道,一直按着方向键,光标就可以不停地,一个字符一个字符地在文档中移动。但如果你同时按住 Option 和方向键,那么光标移动的颗粒度就变成了单词,你就可以在文档中以单词为单位不停地移动光标了
  • 第二种方式是把光标移动到行首或者行末。比如第一行代码是 function foo() {,你只需按住 Cmd + 左方向键(Windows 上是 Home 键),就可以把光标移动到了这行的第一列;而如果你按住 Cmd 和右方向键(Windows 上是 End 键),光标就会被移动到 { 的后面。

  • 接下来一种是对于代码块的光标移动。很多编程语言都使用花括号将代码块包裹起来,比如 if、for 语句等,你很可能会希望通过一个快捷键,就能实现在代码块的始末快速跳转。比如在这5行代码示例中,第一行到第三行代码是函数 foo 的定义,由一对花括号包裹起来,当你把光标放在花括号上时,只需按下 Cmd + Shift + \(Windows 上是 Ctrl + Shift + \),就可以在这对花括号之间跳转。

  • 最后一种基础的光标操作就是移动到文档的第一行或者最后一行,你只需按下 Cmd 和上下方向键即可(Windows 上是 Ctrl + Home/End 键)。

文本选择

  • 掌握了上面的快捷键之后,你还可以非常轻松地掌握文本选择的操作。因为对于基于单词、行和整个文档的光标操作,你只需要多按一个 Shift 键,就可以在移动光标的同时选中其中的文本

删除操作

  • 比如你想把当前行中光标之前的文本全部删除,就可以先选中这段文本(Windows/Linux: Home + Shift,macOS: Cmd + Left + Shift ),然后再按删除键。不过对于频繁使用的删除操作,你肯定希望单次操作就可以完成任务,而不是重复地选择文本然后删除,那么你需要记住下面几个命令。

  • 假设你把光标放在第二行代码的中间位置,然后按下 Cmd 和 Backspace(MacOS上就是“fn + delete”的组合,Windows 上未绑定快捷键,可以打开命令面板运行“删除右侧所有内容”),就能够把第二行代码光标后(右侧)的字符全部删掉。

  • 按下 Cmd 和 Delete 键则是删除当前行中光标前(左侧)的所有内容(Windows 上未绑定快捷键,可以打开命令面板运行“删除左侧所有内容”)

  • 删除单词内的字符与此类似。假设把光标放在第一行第四个字符 c 的后面。Option 加左方向键把光标移动到 function 这个单词的开头,Option加左方向键再加 Shift 即可选中 func 这四个字符,而Option 加 Delete 则会删除 func 这四个字符。这里你可能看出来了,这些快捷键共同的是 Option 键,然后通过按下 Shift 或者 Delete 键,来达到不同的效果。

  • 相反地,Option 加 Backspace(MacOS上就是“fn + delete”的组合) 则会删除 function 的后四个字符 tion。

自定义快捷键

  • 前面我们提到,VS Code 内置了很多的命令,但是并没有为每个命令都提供一个快捷键,毕竟快捷键的组合总是有限的。不过 VS Code 提供了快捷键的修改和自定义功能,这样你就可以根据自己的使用习惯,给自己常用的命令指定顺手的快捷键。

  • 首先你可以打开命令面板(你还记得它的快捷键不?),搜索“打开键盘快捷方式”然后执行,这时你将看到相对应的界面。

  • 然后通过搜索找到你希望修改快捷键的命令,双击,接下来你只要按下你期望的快捷键,最后按下回车键就可以了。

  • 比如,你可以搜索“选择括号内所有内容”,双击,按下”Cmd + Shift + ]”,然后按下回车,这个快捷键就绑定上了。

  • VS Code 的快捷键修改界面已经考虑到了这一点,你可以在搜索框内搜索你使用的快捷键,然后就可以看到这个快捷键当前对应的命令是哪个。

VSCode 代码行编辑

  • 要删掉一行代码,你可以选中它,然后再按 Delete 键。不过还有一个快捷键,那就是直接按下 “ Cmd + Shift + K ” (Windows 上是 “Ctrl + Shift + K”),当前代码行就可以被删除了。

  • 如果你只是想要剪切这行代码,那么你直接按下 “ Cmd + x ” (Windows 上是 “Ctrl + x”) 即可。

  • 我想你肯定很清楚,“Enter” 键的基础作用是能在编辑器里光标所在的位置添加一个换行符。但是很多时候你可能并不是单纯地要将一行分成两段,而是希望在这行的下面或者上面开始一段新的代码。

  • 这个功能对应的快捷键非常好记,它跟 “Enter”键十分接近。当你想在当前行的下面新开始一行时,你只需按下 “Cmd + Enter” (Windows 上是 “Ctrl + Enter”);而当你想在当前行的上面新开始一行时,你只要按下 “Cmd + Shift + Enter” (Windows 上是 “Ctrl + Shift + Enter”)就行了

  • 当你想移动一段代码时,一般你可能会分三步走:先选中,再剪切,最后粘贴。不过我更喜欢的是按住 “Option + 上下方向键”(Windows中就是“Alt + 上下方向键”) ,将当前行,或者当前选中的几行代码,在编辑器里上下移动。

  • 如果你同时按住 “Shift” 键的话,也就是 “Option + Shift + 上下方向键”(Windows中就是“Alt + shift + 上下方向键”),那就可以复制这几行,然后粘贴到当前行的上面或者下面。

  • 另外,你在尝试“Option + 上下方向键”这个快捷键“上下移动”时,可能也发现了,当你把一段代码移动到花括号里面或者外面时,代码前的制表符或者空格的数量会自动发生改变,这样你就不需要移动完代码后再调整了。

VSCode 撤销光标移动

  • VSCode 撤销光标移动,撤销光标的移动和选择。有的时候你移动完光标之后,又希望把光标回退到上一个位置,这时你只需按下 “Cmd + U”(Windows 上是 Ctrl + U),就可以撤销这一次光标的移动。

VSCode 行排序

  • VSCode 行排序。无论是你在写代码,还是写 Markdown,你都可以把代码行按照字母序进行重新排序。不过这个命令比较小众,VS Code 并没有给这个命令指定快捷键,你可以调出命令面板,然后搜索 “按升序排列行” 或者 “按降序排列行” 命令执行。

VSCode 合并代码行

  • VSCode 合并代码行。有的时候你可能会为了避免代码看起来过于冗余,就会把比较短小的几行代码合并到一行里面去。这时,你只需要按下 “ Ctrl + j ” (Windows 上未绑定快捷键,可以打开命令面板,搜索 ”合并行“)就可以了,而不需要不断地调整光标、删除换行符。

VSCode 调整字符大小写

  • VSCode 调整字符大小写,我估计这个你会经常用到。你可以选中一串字符,然后在命令面板里运行“转换为大写”或 “转换为小写”, 来变换字符的大小写。

VSCode 调换字符位置

  • VSCode 调换字符的位置。你可以按下 “Ctrl + t” (Windows 上未绑定快捷键,可以打开命令面板,搜索 ”转置游标处的字符“) 来把当前光标前后的字符调换位置。

VSCode 代码缩进

  • VSCode 代码缩进,有的时候,你会觉得代码格式化太重了,需要的可能只是把代码里的缩进调整一下。这时你可以打开命令面板(快捷键“Cmd + Shift + P”),搜索 “缩进”,然后使用 “重新缩进行” 将整个文档的缩进进行调整,但更多时候,你只需要运行 “重新缩进选中行” 来调整部分选中代码行的缩进。

VSCode 代码格式化快捷键

  • VSCode 代码格式化快捷键,我们平常在做自己的小项目或者随便写一些脚本的时候,可能不会太在意代码的格式。不过一旦开始团队合作,整个项目组则会选择同一个代码风格和格式以有效降低协同成本。所以定期对自己写的代码进行格式化是个很好的习惯。

  • 你可以按下 “Option + Shift + F” (Windows 上是 Alt + Shift + F)来对整个文档进行格式化,VS Code 也会根据你当前的语言,选择相关的插件。当然,前提条件是你已经安装了相关插件。

  • 你也可以选中一段代码,然后按下 “Cmd + K Cmd + F” (Windows 上是 Ctrl + K Ctrl + F),这样只有这段被选中的代码才会被格式化。

VSCode 添加代码注释

  • VSCode 添加代码注释,你在调试代码时,肯定经常需要临时地把一些代码注释掉。如果你要将一行代码注释掉,你只需按下 Cmd + / (Windows 上时 Ctrl + /)。如果你需要把一整段代码注释掉,按下 Option + Shift + A即可。

VSCode 自动补全

  • VS Code自动补全,VS Code 当中的自动补全内容,其实是由语言服务来提供的。本文介绍VS Code自动补全功能和VS Code自动补全设置。

  • VS Code 为编程语言工作者提供了统一的 API ,即 Language Server Protocol,每种语言都能够通过实现这个 API 在 VS Code 上得到类似 IDE 的开发体验,而各个语言根据这个 API 实现的服务,就被称为语言服务。

  • 语言服务会根据当前的项目、当前的文件,以及光标所在的位置,为我们提供一个建议列表。这个列表包含了在当前光标位置下我们可能会输入的代码。当我们不断地输入字符,VS Code 就会根据当前输入的字符,在这个列表进行过滤。

  • 如果我们偶尔觉得这个自动补全窗口是多余的,希望暂时不看到它,可以按下 Escape 键将其隐藏。后续如果希望再次看到这个窗口,除了通过打字来将其调出以外,我们还可以按下 “Ctrl + 空格键”来手动地调出建议列表。

  • 刚才我们提到,VS Code 会根据我们输入的字符在这个建议列表里进行过滤。同时,这个过滤是允许我们犯一点小错误的,比如打字特别快的时候少打一个字母,VS Code 也能处理这个情况。比如在下面的动图里,我想使用 console 里的 debug 函数,但是我只打了 db 两个字母,建议列表依然为我提供了 debug 这个选项。

  • 上面的这几个窗口,它们都是通过我们输入的内容自动触发的,也就是说,编程语言决定了我们什么时候看到什么内容。虽然我们可以通过快捷键将其快速地关闭和唤出,但是有的时候自动补全窗口出现得过于频繁,也是会影响我们的编程体验的,毕竟悬浮窗口会遮盖一部分代码,影响我们的阅读。

  • 不过,我们可以通过几个设置,控制自动补全窗口出现的频率和方式,甚至这个窗口的大小。

  • 首先我们可以通过设置 “editor.quickSuggestions” 来决定在什么语境下自动补全窗口会被唤出。默认设置如下:

    1
    2
    3
    4
    5
    "editor.quickSuggestions": {
    "other": true,
    "comments": false,
    "strings": false
    }
    • 这个配置有三个选项:other、comments和strings。其中,comments 就是代码注释,strings 就是指字符串。默认情况下,当光标在代码注释或者字符串里,自动补全窗口就不会被自动唤出了。但如果你希望这个窗口永远不被自动唤出,那么你就需要将“other” 也改为 “false”。
  • 这时你可能会问了,如果关闭了这个设置,我想看到自动补全该怎么办呢?不用担心,当你按下 “Ctrl + 空格键” 之后,这个窗口依然会被打开,不管设置是关闭还是开启的状态。看到这个设置,你肯定也就明白了,为什么默认情况下你在写注释的时候没有代码自动补全的提示了吧。

  • 参数预览窗口也是一样的,你可以通过参数 “editor.parameterHints.enabled” 将其关闭。当你觉得自己需要看一看参数预览时,按下快捷键或者通过命令面板就能够将其打开了。

  • 上面的这个设置决定“是与否”的问题,但你也可以控制自动补全窗口出现的时间。自动补全窗口监听文件内容的变化,当你停止输入时,它就会试着给你提供建议。但是有的时候你打字稍微快一些,自动补全窗口才刚刚出现,你就输入了更多的内容,紧接着代码服务就要重新计算并提供建议了。如果你希望减少这种不必要的提示,可以增大设置 “editor.quickSuggestionsDelay” 的值,这样在你输入完代码后,自动补全窗口就会多等一会儿,然后再跳出来。

  • 其他几个自动补全的设置,你可以在设置里搜一搜 “editor.suggest”,自己修改玩一玩。

  • 上面提到的几个功能,它们都依托于语言服务来提供内容。但是有的时候,语言服务并不完美。编辑器于是提供了一种相对 “笨” 一些的提示,那就是基于单词的提示。编辑器通过分析当前的文件里的内容,进行简单的正则表达式匹配,给我们建议已经出现过的单词。

VSCode 文本选择

  • 最简单的方式,也是我们每个人最熟悉的方式,就是按住鼠标左键,然后拖动鼠标,直到选中所有我们想要选择的文字为止,再松开鼠标即可。

  • 那是不是说鼠标用户要完成类似的操作,就只能“一点、二拖、三松手”呢?当然不是,VS Code 其实给鼠标也配备了类似的快捷键。

  • 在VS Code中:

    • 你单击鼠标左键就可以把光标移动到相应的位置。
    • 而双击鼠标左键,则会将当前光标下的单词选中。
    • 连续三次按下鼠标左键,则会选中当前这一行代码。
    • 最后是连续四次按下鼠标左键,则会选中整个文档。
  • 到这里你可能会问,如果我想要使用鼠标,选中其中的多行代码该怎么办?VS Code也考虑到了这个情况,在编辑器的最左边,显示的是每一行的行号。如果你单击行号,就能够直接选中这一行。如果你在某个行号上按下鼠标,然后上下移动,则能够选中多行代码。

VSCode 快速预览

  • VS Code快速预览是指,有的时候,当我们看到一个建议列表里的某个函数名,我们可能并不能够立刻想起它的作用是什么,它的参数定义是什么样的。
  • 这时候我们可以单击当前这一项建议的最右侧的蓝色图标。
  • 点击这个图标后,建议列表旁边就有出现一个快速预览的窗口,而这个窗口里面呈现的就是这个函数的定义。具体如下图:
  • 除了使用鼠标键外,我们还可以使用 “Ctrl+空格键”组合键来快速调出这个快速预览窗口。

VSCode 参数预览

  • VS Code参数预览,当我们从建议列表选择了一个函数,然后输入括号,准备开始输入参数时,我们会看到一个参数预览的悬浮框。通过这个参数预览的窗口,我们可以知道这个函数可以传入哪些参数,它们的参数类型又是什么样的。
  • 同样的,隐藏这个窗口的快捷键也是 Escape。如果你想再次将其调出的话,需要按下 “Cmd + Shift + Space” (Windows 上是 Ctrl + Shift + Space)。

VSCode 重构

  • 当我们想要修改一个函数或者变量的名字的时候,我们只需要把光标放到函数或者变量名上,然后按下F2,这样这个函数或者变量出现的地方就都会被修改

  • 这个操作并不是一个粗暴的搜索关键词并替换,在上面的动图中你可以看到,最后一行代码里有个 bar3函数调用,但当我们去重命名 bar这个函数时,bar3并没有受到影响。

  • 除了重命名外,另一个常用的重构的操作就是把一段长代码抽取出来转成一个单独的函数。在VS Code中,我们只需选中那段代码,点击黄色的灯泡图标,然后选择对应的重构操作即可。

  • 要注意的是,并不是每个语言服务都支持重构的操作。如果你选中一段代码后,但没有看到那个黄色的灯泡图标,那么也就是说你使用的这门语言可能还没有支持快速重构。

VSCode 文本编辑

  • VS Code文本编辑,在 VS Code中,我们除了能够使用鼠标来选择文本以外,还能够使用鼠标对文本进行一定程度的修改,我们把它称为拖放功能(drag and drop)。

  • 比如在今天的示例代码中,我们选中 bar 这个函数,然后将鼠标移到这段选中的代码之上,按下鼠标左键不松开。这时你可以看到,鼠标指针已经从一条竖线,变成了一个箭头。这时候我们移动鼠标的话,就可以把这段文本拖拽到我们想要的位置。

  • 在移动的过程当中,我们能够在编辑器中看到一个由虚线构成的光标,当我们松开鼠标左键的时候,这段文本就会被移动到这个虚拟的光标所在的位置。

  • 如果我们在拖拽这段文本的同时,按下 Option 键(Windows 上是 Ctrl 键),鼠标指针上会多一个加号,这时候我们再移动鼠标或虚拟光标至我们想要的位置,然后当我们松开鼠标左键的时候,这段文本将会被复制粘贴到虚拟光标所在的位置,也就是我们既定的目标位置。

  • 你看,在移动鼠标的过程中,多按了个 Option 键(Windows 上是 Ctrl 键),操作结果就由原来的“剪切+粘贴”变为“复制+粘贴”了。

VSCode 多光标

  • VSCode 多光标特性,在我们的日常编码过程中,有很多工作,它本身就是具有“重复”属性的。比如你需要把多个单词的第一个字母从小写变成大写,这种跟业务逻辑相关的重复性操作,编辑器很难为它们一个个单独做优化。

  • 而 VS Code 的多光标特性其实就是用来解决这类问题的。当你在一个文本框或者某个输入框里打入字符时,会有一个竖线来显示你将要输入文字的位置,这就是“光标”。顾名思义,多光标其实就是多个输入位置,这里你可以脑补下多个竖线的场景。

  • 多光标特性允许你在输入框的多个位置创建光标,这样你就可以在多个不同的位置同时输入文字或者执行其他操作

  • “Cmd + D” 这个命令的作用是,第一次按下时,它会选中光标附近的单词;第二次按下时,它会找到这个单词第二次出现的位置,创建一个新的光标,并且选中它。这样只需要按下三次,你就选中了所有的“5”。这个时候你再按下 “右方向键”,输入“px”,即可完成任务。

  • 接下来讲讲第二种,是跟代码行批量处理有关,也还是用的前面的代码。首先你选择多行代码,然后按下 “Option + Shift + i” (Windows 上是 Alt + Shift + i),这样操作的结果是:每一行的最后都会创建一个新的光标。

  • 不过,VS Code 中还有一个更加便捷的鼠标创建多光标的方式。当然,这首先要求你的鼠标拥有中键。你只需按下鼠标中键,然后对着一段文档拖出一个框,在这个框中的代码就都被选中了,而且每一行被选中的代码,都拥有一个独立的光标。

VSCode 代码跳转和链接

  • 我们还是把鼠标移动到示例代码的第五行 foo 上,然后按下 Cmd 键,这时候 foo下面出现了一个下划线。然后当我们按下鼠标左键,就跳转到了 foo函数的定义处。

VSCode 文件跳转

  • 在VS Code中,解决这个问题的第一个方法,就是按下 “Ctrl+Tab”,然后继续按着 “Ctrl”键但是松开 “Tab” 键,这样你就可以打开一个文件列表,这个列表罗列了当前打开的所有文件。接下来,你可以通过按下 “Tab”键在这个列表里跳转,选择你想要打开的文件。最后选到你想打开的文件后,松开 “Ctrl” 键,这个文件就被打开了
  • 还好,VS Code 在命令面板里提供了一种支持搜索的文件跳转方式。当你按下 “Cmd + P” (Windows 上是 Ctrl + P)时,就会跳出一个最近打开文件的列表,同时在列表的顶部还有一个搜索框。
  • 看到这里想必你应该明白了,你可以使用这个搜索框来快速地找到你想要的文件,然后按下 “Enter” 键直接打开,这整个过程简单而且顺畅。

VSCode 行跳转

  • VS Code也提供了一种极为简单的方式来支持行跳转,你只需要按下 “Ctrl + g”,紧接着编辑器就会出现一个输入框
  • 如果你想跳转到某个文件的某一行,你只需要先按下 “Cmd + P”,输入文件名,然后在这之后加上 “:”和指定行号即可。跳转到指定文件的指定行数

VSCode 符号跳转

  • VS Code符号跳转,文件跳转和行跳转,是代码跳转的基本操作,也是日常编码中的高频操作。不过有的时候,你可能会希望能够立刻跳转到文件里的类定义,或者函数定义的位置。为了支持这种跳转,VS Code 提供了一套 API 给语言服务插件,它们可以分析代码,告诉 VS Code 项目或者文件里有哪些类、哪些函数或者标识符(我们把这些统称为符号)。

  • 如果要在一个文件里的符号之间跳转,你只需按下 “Cmd + Shift + O” (Windows 上是 Ctrl + Shift + O),就能够看到当前文件里的所有符号。

  • 使用方向键,或者搜索,找到你想要的符号后,按下回车,就能够立刻跳转到那个符号的位置。如下图所示:通过符号功能跳转到指定的代码位置

  • 请注意,在按下 “Cmd + Shift +O”后,输入框里有一个 “@”符号,这个符号在这里的意义,我会在后面的章节里去介绍,你可以先留个心眼。这时,如果你输入 “:”,就可以将当前文件的所有符号,进行分类,这样搜索符号也就更加方便。

  • 有些语言除了提供单个文件里的符号,还支持在多个文件里进行符号跳转。比如在 VS Code 里,如果你打开了多个 JavaScript 文件,就可以按下 “Cmd + T” (Windows 上是 Ctrl + T),搜索这些文件里的符号。

  • 通过“Cmd + T”,搜索多个文件的符号

VSCode 定义和实现间跳转

  • F12跳转到函数定义的位置
  • 也可以按下 “Cmd + F12” (Windows 上是 Ctrl + F12),跳转到函数的实现的位置。

VSCode 跳转到引用的地方

  • VS Code引用跳转,很多时候,除了要知道一个函数或者类的定义和实现以外,你可能还希望知道它们被谁引用了,以及在哪里被引用了。这时你只需要将光标移动到函数或者类上面,然后按下 “Shift + F12”,VS Code 就会打开一个引用列表和一个内嵌的编辑器。在这个引用列表里,你选中某个引用,VS Code 就会把这个引用附近的代码展示在这个内嵌的编辑器里。

  • Shift+ F12打开函数引用预览

VSCode 代码片段

  • VSCode代码片段,有的时候,我们经常输入的代码是业务强相关的,语言服务没法做出优化;或者是一些我们经常使用的定式,比如循环语句、创建一个新的类或者一个 UI 控件,我们经常写类似的代码,只不过每次都要做细微的修改。对于这些代码,我们可以将它们抽象成模板,保存起来,等下次要使用的时候直接调用即可。

  • 代码片段是对常用代码的一个抽象,它保留了大部分不变的代码,然后把需要经常变动的部分,换成变量,这样等下次调用它的时候,只需要把这些变量换成我们需要的就可以了

  • 首先,我们打开命令面板,搜索“配置用户代码片段”(Configure User Snippets)并且执行。这时候我们会看到一个列表,让我们选择语言。这里我们依然选择 JavaScript 作为我们的示例语言,不用担心,代码都是非常简单和易于理解的。命令面板,搜索“配置用户代码片段”并且执行

  • 选择完语言后,我们就能看到一个 JSON 文件被打开了,这个文件里的内容,现在都是被注释掉的。我们可以选中第七行到第十四行,按下 Cmd+ / 取消注释。

  • 在上面的例子里,这个代码片段的名字叫做 Print to console 。这个代码片段对象的值,也就是花括号里的代码,必须要包含 “prefix” 前缀和 “body” 内容这两个属性。同时,这个值还可以包含 “description” 描述这个属性,但这个属性不是必须的。

  • “prefix” 的作用是,当我们在编辑器里打出跟 “prefix” 一样的字符时,我们就能在建议列表里看到这个代码片段的选项,然后我们按下 Tab 键,就能够将这个代码片段的 “body” 里面的内容插入到编辑器里。如果这个代码片段有 “description” 这个属性的话,那么我们还能够在建议列表的快速查看窗口里看到这段 “description”。

  • 输入 log 即可看到 Print to console 代码片段,然后再按下回车或者 Tab 键,就能够将这个代码片段插入编辑器了。

VSCode 折叠代码快捷键

  • VSCode折叠代码快捷键,我们再来一起看一下有哪些折叠和展开代码的快捷键。首先是折叠和展开代码的两个快捷键。

  • 当我们按下 “Cmd + Option + 左方括号”(Windows 上是 Ctrl + Shift + 左方括号),当前光标所处的最内层的、可以被折叠的代码就会被折叠起来。请注意,我们在这里加了两个限制条件,“最内层”和“可以被折叠”。我们可以先用下面一个小例子来理解这两个条件。

VSCode 小地图

  • 如果你是在一个比较大的屏幕上工作,需要快速了解整个文件的全貌,并且还能靠鼠标快速地移动,那么这时小地图就很有用了。这个功能默认是打开的,所以你无需特别设置。这个使用起来比较简单,你可以像我在图中展示的那样试着打开一个较大的文件,感受一下它的妙处。

  • 很多游戏中也有类似的小地图功能,不知道你有没有似曾相识的感觉。

  • 除了控制小地图是否打开,编辑器还为我们提供了几个渲染的配置项。比如说,默认情况下,小地图会将每个字符都渲染出来。但是我们并不能真正地通过小地图来看代码,我们只是要看个大概结构罢了,那么我们可以打开命令面板,搜索“打开设置”(Open Settings),进入设置界面后,搜索 “editor.minimap.renderCharacters” ,找到后将其关闭,这样一来,所有的字符,都会被渲染成一个个小色块。

  • 同样的,我们还可以通过 “editor.minimap.maxColumn” 来控制小地图里每一行渲染多少个字符。很多时候我们只需看下每行代码前的缩进和前面的代码高亮,就能看出个大概来了。

VSCode 单文件搜索

  • VSCode单文件搜索,今天我们重新回到原点,来看一下如何使用编辑器自带的文本搜索功能,快速地穿梭于海量的代码之中。在我看来,一个功能丰富且快速的搜索,在很多情况下甚至会比语言服务还要来得有用。

  • 我们把光标放在编辑器当中,然后按下 “Cmd + F” (Windows 上是 Ctrl + F),就能够快速地调出搜索窗口(可能这个命令你早就发现了或者经常使用了)。当我们调出搜索窗口的时候,编辑器就会把当前光标所在位置的单词自动填充到搜索框中。与此同时,当前文件里和搜索关键词相同的单词都会被高亮出来。

  • 自动填充搜索关键词的好处在于,当我们按下 “Cmd +F” 搜索这个单词之后,我们还能够立刻通过回车键或者 “shift+回车键” 在所有搜索结果当中快速跳转。

  • 这里需要注意的事情是,当我们开始搜索的时候,光标已经被移动到了搜索框当中,如果在这时候我们继续打字的话,那原有的搜索关键词将会被修改。

  • 如果我们希望找到搜索结果后,接下来就直接修改编辑器中的内容,那么就得将光标重新移动到编辑器当中,听起来就挺不方便的,是不是?

  • 这种情况下,我们不妨换一个快捷键。首先我们将光标移动到我们想要搜索的单词处,然后按下 “Cmd + G” (Windows 上是 F3),此时我们同样调出了搜索框,但与前面 “Cmd +F ” 这个快捷键不同的是,这时光标依然是在编辑器当中,而不是在搜索框中。

  • 下面我们再一起来看下这个搜索框中都有哪些功能。

  • 当我们在搜索框中打字的时候,搜索操作是自动触发的,而无需我们再按下回车键去手动地执行搜索这个操作。

  • 除了搜索纯文本以外,搜索框还支持多种不同的搜索方式。比如,在搜索框的最右侧,就有三个配置按钮。

    • 第一个是大小写敏感。 这个很好理解,就是在文档中搜索关键词的时候,搜索的结果是否要跟关键词大小写完全一致。默认情况下,VS Code 的搜索是不区分大小写的,也就是说哪怕大小写不一样,也会算到搜索结果里去。但如果我们不想要这个特性,就可以点击这个按钮,或者按下 “Cmd+Option+C” (Windows 上是 Alt + C)来关闭它。
    • 第二个是全单词匹配。 有的时候我们搜索的单词恰好是别的某个单词中间的一部分,如果我们不希望这样的结果出现在搜索结果中,那么就可以点击这个按钮或按下 “Cmd+Option+W” (Windows 上是 Alt + W)来关闭它。
    • 第三个,就是正则表达式匹配了。 当我们点击这个按钮或按下 “Cmd + Option + R” (Windows 上是 Alt + R ),就能够打开正则表达式的支持,然后在搜索框中输入正则表达式来搜索。要注意的是,编辑器中的这个搜索框,它里面的正则表达式使用的是 JavaScript 的正则引擎。
  • 这三个功能的快捷键的配置,相信你已经看出其中的诀窍了,它们分别使用了 Case、Word 和 Regular Expression 的第一个字母作为快捷键的一部分,若你知道是这几个单词,那相信对应的快捷键你就不会容易忘了。

  • 我们可以先选中一段文本,然后按下 “Cmd + F” 调出搜索框,这之后点击这个按钮,就可以将这段文本的范围设置为接下来的搜索区域。然后当我们在输入框里输入关键字后,编辑器就只会在这个区域里进行搜索。

  • 上面我们提到的功能,都是 VS Code 的默认行为。但也有部分用户不喜欢搜索框的一部分行为,比如说自动填充搜索关键词。那你可以打开设置,搜索 “editor.find.seedSearchStringFromSelection” 来关闭它。

  • 也有个别用户觉得,如果选中了多行文本,那么当开始搜索时,应该自动地只在这几行代码里进行搜索。要达成这样的目的,你则需要打开设置 “editor.find.autoFindInSelection” 。

VSCode 单文件替换

  • VSCode单文件替换,在搜索到我们想要的结果之后,我们可以直接在文件中进行修改,也可以使用替换窗口进行批量替换。如果你在使用鼠标或者是触控板的话,只需按一下搜索窗口最左侧的箭头按钮即可打开替换框。
  • 替换框的后面,一共有两个按钮:第一个能够替换单个搜索结果,第二个则能够替换全部的搜索结果。它们对应的快捷键我就不多加赘述,我们只需把鼠标指针移动到它们上面,就能够看到了。
  • 我们也可以通过快捷键直接调出替换窗口。最常用的命令就是按下 “Cmd + Option + F”(Windows 上是 Ctrl + H)键,这样当前光标所在的单词就会被用作为搜索关键词,同时编辑器将光标移动到替换窗口中,我们只需直接输入想要替换的关键词就行了,是不是很便捷呢?
  • 当然,如果你在书写完替换文本后,觉得搜索关键词需要修改,那你可以按下 “Shift + Tab” 键将光标移动到上面的搜索输入框里。“Tab” 和 “Shift + Tab” 键能够帮助你在这两个输入框直接进行跳转。

VSCode 多文件搜索和替换

  • 多文件搜索的运行方法跟单文件搜索非常类似。单文件搜索,我们是通过按下“Cmd+ F” 来调出搜索窗口的,而多文件搜索则是通过按下 “Cmd + Shift + F” (Windows 上是 Ctrl + Shift + F)来调出多文件搜索的视图。

  • 默认情况下,当我们调出多文件搜索的视图时,VS Code 会在当前打开的文件夹下进行搜索。不过,要发挥多文件搜索的更大功效,我们可以通过书写配置来决定在哪些子文件夹下进行搜索,以及过滤掉哪些特殊的文件或者文件夹。

  • 要完成这样的配置,我们需要点击搜索框下三个点形状的图标,点开后,我们能看到两个输入框,它们的名字分别是“包含的文件” 和 “排除的文件”。这两个配置的书写格式是 glob,很多编程语言和配置都会使用 glob 来模糊匹配文件名和文件夹,估计你已经有所了解。而如果你不熟悉的话,就当作是课后作业了,这一定不是你最后一次需要书写 glob。

  • 第一个是 “search.collapseResults” 。它是用来控制是否自动展开搜索结果。默认的配置是 “auto” 自动, 也就是说,VS Code 会根据搜索结果的多少来决定是否要将某个文件下的搜索结果展开,如果某个文件夹下的结果过多的话,就会将其暂时折叠,用户需要展开结果。我自己喜欢将其设置为 “alwaysExpand”,这样我每次都能直接看到结果了。

  • 第二个是 “search.location” ,也就是多文件搜索视图的位置。默认情况下,搜索视图会出现在侧边栏。但是 VS Code 同样允许你把搜索视图放到底部面板中去,你只需将其修改为 “panel” 即可。相信很多用户都跟我一样,使用过非常多把搜索视图放在底部的开发工具,并且很习惯了,那这个设置就能够帮助到我们。

VSCode 行号

  • 我则是通过更改设置 editor.renderLineHighlight: “all” 把当前代码行的行号下的背景色也修改了,所以你可以看到图 2 的行号 5 的背景色也成为了绿色,整体上看起来更统一。

VSCode 渲染出空格符和制表符

  • 在图2中你能够在不少代码行前面看到灰色的“点”,这每一个“点”都代表着一个空格符。你可以通过设置 editor.renderWhitespace: all 让编辑器将所有的空格符、制表符等全部都渲染出来。这样你就能够一眼看出这个文件中使用的究竟是制表符还是空格符,以及有没有在哪里不小心多打了一个空格等。

VSCode 缩进参考线和垂直标尺

  • 编辑器会根据你指定的制表符的长度,来决定缩进参考线的位置。这样你就可以非常清楚地知道代码有没有正确地缩进,而且也方便你区分出不同代码块之间的层级关系。这个功能是可以通过 editor.renderIndentGuides 来控制开关的。

  • 而图2中的竖线则不一样了,它叫做垂直标尺。如果你的项目中有规定说每一行代码不得超过多少个字符,比如说120个字符,那么你就可以将标尺设置为 120,即 editor.rulers: [120]。这样的话编辑器就会在第120个字符所在的位置处画出这样一条垂直的竖线,所以你一眼就可以看出自己的代码是否达标。

VSCode 光标样式

  • 在图1中,光标是一条竖线,而在图2中光标则相对粗一些。编辑器中的光标样式有非常多种,你可以控制粗细,也可以控制它怎么闪烁。你需要调整的设置是 editor.cursorBlinking editor.cursorStyle 和 editor.cursorWidth。

VSCode 如何管理文件和文件夹

  • VS Code是如何管理文件和文件夹,首先需要说明的是,VS Code 的各个功能,都是基于当前打开的文件或者文件夹的。

  • 该怎么去理解这个概念呢?

    • 如果你使用过 IDE 的话, 你应该记得在第一次打开 IDE 的时候,它们往往需要你创建一个工程,这个工程会生成一个特殊的工程文件。这个工程文件记载了这个项目有哪些相关的文件、项目的配置、构建脚本等等。这个文件记录着 IDE 管理工程的元信息,开发团队也能够通过共享这个工程文件保证成员工作环境的一致性。但是工程文件对用户体验就不太友好了,比如说项目文件可能对 IDE 的版本有所要求,项目文件损坏了 IDE 读取不了但是我们也不知道如何修复,等等。
  • VS Code 则选择了一种相对轻量,而且大家都易于理解的方式,那就是所有的操作都基于文件和文件夹。当你打开一个文件夹,VS Code 的核心功能就会对这个文件夹进行分析,并提供对应的功能。比如,在打开的文件夹下检测到有 .git 文件,就加载 Git 插件来提供版本管理的功能;或者发现文件夹下有 tsconfig.json ,就会激活 TypeScript 插件提供语言服务

  • 当你第一次打开 VS Code 的时候,工作台中还没有打开任何文件夹。这时候在欢迎界面的左上方,你能够看到:“新建文件”和“打开文件夹”等这样的快捷键。

  • 未打开文件夹,状态栏为紫色

  • 这时候请注意工作台最下方的状态栏,当 VS Code 没有打开任何文件夹的时候,它的颜色是紫色的。而如果在工作台中打开了某个文件夹,状态栏的颜色就会变成蓝色。

VSCode 多文件夹工作区

  • VS Code 多文件夹工作区,多文件夹工作区(multi-root workspace)。老实说呢,这个概念是有一定的理解难度的。

  • 上面我们提到的基于文件夹的这种项目管理方式,从 VS Code 第一天开始就存在了。也几乎从第一天开始,我们就收到了用户对于这一个设计不满的反馈。对于这些不满的用户而言,他们的痛点在于他们经常需要同时对多个文件夹下的代码进行操作。但是 VS Code 关于单个文件夹的这种操作模式,要求了他们必须同时打开多个窗口,并不停地在它们之间切换。

  • 多文件夹工作区就是为了针对这个问题而实现的解决方案。那下面我们就一起来看一看怎样去创建一个多文件夹工作区。

  • 首先,在 VS Code 中打开一个文件夹,此时 VS Code 处于一个单文件夹的状态。然后你可以调出命令面板,搜索 “将文件夹添加到工作区” (add folder to workspace)并执行,或者使用菜单,“文件 —> 将文件夹添加到工作区”,这之后,选择你想要在当前窗口打开的文件夹。

  • 此时在资源管理器里的标题栏里,你能看到“无标题 (工作区)”这样的文字,这说明当前的工作区已经有多个文件夹了,只是现在你还没有保存这个多文件工作区,也没有给它指定一个名字。

  • 要保存这个工作区,接下来你可以调出命令面板,搜索“将工作区另存为” (save workspace as),VS Code 就会为这个工作区创建一个文件,这个文件的后缀名是 “code-workspace”。比如,在下面的动图中,我给这个工作区取名为 sample,然后指定在 Code中这个文件夹下保存。这样操作后,VS Code 就会在 Code 文件夹下创建一个 sample.code-workspace 文件。

  • 你可以看到,操作完之后资源管理器的标题栏已经相应地改变了。另外,sample.code-workspace 虽然有个特殊的后缀,但这个文件的格式其实也是 JSON,你可以自行打开这个文件查看一下。

  • 这个 JSON 文件,默认有两个键(key)。第一个是 folders 文件夹,它里面罗列的是这个多文件工作区里有哪些文件夹。可以看出,这些文件夹的地址,都是这个 sample.code-workspace 文件的相对路径。第二个则是 settings 设置,你可以在这个值里面添加专属于这个多文件夹工作区的设置。它的作用,跟上面我们介绍的 .vscode 文件夹下的 settings.json 文件是类似的。

VSCode 工作区切换

  • VSCode 工作区切换,如果你同时打开了多个窗口,可以按下 Ctrl + W,或者调出命令面板,搜索 “切换窗口(Switch Window)”,然后选择你要跳转的那个文件夹中去。
  • 如果你只是要跳转到上一个打开的窗口,那就更方便了。打开命令面板,搜索“快速切换窗口(Quick Switch Window)”并执行,就能够直接跳转到之前的窗口了,而无需再做选择。这里我倒是非常建议你给这个命令指定一个快捷键,这样你就能在窗口之间一键切换了。
  • 如果你同一时间只会关注一个项目,那你也大可不必使用多个窗口。我就经常只用一个显示器和一个窗口,然后当我想在另外一个项目上工作时,我就会按下 Ctrl + R(或者使用命令面板,搜索 “打开最近的文件”),此时我就能够看到最近操作过的文件夹并按下回车键进行切换了。
  • 当你按下 Ctrl + R 调出最近打开的文件夹的列表后,也能够按下 Cmd + 回车键,将它在一个新的窗口中打开。
  • 正是因为有上面这几个命令的存在,让我觉得没有多文件夹工作区也是可以的。当然,多文件夹工作区在某些方面的优势是不可比拟的,比如说跨文件夹的代码调试,这个我们后面也会介绍。

VSCode 代码调试器

  • VSCode 代码调试器,和语言功能一样,VS Code 是把调试功能的最终实现交给插件来完成的。VS Code 提供了一套通用的图形界面和交互方式,比如怎么创建断点、如何添加条件断点、如何查看当前调试状态下参数的值,等等。无论你使用哪个编程语言或者调试器,这一套交互流程都是相似的。

  • 而对于插件作者而言,他们需要完成的是如何把真正的调试工作跟 VS Code 的界面和交互结合起来,为此 VS Code 为插件作者提供了一套统一的接口,叫做Debug Adapter Protocol(DAP)。当用户在界面上完成一系列调试相关的操作时,VS Code 则通过 DAP 唤起调试插件,由插件完成最终的操作。

  • VS Code 中有一个专门的用于管理调试功能的视图。我们可以点击界面左侧“昆虫”(也就是 bug 啦)形状的按钮,或者按下 “Cmd + Shift + D” (Windows 上是 Ctrl + Shift + D)来唤出调试视图。

  • 在视图的最上侧,有个绿色的箭头按钮。这个按钮是用于启动调试器的。但是在上面的截图里,你可以看到在绿色箭头的右侧写着 “没有配置”。这说明现在 VS Code 还不知道该使用什么调试器来调试当前的代码。此时点击这个按钮或者按下 F5,我们能够看到一个列表。

  • 首先,我们将鼠标移动到第五行代码的行号前面,点击鼠标左键,我们能够看到一个红色的圆点被创建了出来,这就是断点。当然,我们也可以把光标移动到第五行,然后按下 F9,同样可以在第五行创建断点。

  • 此时,当我们再次点击调试视图上面的绿色箭头按钮,或者按下 F5,启动调试器,并且选择 Node.js ,VS Code 就会进入调试模式。

  • VSCode 代码调试器配置launch.json介绍,在调试视图的最上方,我们能够看到一个齿轮形状的按钮,它可以用于创建和修改 launch.json 文件。由于当前文件夹下没有 launch.json 文件,所以这个按钮的右上角有个红色的点,它告诉我们当前的调试配置有一点问题,让我们点击这个按钮。

  • 这个 JSON 文件里的 configurations 的值就是当前文件夹下所有的配置了。现在我们只有一个调试配置,它有四个属性:

    • 第一个是 type,代表着调试器的类型。它决定了 VS Code 会使用哪个调试插件来调试代码。
    • 第二个是 request,代表着该如何启动调试器。如果我们的代码已经运行起来了,则可以将它的值设为 attach,那么我们则是使用调试器来调试这个已有的代码进程;而如果它的值是 launch,则意味着我们会使用调试器直接启动代码并且调试
    • 第三个属性 name,就是这个配置的名字了。
    • 第四个属性 program,就是告诉 Node.js 调试器,我们想要调试哪个文件。这个值支持预定义参数,比如在上面的例子里,我们使用了${file},也就是当前编辑器里打开的文件。
  • 下面我们把 program 的值改为 ${workspaceFolder}/index.js,其中${workspaceFolder} 是代表当前工作区文件夹地址的预定义参数,使用它就能够准确地定位当前工作区里 index.js 文件了。(关于在配置文件里可以使用的预定义参数,请参考Visual Studio Code Variables Reference。 https://code.visualstudio.com/docs/editor/variables-reference

  • 通用属性

  • 虽然每个调试器各自控制着用户可以使用哪些属性,但是调试器之间还是有很多相同的地方,调试插件在很多时候都会使用相同的属性名来代表同样的功能。比如,我自己就是 Ruby 插件的作者,我在实现 Ruby 调试插件的时候,参考了很多 Node.js 和 PHP 调试插件对于属性的命名和使用。我在书写不同语言的调试配置时,经常使用的有下面这些:

    • program 一般用于指定将要调试的文件。
    • stopOnEntry,当调试器启动后,是否在第一行代码处暂停代码的执行。这个属性非常方便,如果没有设置断点而代码执行非常快的话,我们就会像文章的最开头那样,代码调试一闪而过,而没有办法在代码执行的过程中暂停了。而设置了 stopOnEntry 后,代码会自动在第一行停下来,然后我们就可以继续我们的代码调试了。
    • args 参数。相信你应该记得在前面任务系统配置的文章里,我已经说明了可以使用 args 来控制传入任务脚本的参数,同样的,我们也可以通过 args 来把参数传给将要被调试的代码。
    • env 环境变量。大部分调试器都使用它来控制调试进程的特殊环境变量。
    • cwd 控制调试程序的工作目录。
    • port 是调试时使用的端口。

VSCode 经典插件推荐

  • VSCode 经典插件推荐,今天我要介绍的是:能够在某些领域大幅度提高VS Code使用效率和体验的工具。能够取代 VS Code原有功能的工具。对插件 API 的使用别出心裁的工具。

Git

  • GitLens

  • VS Code中的 Git 体验在易用性和完整性之间取得了一个不错的平衡,大部分用户都能够使用它完成工作,同时又不会被太多的功能吓到。但是很多硬核的 Git 用户肯定会觉得功能还不够用。包括但不限于:

    • 不能查看某个 commit 中的代码改动;
    • 不能比较两个 commit 或者 branch,然后阅览代码改动;
    • 不能查看代码历史记录。
  • RemoteHub

  • GitLens 作者 Eric Amodio 又出一款力作——RemoteHub。安装这个插件后,当你想在本地看某个 GitHub repository的代码时,你就不需要将代码 clone 下来了,你可以直接打开这个 repository 相关的工作区,所有文件、文件夹都是从 GitHub 按需下载下来。如果你连接 GitHub 的网速不错的话,那么使用体验可是比 GitHub 网站要好得多。

  • GitHub Pull Request

  • 除了 Git 支持以外,一个呼声一直非常高的需求,就是在 VS Code中查看和审核 GitHub 上的 Pull Request。好消息是,VS Code团队和 GitHub 的 Editor Tools 团队一起合作,为我们提供了 GitHub Pull Request这个插件。

工作区

  • Settings Sync

  • 如何在不同设备之间同步个人设置?VS Code自己并没有提供设置的同步,但通过 Settings Sync这个插件,你可以将个人设置同步到 Gist 中。

  • 不过值得注意的是,虽然你的设置是同步到自己私人的 Gist 中,但是如果你的设置中有一些隐私信息,像密码、Token 之类的,还是不要使用此插件比较好。

  • Project Manager

  • 我们在工作台的部分,介绍过 VS Code支持多文件夹工作区(multi-root workspace),以及如何通过快捷键在不同的项目之间来回切换。如果你不喜欢 VS Code默认的方式,那么你也可以试试 Project Manager。Project Manager 甚至还有一个专门的视图来展示所有的项目,非常方便。

编辑器

  • VIM

  • 编辑器相关的插件中最厉害的应该就是 Vim 相关的插件了,VS Code提供了一个 API 保证了 Vim 插件能够被正确地实现。不过 Vim 插件并不只有一个,下载量最大的,也是我参与的就是 VSCodeVim,它对 Vim keybings 的覆盖程度非常高。另一个非常受大家欢迎的就是amVim,它的性能也非常不错。

  • Rainbow Brackets

  • 不管你是不是写函数式语言,当你的代码中有比较多的花括号时,要保证它们对称可以说是非常困难了。Rainbow Brackets这个插件,为同一对花括号指定一个单独的配色,这样你就能够轻松地一眼看出花括号的配对了。

  • Indent Rainbow

  • 上面的 Rainbow brackets 是给花括号加上多种颜色,而 Indent Rainbow则是为你的代码缩进提供颜色上的提示:

  • 这两个插件有异曲同工之妙,当然我还是建议写代码的时候,不要有太多的层级。

  • Pigment

  • 既然说到颜色,就不得不提Pigment 这个插件。在介绍择色器(Color Picker)的时候我介绍过,VS Code会在每个颜色前面加上一个方块,用方块来展示代码所对应的颜色。Pigment 则是将颜色渲染在这段代码的下面,我自己还是蛮喜欢这种方式的。

  • Import Cost

  • JavaScript 经常被吐槽的一个地方,就是大家对 npm 库的使用程度非常高,经常为了一个简单的功能,引入了几兆甚至十几兆的 npm 包。Import Cost这个插件,很好地在代码中给我们以提示,告诉我们引入的某个包,它最终会导致整个项目的大小增加多少。

调试:Debugger for Chrome

  • 虽然我们并不介绍语言相关的插件,但是还是有一个调试相关的插件值得一提,那就是 Debugger for Chrome。这个插件,允许在 VS Code中调试前段代码,这样你就不需要再使用 Chrome Dev Tools 了。你可以直接在自己的代码上加上断点,发现错误后直接修改,非常方便。

其他

  • Rest Client

  • 我们使用 REST API 的时候,经常需要发送一些样例数据对 API 进行测试,这时我们可以使用 Postman 这类的独立应用,也可以在 VS Code中使用 Rest Client插件,直接在编辑器里发送 REST 请求。

  • Code Runner

  • macOS 用户对 Code Runner 这个应用一定非常熟悉了,你可以使用 Code Runner 快速地书写代码并且执行,而无需设置环境配置工程之类的。VS Code里也有这样的插件,如果你有类似的需求,可以试一试。

  • Live Share

  • Live Share是微软官方出品的非常强大的服务,通过 Live Share service,你可以将你本地的工作区,直接分享给你的同伴,然后你的同伴就可以直接编辑你的代码,与你共享代码调试、集成终端等等,而无需安装任何环境。Atom 也有类似的服务叫做 Teletype。我工作中每次要和同事 Pair Programming 的时候,就会使用 Live Share。
    同时 Live Share 服务还支持语音通讯,不过需要安装另一个插件 Live Share Audio。

如何分享插件

  • 当然有!你可以通过在项目的 .vscode 文件夹下,创建一个文件 extensions.json。你很熟悉了,这又是一个 JSON 文件,在这个 JSON 文件里,你只需提供一个键(key) recommendations,然后将你想要推荐给这个项目的其他工程师的插件的 ID 们,全部放入到这个数组中。当他们打开这个项目,而且并没有安装这些插件时,VS Code就会给他们提示了。

  • 除了在 .vscode/extensions.json 文件推荐插件,如果你在使用多文件夹工作区(multi-root workspace),也可以在多文件夹工作区的配置文件里添加如下的设置:

VSCode C++ 配置

三个文件:task.json launch.json c_cpp_properties.json

  1. IntelliSense:Intelligence Sense,代码自动补全
  2. Task.json:
    告诉VScode如何编译.cpp文件,配置后将调用g++编译器基于源代码创建可执行文件。
    参数:
    command:设置要运行的指定程序
    args:参数数组指明要传送给g++的命令行参数,这些参数必须按照编译器要求的顺序来说明
    ${file}:g++执行的活动文件
    ${fileDirname}:当前目录
    label:任务列表中显示的值
  3. launch.json:使用F5启动GDB调试器来调试程序
  4. c_cpp_properties.json:对c/c++扩展实现更多控制,可以改变编译器的路径,C++标准以及更多

变量替换

  • VScode在launch.json调试文件和task.json任务文件中是支持变量替换的,这就意味着可以很方便的使用VScode一些预定以的变量。
  • 变量的使用方式:${variableName}
  • 常用的变量:
    • ${workspaceFolder} : 项目文件夹在VScode中打开的路径
    • ${file} : 当前打开的文件
    • ${relativeFile} : 相对于${workspaceFolder}的文件路径
    • ${fileBasename} : 当前打开文件的名称
    • ${fileBasenameNoExtension} : 当前打开文件的名称,不带扩展名
    • ${fileExtname} : 当前打开文件的扩展名
    • ${fileDirname} : 当前打开文件的文件夹名称

调试 断点

日志点,Logpoints

  • 日志点是断点的变体,它不会“中断”到调试器中,而是将消息记录到控制台。日志点对于再调试无法暂停或停止的生产服务器时注入日志记录特别有用。(A Logpoint is a variant of a breakpoint that does not “break” into the debugger but instead logs a message to the console. Logpoints are especially useful for injecting logging while debugging production servers that cannot be paused or stopped)
  • 日志消息是纯文本,但可以包含表达式,需要使用花括号

表达式条件断点

  • 条件断点是表达式结果为true时才会进行断点

命中计数断点

  • 只有该行代码命中了指定次数,才会进行断点

内联断点

  • 仅当执行到达与内联断点关联的列时,才会命中内联断点。
  • 这在调试一行中包含多个语句的缩小代码时特别有用。比如for循环,短路运算符等一行代码包含多个表达式时
  • 在指定位置按shift + F9

快捷键

  • ctrl + p : 快速搜索文件并跳转,添加:可以跳转到指定行
  • alt + 鼠标左键 : 选中多行同时编辑

简介

  • VSCode 编辑器常用的技巧

vscode windows下PATH环境变量更新,vscode未识别

  • 先退出VSCode
  • 打开cmd窗口,输入并执行 code 命令
  • 在新打开的VSCode的终端里,环境变量已经是最新的

vscode 页面切换到左侧工具栏

在 Visual Studio Code 中,如果你希望将焦点从编辑器切换到左侧的侧边栏(也称为 Activity Bar),可以使用以下方法:

  1. 使用键盘快捷键

    • 按下 Ctrl + 0(Windows/Linux)或者 Cmd + 0(Mac)可以将焦点从编辑器切换到侧边栏的第一个图标。
    • 使用 Ctrl + 1Ctrl + 2Ctrl + 3 等数字键(Windows/Linux)或者 Cmd + 1Cmd + 2Cmd + 3 等数字键(Mac)可以将焦点切换到侧边栏的不同图标。
  2. 使用鼠标

    • 直接点击侧边栏的图标来切换到相应的视图,例如资源管理器、搜索、源代码管理等。

这些方法可以帮助你快速地将焦点从编辑器切换到左侧的工具栏,以便访问不同的功能和视图。

vscode 分屏显示 切换

在 Visual Studio Code 中,你可以使用以下快捷键来实现分屏显示和切换:

  1. 分屏显示

    • 打开第一个文件后,按下 Ctrl + \(Windows/Linux)或者 Cmd + \(Mac)来进行分屏显示。这会在当前编辑器的右侧打开一个新的编辑器。
    • 或者,你可以通过右键点击文件选项卡,选择 “Split Editor”。
  2. 切换焦点

    • 使用 Ctrl + 1Ctrl + 2 等数字键(Windows/Linux)或者 Cmd + 1Cmd + 2 等数字键(Mac)来切换到不同的编辑器。
    • 或者,你可以使用 Ctrl + \(Windows/Linux)或者 Cmd + \(Mac)来切换焦点到分屏的另一个编辑器。
  3. 关闭分屏

    • 在分屏模式下,将鼠标悬停在编辑器的右上角,会看到一个关闭按钮(’X’)。点击该按钮可以关闭分屏。
  4. 重新分屏

    • 如果你只剩下一个编辑器,但是想要重新进行分屏,你可以使用 Ctrl + \(Windows/Linux)或者 Cmd + \(Mac)来重新分屏。

通过这些快捷键,你可以方便地在 Visual Studio Code 中进行分屏显示和切换。

日志断点调试

  • 断点模式设置为日志断点,输入表达式,随即会在output一栏输出
  • 例如输出变量command,则表达式为{command},随后回车就会输出

配置C++智能匹配

  • open the Command Palette (Ctrl+Shift+P)
  • enter Select IntelliSense Configuration.

配置C++

  • You can view the C/C++ configuration UI by running the command C/C++: Edit Configurations (UI) from the Command Palette (Ctrl+Shift+P).

简介

  • Boost 库概述

cmake配置boost

1
2
3
4
5
6
7
8
9
10
11
find_package(Boost 1.86 REQUIRED COMPONENTS system filesystem PATHS "${BaseDIR}/packages/boost_1_86_0/lib/cmake/Boost-1.86.0/" )
if(Boost_FOUND)
include_directories(${Boost_INCLUDE_DIRS})

MESSAGE( STATUS "Boost_INCLUDE_DIRS = ${Boost_INCLUDE_DIRS}.")
MESSAGE( STATUS "Boost_LIBRARIES = ${Boost_LIBRARIES}.")
MESSAGE( STATUS "Boost_LIB_VERSION = ${Boost_LIB_VERSION}.")

add_executable(foo foo.cpp)
target_link_libraries (foo ${Boost_LIBRARIES})
endif()

C++ boost库是什么

Boost 是一个功能强大的 C++ 库集合,为开发者提供广泛的、可移植的功能扩展。它被设计为标准库(STL)的补充,是现代 C++ 编程的重要工具。Boost 提供了许多高质量的库,涵盖了文件系统操作、多线程处理、智能指针、正则表达式、图算法等多个领域。


Boost 的主要特点

  1. 跨平台支持:Boost 的库可以在不同操作系统和编译器之间无缝工作。
  2. 标准化来源:Boost 的许多功能后来被采纳进了 C++ 标准库,例如 std::shared_ptr 和正则表达式。
  3. 模块化设计:Boost 包含了 100 多个模块,开发者可以按需使用特定的库,而不需要加载整个库集合。
  4. 高性能和可靠性:Boost 提供的功能经过社区的广泛测试和优化,性能优异且稳定。

Boost 的常用模块

以下是一些常用的 Boost 模块及其功能:

  1. Boost.SmartPtr
    提供智能指针(如 shared_ptr, weak_ptr),简化内存管理。

    1
    2
    #include <boost/shared_ptr.hpp>
    boost::shared_ptr<int> p(new int(10));
  2. Boost.Regex
    提供强大的正则表达式功能,类似于 std::regex

    1
    2
    #include <boost/regex.hpp>
    boost::regex expr("(\\w+)@(\\w+\\.\\w+)");
  3. Boost.Thread
    实现多线程编程,包括线程同步工具(如 mutexcondition_variable)。

    1
    2
    3
    #include <boost/thread.hpp>
    boost::thread t([] { std::cout << "Hello from Boost.Thread!" << std::endl; });
    t.join();
  4. Boost.Filesystem
    提供跨平台的文件和目录操作。

    1
    2
    #include <boost/filesystem.hpp>
    boost::filesystem::path p("/path/to/file");
  5. Boost.Asio
    支持异步输入输出(I/O)操作,用于网络编程和异步任务管理。

    1
    2
    3
    #include <boost/asio.hpp>
    boost::asio::io_context io;
    boost::asio::steady_timer timer(io, boost::asio::chrono::seconds(5));
  6. Boost.Serialization
    支持对象序列化和反序列化。

    1
    2
    3
    #include <boost/archive/text_oarchive.hpp>
    boost::archive::text_oarchive oa(std::cout);
    oa << my_object;
  7. Boost.Graph
    提供图算法和数据结构,用于复杂的图计算。


Boost 的安装与使用

  1. 下载与安装

    • Boost 官方网站 下载最新版本。
    • 解压后构建必要的库(静态或动态):
      1
      2
      ./bootstrap.sh
      ./b2
  2. 项目中使用 Boost

    • 在编译器中添加 Boost 的头文件路径:
      1
      g++ main.cpp -I /path/to/boost/include -L /path/to/boost/lib -lboost_system

Boost 的使用场景

  • 高效的内存管理:通过智能指针管理动态分配的内存,避免内存泄漏。
  • 跨平台文件操作:使用 Boost.Filesystem 处理文件路径和文件操作。
  • 异步编程:Boost.Asio 提供高效的异步任务处理和网络编程支持。
  • 多线程开发:Boost.Thread 为跨平台线程编程提供一致的接口。

Boost 的优缺点

优点:

  • 提供大量实用工具,节省开发时间。
  • 社区活跃,文档详尽。
  • 与 C++ 标准库深度结合,部分功能已纳入 C++ 标准。

缺点:

  • 有些模块的学习曲线较陡。
  • 部分库构建耗时较长。
  • 在某些情况下,库的体积较大。

相关资源

BOOST库 有哪些模块 详解

Boost 库是一个流行的 C++ 库集合,提供了各种各样的模块和功能,涵盖了多个领域,包括但不限于数据结构、算法、多线程、文件系统、网络编程、数学计算等。下面是一些主要的 Boost 模块:

  1. Boost.Asio:提供异步 I/O 和网络编程功能,包括套接字、TCP、UDP、定时器、异步任务等,用于构建高性能网络应用程序。

  2. Boost.Beast:基于 Boost.Asio,提供对 HTTP、WebSocket 和网络通信的支持,简化了处理 HTTP 和 WebSocket 协议的操作。

  3. Boost.Filesystem:提供了对文件系统操作的抽象接口,包括文件路径操作、文件/目录的创建、删除、移动、复制等。

  4. Boost.ThreadBoost.Atomic:提供了多线程和原子操作的支持,使得在 C++ 中更容易编写并发代码。

  5. Boost.Container:提供了一系列 STL 容器的扩展和优化版本,如 flat_map、flat_set、static_vector 等,增强了标准库中的容器功能。

  6. Boost.Graph:提供了图论相关的数据结构和算法,包括图的表示、遍历、最短路径等操作。

  7. Boost.Math:提供了数学计算的库,包括数值计算、特殊函数、概率分布、统计函数等,用于处理数学问题。

  8. Boost.Serialization:提供了用于对象序列化和反序列化的库,可以将对象转换为字节流以便存储或传输,并从字节流恢复为对象。

  9. Boost.Regex:提供了正则表达式的功能,用于文本匹配和处理。

  10. Boost.Date_Time:提供了日期和时间处理的库,包括日期时间表示、格式化、时区处理等。

  11. Boost.PropertyTree:用于处理树状结构的数据,例如 XML、JSON 等格式的数据。

  12. Boost.UUID:用于生成和操作 UUID(通用唯一标识符)的库。

  13. Boost.Test:提供了单元测试框架,用于进行 C++ 程序的单元测试。

  14. Boost.Locale:提供了本地化和国际化支持,包括字符集转换、日期时间格式化、货币格式化等功能。

  15. Boost.Process:提供了对进程的管理和操作功能。

以上列举的模块只是 Boost 库中的一小部分,Boost 还包含许多其他模块和库,每个模块都专注于不同的领域,为 C++ 开发提供了广泛的工具和功能支持。每个模块都有其自己的文档和用法,开发者可以根据需要选择并使用适合的模块。

简介

  • boost库相关的笔记

Boost 库 编译 教程 详解

Boost C++ 库是一个非常强大的 C++ 库,提供了大量的功能和工具,包括智能指针、多线程、正则表达式、容器、图形学等。编译 Boost 库可能会有一些复杂性,因为 Boost 包含了大量的头文件和库,但下面是一个简要的教程,演示如何编译 Boost 库。

步骤 1:下载 Boost 库

首先,您需要下载 Boost 库的源代码。您可以从 Boost 官方网站(https://www.boost.org/)下载最新版本的 Boost 库。将源代码文件解压到您选择的目录中。

步骤 2:打开终端

在终端中导航到 Boost 库的源代码目录。您可以使用 cd 命令切换到该目录。

步骤 3:运行 Bootstrap 脚本

Boost 提供了一个名为 bootstrap.sh(Linux/macOS)或 bootstrap.bat(Windows)的脚本,用于配置编译过程。在终端中运行以下命令:

对于 Linux/macOS:

1
./bootstrap.sh

对于 Windows:

1
bootstrap.bat

这将为您的系统配置 Boost 库的编译过程。

步骤 4:运行 b2 命令

接下来,您需要运行 b2 命令来编译 Boost 库。您可以使用以下命令:

1
./b2

这将默认编译所有 Boost 库的组件。如果您只需要特定的库,您可以在 b2 命令后面添加库名称,例如:

1
./b2 --prefix=/tmp/boost --with-filesystem --with-system

这将仅编译文件系统和系统库。

步骤 5:等待编译完成

编译 Boost 库可能需要一些时间,具体取决于您的系统性能和所选的库。一旦编译完成,您将在 Boost 源代码目录中找到生成的库文件。

步骤 6:安装 Boost 库

您可以选择将编译后的库文件安装到系统目录中,以便其他项目可以轻松使用它们。运行以下命令:

1
sudo ./b2 install

这将把库文件复制到系统的默认位置。

注意:在 Windows 上,您可能需要使用 Visual Studio 编译工具来编译 Boost 库。您可以在 Boost 官方网站上找到有关使用 Visual Studio 的更多信息。

这是一个简要的 Boost 库编译教程。具体的步骤可能因您的系统和需求而有所不同。为了获得更详细的信息和特定于您的平台的说明,请查阅 Boost 文档或参考 Boost 官方网站上的编译指南。

cmake中引用Boost库

  • 通过调用find_package可以找到头文件和所需要的库文件或者是一个CMake打包配置文件

    1
    2
    3
    4
    5
    find_package(Boost
    [version] [EXACT] # 可选项,最小版本或者确切所需版本
    [REQUIRED] # 可选项,如果找不到所需库,报错
    [COMPONENTS <libs>...] # 所需的库名称,比如说. "date_time" 代表 "libboost_date_time"
    )
  • 示例

    1
    2
    find_package(Boost 1.62.0 REQUIRED
    COMPONENTS system filesystem thread)
  • 运行之后可以得到很多变量,下面列了一些主要的:

    1
    2
    3
    4
    5
    6
    Boost_FOUND            - 如果找到了所需的库就设为true
    Boost_INCLUDE_DIRS - Boost头文件搜索路径
    Boost_LIBRARY_DIRS - Boost库的链接路径
    Boost_LIBRARIES - Boost库名,用于链接到目标程序
    Boost_VERSION - 从boost/version.hpp文件获取的版本号
    Boost_LIB_VERSION - 某个库的版本
  • 如果Boost库是自定义安装路径,可以在搜索package之前,通过设置一些变量来帮助boost库查找

    1
    2
    3
    4
    BOOST_ROOT             - 首选的Boost安装路径
    BOOST_INCLUDEDIR - 首选的头文件搜索路径 e.g. <prefix>/include
    BOOST_LIBRARYDIR - 首选的库文件搜索路径 e.g. <prefix>/lib
    Boost_NO_SYSTEM_PATHS - 默认是OFF. 如果开启了,则不会搜索用户指定路径之外的路径
  • 如果目标程序foo需要链接Boost库的regex和system,编写如下的CMakeist文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    # CMakeLists.txt
    project(tutorial-0)
    cmake_minimum_required(VERSION 3.7)
    set(CMAKE_CXX_STANDARD 14)

    set(BOOST_ROOT /usr/local/install/boost_1_62_0) // 设置boost库搜索路径
    set(Boost_NO_SYSTEM_PATHS ON) // 只搜索上语句设置的搜索路径

    find_package(Boost COMPONENTS regex system REQUIRED)

    if(Boost_FOUND)
    include_directories(${Boost_INCLUDE_DIRS})

    MESSAGE( STATUS "Boost_INCLUDE_DIRS = ${Boost_INCLUDE_DIRS}.")
    MESSAGE( STATUS "Boost_LIBRARIES = ${Boost_LIBRARIES}.")
    MESSAGE( STATUS "Boost_LIB_VERSION = ${Boost_LIB_VERSION}.")

    add_executable(foo foo.cpp)
    target_link_libraries (foo ${Boost_LIBRARIES})
    endif()
  • 通过设置BOOST_ROOT来设置首选的搜索路径

  • 通过MESSAGE函数把差找的结果都打印出来

  • Boost动态库链接。如果项目包含多个子模块,且子模块只用到顶层模块find_package找到的部分库,则用下述语句实现只链接子模块需要的Boost动态库

    1
    target_link_libraries(foo Boost::regex) // 只使用regex
  • Boost头文件库链接。在Boost库中有部分库只用头文件实现,并没有响应的动态库,若使用这部分库可以通过以下语句实现

    1
    2
    3
    target_link_libraries(foo Boost::boost)
    or
    target_include_directories(${Boost_INCLUDE_DIRS})

Boost C++ 库 使用示例 详解

Boost C++ 库包含了众多功能和组件,因此无法在一篇回答中详尽介绍所有。下面将展示一个使用 Boost 库的示例,涵盖智能指针、正则表达式和文件系统三个常用部分。请确保您已经成功编译了 Boost 库并将其包含在项目中。

1. 智能指针(Smart Pointers)示例:

智能指针帮助管理动态分配的内存,防止内存泄漏。以下是一个使用 Boost 智能指针的示例:

1
2
3
4
5
6
7
8
9
10
11
12
#include <boost/shared_ptr.hpp>
#include <iostream>

int main() {
boost::shared_ptr<int> sharedInt(new int(42));

std::cout << "Shared pointer value: " << *sharedInt << std::endl;
std::cout << "Reference count: " << sharedInt.use_count() << std::endl;

// sharedInt 在离开作用域时会自动释放内存
return 0;
}

在上述示例中,我们使用了 boost::shared_ptr 来创建一个智能指针,它会自动管理整数的内存分配和释放。use_count() 函数用于获取引用计数。

2. 正则表达式示例:

Boost.Regex 提供了强大的正则表达式功能。以下是一个使用 Boost.Regex 的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <boost/regex.hpp>
#include <iostream>
#include <string>

int main() {
std::string text = "Boost Libraries are awesome!";
boost::regex reg("Boost.*awesome");

if (boost::regex_search(text, reg)) {
std::cout << "Match found!" << std::endl;
} else {
std::cout << "No match found." << std::endl;
}

return 0;
}

上述示例中,我们使用 boost::regex 创建了一个正则表达式,然后使用 boost::regex_search 检查字符串中是否包含匹配的文本。

3. 文件系统示例:

Boost 文件系统库提供了文件和目录操作功能。以下是一个使用 Boost 文件系统库的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <boost/filesystem.hpp>
#include <iostream>

int main() {
boost::filesystem::path dirPath("./my_directory");

if (boost::filesystem::exists(dirPath)) {
if (boost::filesystem::is_directory(dirPath)) {
std::cout << "Directory exists." << std::endl;
} else {
std::cout << "Not a directory." << std::endl;
}
} else {
std::cout << "Directory does not exist." << std::endl;
}

return 0;
}

在上述示例中,我们使用 boost::filesystem::path 表示目录路径,并使用 boost::filesystem::existsboost::filesystem::is_directory 函数来检查目录是否存在以及是否为目录。

这些示例涵盖了 Boost C++ 库的一小部分功能。要使用 Boost 的其他组件和功能,请查阅 Boost 官方文档,其中包含了详细的教程和示例代码,以帮助您更好地理解和应用 Boost 库的各个组件。

Boost C++ 库 详解

Boost C++ 库是一个非常受欢迎的开源 C++ 库集合,提供了各种功能和工具,用于增强 C++ 编程,包括数据结构、算法、多线程、正则表达式、智能指针、文件系统操作等。以下是 Boost C++ 库的一些主要组成部分和功能的详细介绍:

  1. 智能指针(Smart Pointers):Boost 提供了各种智能指针,如 shared_ptrunique_ptrweak_ptr,用于管理动态分配的内存,以避免内存泄漏和提高代码安全性。

  2. 容器和数据结构:Boost 包括许多增强的容器和数据结构,如 unordered_mapunordered_setmulti_indexvariantany,用于更有效地管理数据。

  3. 多线程支持:Boost 提供了一套多线程库,包括线程、锁、条件变量和原子操作,以便于编写并发程序。

  4. 正则表达式:Boost.Regex 提供了强大的正则表达式库,使您可以进行高级文本匹配和处理。

  5. 文件系统:Boost 文件系统库允许您进行文件和目录的操作,包括文件检查、复制、移动和删除。

  6. 日期时间和时间戳:Boost.Date_Time 库提供了日期、时间和时间戳处理的功能,可用于处理时间相关的任务。

  7. 图形学库:Boost.Graph 库用于图形算法和数据结构,支持图形遍历、搜索和分析。

  8. 泛型编程和元编程:Boost 具有强大的泛型编程和元编程工具,包括预处理器宏、类型萃取和模板元编程,用于创建通用、高效的代码。

  9. 库之间的交互性:Boost 库之间通常能够很好地协同工作,因此您可以轻松地将它们组合在一起,以满足特定需求。

  10. 跨平台性:Boost 在多种操作系统和编译器上都能正常工作,因此它具有很强的跨平台性。

  11. 社区支持:Boost 是一个由社区驱动的项目,拥有广泛的用户和开发者社区,因此您可以在社区中获得支持和解答问题。

  12. C++标准化贡献:Boost 库中的一些功能已被采纳并成为 C++ 标准库的一部分,如智能指针 (std::shared_ptrstd::unique_ptr) 和正则表达式 (std::regex)。

要使用 Boost 库,通常需要将其源代码包括头文件和库链接到您的项目中。您可以在 Boost 官方网站上找到详细的文档和教程,以帮助您入门和使用 Boost 库的各个组件。Boost 提供了广泛的文档和示例代码,以便您更好地理解和使用这些功能。

简介

  • boost常用函数

boost::ignore_unused()

boost::ignore_unused() 不是一个函数,而是一个辅助函数宏,用于防止编译器产生“未使用变量”的警告。这个宏是在 Boost 库中定义的,位于 <boost/core/ignore_unused.hpp> 头文件中。

在 C++中,如果你声明了一个变量但在后续的代码中没有使用,编译器可能会发出警告。为了避免这样的警告,可以使用 boost::ignore_unused() 来告诉编译器你有意不使用这个变量。

下面是 boost::ignore_unused() 的简要说明和示例:

  1. 函数签名:

    1
    2
    template <class... Ts>
    inline void ignore_unused(Ts const&...) noexcept;

    这个宏接受任意数量的参数,并在编译时告诉编译器忽略这些参数的未使用警告。

  2. 使用场景:

    • 防止未使用变量的警告: 当你有意声明了一个变量但在后续的代码中没有使用时,可以使用 boost::ignore_unused() 来避免编译器产生未使用变量的警告。
  3. 示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    #include <boost/core/ignore_unused.hpp>

    int main() {
    int x = 42;

    // 防止编译器警告,因为 y 没有在后续的代码中使用
    boost::ignore_unused(x);

    return 0;
    }

    在这个示例中,boost::ignore_unused(x) 告诉编译器忽略变量 x 的未使用警告。这在一些情况下很有用,例如在编写模板代码时,你可能有一些参数是在某些情况下使用的,但在其他情况下可能未使用。

总的来说,boost::ignore_unused() 是一个方便的宏,用于防止编译器因未使用变量而产生的警告。

简介

  • Asio库相关的理论基础知识

  • Asio库是仅包含头文件的库

C++ Asio库独立版本详解

Boost.Asio 是一个非常流行的 C++ 库,用于实现异步 I/O 操作和网络编程。它最初作为 Boost C++ 库的一部分,但后来成为 C++ 标准库的一部分(自 C++11 起)。因此,Boost.Asio 的某些版本也包含在 Boost 库中。

独立版本的 Boost.Asio 是指可单独使用、独立于 Boost 库的版本。它通常是从 Boost 库中分离出来的、具有更轻量级的库。这样的版本可以更容易地集成到项目中,而无需包含整个 Boost 库。

独立版本的 Boost.Asio 通常具有以下特点:

  1. 独立性: 它不需要完整的 Boost 库作为依赖,可以单独使用。
  2. 轻量级: 与完整的 Boost 库相比,其体积较小,减少了不必要的依赖。
  3. 简化构建和集成: 由于它是一个独立的库,因此在项目中使用起来更加方便,构建和集成更简单。

Boost.Asio 独立版本的详细内容和特性与原始的 Boost.Asio 库非常相似,主要提供异步 I/O、TCP、UDP、定时器和网络编程相关的功能。它仍然支持异步事件和异步操作,提供了用于管理 I/O 对象、定时器和网络通信的类和函数。

要使用独立版本的 Boost.Asio,你可以从官方的 GitHub 仓库或下载页获取源代码,并按照提供的指南进行构建和集成。通常,独立版本的 Boost.Asio 具有更加现代化和改进的特性,因为它们可能是从较新版本的 Boost 库中提取出来的。

请注意,独立版本的 Boost.Asio 可能会有所不同,具体取决于版本和发布的时间。因此,建议查看官方文档和版本说明以获取特定版本的详细信息和使用说明。

简介

  • BOOST常用编程技巧

boost::filesystem::path 转 std::string

要将 boost::filesystem::path 转换为 std::string,你可以使用 boost::filesystem::path 对象的 string() 成员函数。这个函数返回一个表示路径的 std::string。以下是一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <boost/filesystem.hpp>
#include <iostream>
#include <string>

int main() {
boost::filesystem::path filePath("/path/to/your/file.txt");

// 将 boost::filesystem::path 转换为 std::string
std::string filePathString = filePath.string();

// 打印转换后的 std::string
std::cout << "File path as string: " << filePathString << std::endl;

return 0;
}

在这个示例中,filePath.string() 函数将 boost::filesystem::path 对象 filePath 转换为一个 std::string,然后我们将其打印出来。

简介

  • Asio相关的函数

Asio asio::chrono::seconds()函数 详解

在 Boost.Asio 中,asio::chrono::seconds() 函数是用于创建时间持续时间(duration)对象的函数,表示以秒为单位的时间段。

这个函数位于 Boost.Asio 的时间相关命名空间中,asio::chrono,用于创建与时间相关的持续时间对象,其中包括 std::chrono::duration 的各种变种。

使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
#include <boost/asio.hpp>

int main() {
// 使用 asio::chrono::seconds() 创建秒数为 5 的持续时间对象
boost::asio::steady_timer timer(boost::asio::chrono::seconds(5));

// 打印定时器的过期时间(当前时间加上 5 秒)
std::cout << "Timer will expire after 5 seconds." << std::endl;

return 0;
}

在上述示例中,asio::chrono::seconds(5) 创建了一个表示 5 秒的时间段,然后将该时间段传递给 boost::asio::steady_timer 的构造函数,用于设置定时器在 5 秒后触发。

此函数的作用在于以秒为单位创建时间段,可以用于设置定时器、指定等待时间等场景。 Boost.Asio 中的这些时间函数通常是与异步操作和定时器相关的,使得在异步编程中方便地创建和管理时间段。

Asio asio::steady_timer::async_wait() 函数 详解

asio::steady_timer::async_wait() 是 Boost.Asio 中 steady_timer 类的成员函数,用于异步等待定时器到期并触发回调函数。

函数签名:

1
2
template <typename WaitHandler>
void async_wait(WaitHandler&& handler);

参数说明:

  • WaitHandler:一个可调用对象,用于处理定时器到期时触发的回调函数。可以是函数指针、函数对象、lambda 函数等。

功能:

async_wait() 函数安排一个异步操作,在指定的定时器到期时执行回调操作。该函数不会阻塞当前线程,而是在设置的时间段之后触发回调函数。

使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
#include <boost/asio.hpp>

void timer_handler(const boost::system::error_code& ec) {
if (!ec) {
std::cout << "Timer expired!" << std::endl;
}
}

int main() {
boost::asio::io_context io_context;
boost::asio::steady_timer timer(io_context, boost::asio::chrono::seconds(5));

// 异步等待定时器触发,并指定回调函数
timer.async_wait(timer_handler);

// 运行 io_context 上的事件循环
io_context.run();

return 0;
}

在上述示例中,async_wait() 函数用于异步等待定时器 timer 到期,并指定了一个名为 timer_handler 的回调函数处理定时器到期时的操作。io_context.run() 开始运行事件循环,等待定时器触发并执行回调函数。

该函数常用于异步编程中,用于设置定时器的到期事件,并在到期时执行相应的操作或回调函数。

Asio asio::steady_timer::expires_at() 函数 详解

asio::steady_timer::expires_at() 是 Boost.Asio 中 steady_timer 类的成员函数之一,用于设置定时器的到期时间点。

函数签名:

1
2
template <typename TimePoint>
void expires_at(const TimePoint& time_point);

参数说明:

  • TimePoint:表示时间点的类型,通常为 std::chrono::time_point,用于指定定时器的到期时间点。

功能:

expires_at() 函数用于设置定时器 steady_timer 的到期时间点。当调用该函数并传递一个特定的时间点参数时,定时器将在指定的时间点触发,即执行定时器的回调函数或触发定时器到期事件。

使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
#include <boost/asio.hpp>

int main() {
boost::asio::io_context io_context;
boost::asio::steady_timer timer(io_context);

// 设置定时器到期时间点为当前时间加上 5 秒
timer.expires_at(std::chrono::steady_clock::now() + std::chrono::seconds(5));

// 异步等待定时器触发
timer.async_wait([](const boost::system::error_code& ec) {
if (!ec) {
std::cout << "Timer expired!" << std::endl;
}
});

// 运行 io_context 上的事件循环
io_context.run();

return 0;
}

在上述示例中,timer.expires_at() 用于设置定时器 timer 的到期时间点为当前时间加上 5 秒。随后使用 timer.async_wait() 异步等待定时器触发,在定时器到期时执行回调函数。最后,通过 io_context.run() 开始运行事件循环,等待定时器到期并执行回调函数。

expires_at() 函数对于预先设置定时器的到期时间非常有用,允许在稍后的时间点触发异步操作,使得在异步编程中更容易地控制定时器的触发时间。

Asio asio::steady_timer::expiry() 详解

在 Boost.Asio 中,asio::steady_timer::expiry()steady_timer 类的成员函数,用于获取定时器的到期时间点。

方法签名:

1
std::chrono::steady_clock::time_point expiry() const noexcept;

功能:

expiry() 方法用于获取当前设置的定时器 steady_timer 的到期时间点。返回的是一个 std::chrono::steady_clock::time_point 对象,表示定时器将会到期的时间点。

使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
#include <boost/asio.hpp>

int main() {
boost::asio::io_context io_context;
boost::asio::steady_timer timer(io_context);

// 设置定时器到期时间点为当前时间加上 5 秒
timer.expires_at(std::chrono::steady_clock::now() + std::chrono::seconds(5));

// 获取定时器的到期时间点并打印
std::chrono::steady_clock::time_point expiry_time = timer.expiry();
std::time_t expiry_time_c = std::chrono::steady_clock::to_time_t(expiry_time);
std::cout << "Timer will expire at: " << std::ctime(&expiry_time_c);

io_context.run();

return 0;
}

在这个示例中,使用 timer.expires_at() 将定时器 timer 的到期时间点设置为当前时间加上 5 秒。然后调用 expiry() 方法获取定时器的到期时间点,并将其转换为 std::time_t 格式,最后使用 std::ctime() 打印定时器将会到期的时间点。

expiry() 方法对于需要了解定时器何时到期的情况非常有用,它允许程序员检查定时器当前的到期时间点,以便在需要时执行相应的操作。

Asio asio::make_strand() 详解

在 Boost.Asio 中,asio::make_strand() 是一个用于创建 strand 对象的工厂函数。strand 提供了一种同步操作访问序列化(serialized)的方式,用于确保异步操作在多个线程中按照顺序执行,避免出现竞争条件。

函数签名:

1
2
template <typename ExecutionContext>
typename associated_executor<ExecutionContext>::type make_strand(ExecutionContext& context);

参数说明:

  • ExecutionContext:表示执行上下文的类型,可以是 io_contextstrand 或其他支持的执行上下文类型。

返回值:

  • 返回创建的 strand 对象。

功能:

make_strand() 函数用于创建一个 strand,它实际上是一个执行器(executor),可以确保在同一个 strand 内排队的操作按顺序执行,而不会发生竞争条件。

使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
#include <boost/asio.hpp>

int main() {
boost::asio::io_context io_context;

// 创建一个 strand 对象
auto strand = boost::asio::make_strand(io_context);

// 在 strand 内执行异步操作
boost::asio::post(strand, []() {
std::cout << "Task 1 executed in strand!" << std::endl;
});

boost::asio::post(strand, []() {
std::cout << "Task 2 executed in strand!" << std::endl;
});

io_context.run();

return 0;
}

在这个示例中,make_strand() 创建了一个 strand 对象 strand,然后使用 boost::asio::post() 将两个任务(lambda 函数)提交到 strand 内执行。由于这两个任务被提交到同一个 strand 中,它们会按照顺序在该 strand 中执行,避免了并发操作可能引发的竞争条件。

strand 对于需要序列化异步操作的情况非常有用,可以确保在一个特定的执行上下文中操作按顺序执行,增加了程序的可靠性和安全性。

Asio asio::bind_executor() 详解

在 Boost.Asio 中,asio::bind_executor() 是一个用于绑定执行上下文(executor)到处理器(handler)上的函数,它用于创建一个新的处理器对象,将给定的执行上下文绑定到现有的处理器上,从而确保处理器在指定的执行上下文中执行。

函数签名:

1
2
template <typename Executor, typename Handler>
decltype(auto) bind_executor(const Executor& ex, Handler&& handler);

参数说明:

  • Executor:表示执行上下文的类型,例如 io_contextstrand
  • Handler:表示处理器的类型,可以是函数对象、函数指针或者可调用对象。

返回值:

  • 返回一个新的处理器对象,将给定的执行上下文与原有的处理器绑定在一起。

功能:

bind_executor() 函数用于创建一个新的处理器对象,它在调用原始处理器时,会确保使用提供的执行上下文(executor)进行执行。这样做可以确保处理器在指定的执行上下文中运行,从而实现异步操作的序列化和控制。

使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <boost/asio.hpp>

int main() {
boost::asio::io_context io_context;

// 创建一个 strand 对象
boost::asio::strand<boost::asio::io_context::executor_type> strand(io_context.get_executor());

// 绑定执行上下文到处理器
auto handler = asio::bind_executor(strand, [](const boost::system::error_code& ec) {
if (!ec) {
std::cout << "Handler executed in strand!" << std::endl;
}
});

// 执行处理器(实际上是在 strand 内执行)
handler(boost::system::error_code()); // 模拟传递错误码

io_context.run();

return 0;
}

在这个示例中,asio::bind_executor() 函数将一个处理器对象(lambda 函数)与一个执行上下文(strand)绑定在一起。然后,通过调用返回的新处理器对象 handler,可以确保该处理器在指定的 strand 中运行,从而保证了处理器的操作在该执行上下文中进行。这种绑定执行上下文到处理器的方式常用于确保异步操作的顺序执行或者将处理器绑定到特定的执行上下文中执行。

简介

  • Asio常用的类

Asio asio::io_context 详解

asio::io_context 是 Boost.Asio 库(也是 C++ 标准库中的一部分,自 C++17 起)中的核心类之一。它是实现异步 I/O 操作的关键部分,用于驱动异步事件处理。

作用:

  • 提供 I/O 上下文: io_context 对象是异步操作的执行上下文,用于管理异步操作、事件处理、任务队列和事件循环。
  • 事件驱动: 通过 io_context,可以注册异步操作(如套接字操作、定时器事件等),io_context 将在合适的时机进行调度、执行和完成这些异步操作。

主要功能和方法:

  • run() 开始 io_context 上的事件循环,处理已注册的所有异步操作,直到所有操作完成或 io_context 被停止。
  • stop() 停止 io_context 上的事件循环。停止后,run() 函数将在处理完当前已注册的操作后立即返回。
  • poll() 执行 io_context 上的事件循环,但仅处理当前可立即完成的操作,然后立即返回。
  • restart() 重新启动已经停止的 io_context

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
#include <boost/asio.hpp>

int main() {
boost::asio::io_context io_context;

// 创建一个定时器
boost::asio::steady_timer timer(io_context, boost::asio::chrono::seconds(5));

// 异步等待定时器完成
timer.async_wait([](const boost::system::error_code& ec) {
if (!ec) {
std::cout << "Timer expired!" << std::endl;
}
});

// 运行 io_context 上的事件循环,直到所有操作完成
io_context.run();

return 0;
}

在上述示例中,io_context 用于驱动异步操作(这里是一个定时器异步等待)。通过调用 run() 方法,io_context 开始处理已注册的异步操作,并在完成所有操作或者遇到停止指令时返回。

io_context 是 Boost.Asio 中非常重要的一个类,它为异步操作提供了执行环境,能够有效地管理和调度异步事件,是异步编程的核心。

Asio asio::thread_pool 详解

asio::thread_pool 是 Boost.Asio 库中的一个类,它提供了一个线程池,用于管理和执行异步操作。线程池是一种用于管理线程的技术,它可以预先创建一组线程,以便在需要时执行任务或处理异步操作。

主要作用:

  • 管理线程: asio::thread_pool 提供了线程池,可用于执行异步操作,避免了频繁创建和销毁线程的开销。
  • 处理异步操作: 可以将异步操作(如定时器、套接字操作等)提交给线程池,线程池会自动将其分配到可用的线程上执行。

主要方法和功能:

  • asio::thread_pool(size_t num_threads) 构造函数: 创建具有指定数量线程的线程池。
  • ~thread_pool() 析构函数: 销毁线程池,等待所有线程执行完毕并释放资源。
  • submit(Function && function) 提交任务到线程池,执行 function 函数。
  • stop() 停止线程池,不再接受新的任务,等待所有任务执行完毕后销毁线程池。
  • join() 阻塞等待线程池中的所有任务执行完成。
  • notify_one()notify_all() 用于唤醒正在等待的线程。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
#include <boost/asio/thread_pool.hpp>
#include <boost/asio/post.hpp>

void task() {
std::cout << "Task executed in thread: " << std::this_thread::get_id() << std::endl;
}

int main() {
boost::asio::thread_pool pool(4); // 创建有4个线程的线程池

// 提交任务到线程池
for (int i = 0; i < 8; ++i) {
boost::asio::post(pool, task);
}

// 等待所有任务完成
pool.join();

return 0;
}

在上述示例中,创建了一个具有 4 个线程的线程池 pool,然后向线程池提交了 8 个任务。这些任务会被线程池中的线程异步执行。最后,调用 pool.join() 阻塞等待所有任务执行完成。

asio::thread_pool 提供了一种有效地管理和执行异步操作的方式,避免了线程频繁创建和销毁的开销,并提高了异步操作的执行效率。

Asio asio::ip::tcp::socket 详解

asio::ip::tcp::socket 是 Boost.Asio 库中用于 TCP 协议的套接字类,用于在 C++ 中进行 TCP 网络通信。

主要作用:

  • 实现 TCP 客户端和服务器: asio::ip::tcp::socket 允许 C++ 应用程序创建 TCP 客户端或服务器套接字,并进行数据传输。

主要方法和功能:

  • constructor 构造函数: 创建 TCP 套接字。
  • open() 打开套接字。
  • close() 关闭套接字。
  • connect() 用于客户端,连接到远程服务器。
  • async_connect() 异步连接到远程服务器。
  • bind() 将套接字与本地端口或地址绑定。
  • async_bind() 异步绑定套接字。
  • listen() 在服务器上监听传入连接请求。
  • accept() 接受传入的连接请求。
  • async_accept() 异步接受传入的连接请求。
  • read_some()write_some() 同步读取和写入数据。
  • async_read_some()async_write_some() 异步读取和写入数据。
  • shutdown() 关闭套接字的输入、输出或全部流。

示例(简化的服务器端):

下面是一个简化的 Boost.Asio TCP 服务器端示例,展示了如何使用 asio::ip::tcp::socket 接受连接和读取数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iostream>
#include <boost/asio.hpp>

using boost::asio::ip::tcp;

int main() {
boost::asio::io_context io_context;
tcp::acceptor acceptor(io_context, tcp::endpoint(tcp::v4(), 8080));

while (true) {
tcp::socket socket(io_context);
acceptor.accept(socket);

std::array<char, 128> buffer;
boost::system::error_code error;

// 读取数据
size_t len = socket.read_some(boost::asio::buffer(buffer), error);
if (error == boost::asio::error::eof) {
std::cout << "Connection closed by peer." << std::endl;
} else if (error) {
std::cerr << "Error: " << error.message() << std::endl;
} else {
std::cout << "Received data: " << std::string(buffer.data(), len) << std::endl;
}
}

return 0;
}

asio::ip::tcp::socket 是 Boost.Asio 中用于 TCP 通信的关键类之一,提供了处理 TCP 套接字的方法和功能,可以用于创建 TCP 客户端或服务器,并进行数据的读写操作。

Asio asio::steady_timer详解

asio::steady_timer 是 Boost.Asio 库中的一个定时器类,用于在指定时间点执行或触发操作。

主要作用:

  • 定时触发事件: asio::steady_timer 用于创建定时器对象,可以在设定的时间点之后触发回调函数。

主要方法和功能:

  • constructor 构造函数: 创建定时器对象。
  • expires_at()expires_from_now() 分别设置定时器的到期时间和到期时刻的相对偏移量。
  • async_wait() 异步等待定时器触发。可以向定时器对象提交一个回调函数,在指定时间点触发回调。
  • cancel() 取消定时器,终止尚未触发的操作。
  • wait() 阻塞等待定时器触发。

示例:

下面是一个简单的 Boost.Asio 定时器示例,演示了 asio::steady_timer 的基本用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
#include <boost/asio.hpp>

void timer_handler(const boost::system::error_code& ec) {
if (!ec) {
std::cout << "Timer expired!" << std::endl;
}
}

int main() {
boost::asio::io_context io_context;
boost::asio::steady_timer timer(io_context, boost::asio::chrono::seconds(5));

// 异步等待定时器触发,并指定回调函数
timer.async_wait(timer_handler);

// 运行 io_context 上的事件循环
io_context.run();

return 0;
}

在上述示例中,创建了一个 asio::steady_timer 对象 timer,设置定时器在 5 秒后触发。然后使用 timer.async_wait() 异步等待定时器的触发,并指定了一个回调函数 timer_handler。最后,调用 io_context.run() 运行事件循环,等待定时器触发并执行回调函数。

asio::steady_timer 类是 Boost.Asio 中用于管理定时器的重要类之一,允许程序在指定的时间点执行特定操作,通常用于实现定时任务、超时控制等功能。

简介

  • C++17代码整洁之道阅读笔记

第二章 构建安全体系

2.3 单元测试

  • 单元测试是一小段代码,在特定上下文环境中,单元测试能够执行产品的一部分代码。单元测试能够在很短的时间内,展示出你的代码是否达到了预期的运行结果。

  • 单元测试框架

    • C++的单元测试框架有很多种,例如: CppUnit, Boost.Test, CUTE, Google Test等
    • 一般而言,几个单元测试框架的集合成为xUnit,所有遵循所谓的xUnit的基本设计的单元测试的框架,其结构和功能都是从Smalltalk的SUnit集成而来的。

2.5 良好的单元测试原则

  • 单元测试的代码的质量

    • 高质量的要求产品代码,同样高质量的要求单元测试的代码。更进一步的讲,理论上,产品代码和测试代码之间不应该有任何区别
  • 单元测试的命名

    • 如果单元测试失败,开发人员希望立即知道以下信息:
      • 测试单元的名称是什么?谁的单元测试失败了?
      • 单元测试测试了什么?单元测试的环境是怎么样的(测试场景)
      • 预期的单元测试结果是什么?单元测试失败的实例测试结果又是什么
    • 因此,单元测试的命名需要具备直观性和描述性,这是非常重要的
  • 首先,以这样的方式命名单元测试模块(依赖于单元测试框架,称为测试用具或者测试夹具)是很好的做法,这样单元测试代码很容易衍生于单元测试框架。单元测试应该有一个像 Test的名字,很显然,必须用测试对象的名字来替换 占位符

  • 例如,如果被测试的系统(SUT)是Money单位,与该测试单元对应的单元测试夹具,以及所有的单元测试用例都应该命名为MoneyTest

  • 除此之外,单元测试必须有直观的且易理解的名称,如果单元测试的名称或多或少没有意义,那么单元测试的名称不会有太大的帮助。通过下面的建议,可以为单元测试取一个好名字

    • 一般来说,可以在不同场景下使用多种用途的类,一个直观的且易理解的名称应该包含以下三点:
      • 单元测试的前置条件,也就是执行单元测试之前的SUT的状态
      • 被单元测试测试的部分,通常是被测试的过程,函数或者方法(API)的名称
      • 单元测试预期的测试结果
    • 遵循以上三点建议,测试过程或方法的单元测试命名的模板,如下所示
    • 示例
      • void CustomerCacheTest::cacheIsEmpty_addElement_sizeIsOne();
      • void MoneyTest::giveTwoMoneyObjectsWithDifferentBalance_theInequalityComparison_Works();
    • 另一个构建直观的且易理解的单元测试名称的方法,就是在单元测试名称中显示特定的需求。这样的单元测试的名称通常能够反应应用程序域的需求
    • 示例
      • void UserAccountTest::creatingNewAccountWithExistingEmailAddressThrowsException();
      • void BookInventoryTest::aBookThatIsAlreadyBorrowedCanNotBeBorrowedTwice();
    • 几乎所有的单元测试框架都会把失败的单元测试名称输出到标准输出
  • 单元测试的独立性

    • 每个单元测试和其他的单元测试都必须是独立的。如果单元测试之间是以特定的顺序指定的,那么这将是致命的
    • 永远不要编写 一个单元测试的输出是另一个单元测试的输入 的单元测试。当离开一个单元测试的时候,不应该改变测试单元的状态,这是后续单元测试执行的先决条件
  • 单元测试环境的独立初始化

    • 在运行所有单元测试时,每个单元测试都必须是应用程序的一个独立的可运行的实例,每个单元测试都必须完全自行设置和初始化其所需的环境,着同样适用于执行单元测试后的清理工作。
  • 不对getters和setter做单元测试

  • 不对第三方代码做单元测试

    • 我们可以预测第三方代码都有自己的单元测试。在你的项目中,不使用那么没有自己的单元测试和质量可疑的库或框架,这是一种明智的架构选择。
  • 不对外部系统做单元测试。

  • 如何处理数据库的访问

    • 能不使用数据库进行单元测试,就不使用数据库进行单元测试。
    • 只在内存中执行所有的单元测试。
    • 单元测试不要访问数据库,磁盘,网络等外设
    • 数据库测试不是单元测试的内容,它是系统集成和系统测试级别的内容
  • 不要混淆测试代码和产品代码

  • 测试必须快速执行

第三章 原则

  • 我建议学生们把更多的精力放在学习基本思想上,而不是新技术上,因为新技术在他们毕业之前就有可能过时,而基本思想则永远不会过时。

  • 一般来说,不仅是软件开发,把生活中的一切事情变得尽可能简单并不一定都是坏事。

  • 也就是说,下面这些原则我们不应该学一次就忘掉,建议熟练掌握它们。这些原则非常重要,理想情况下,它们会成为每个开发人员的第二天性。

Asio asio::ip::tcp::endpoint 是什么

Asio(或 Boost.Asio)中,asio::ip::tcp::endpoint 是一个类,表示一个 TCP 端点。它包含了 IP 地址和端口号,用于描述 TCP 网络通信中的目标或源。

作用

asio::ip::tcp::endpoint 是 TCP 套接字(asio::ip::tcp::socket)操作中的核心组件,用来:

  1. 指定服务器的地址和端口,作为客户端的目标。
  2. 指定本地地址和端口,用于绑定监听服务器或设置客户端绑定信息。
  3. 在连接建立后获取对等端的网络信息(如对端 IP 和端口)。

类定义

asio::ip::tcp::endpointasio::ip::basic_endpoint<tcp> 的具体实现,用于 TCP 协议。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
namespace asio {
namespace ip {
class tcp::endpoint {
public:
// 构造函数
endpoint(); // 默认构造,地址为 unspecified,端口为 0
endpoint(const address& addr, unsigned short port); // 使用地址和端口初始化

// 设置/获取地址
void address(const address& addr);
address address() const;

// 设置/获取端口
void port(unsigned short port);
unsigned short port() const;

// 比较运算符
bool operator==(const endpoint& other) const;
bool operator!=(const endpoint& other) const;

// 其他函数
std::string to_string() const; // 返回可读的字符串表示
};
}
}

常见操作

1. 创建端点

1
2
asio::ip::address addr = asio::ip::make_address("127.0.0.1");
asio::ip::tcp::endpoint endpoint(addr, 8080);

这表示一个 TCP 端点,IP 地址是 127.0.0.1,端口号是 8080

2. 默认构造

1
asio::ip::tcp::endpoint endpoint;

创建一个默认的端点,地址为 0.0.0.0(未指定地址),端口为 0

3. 动态绑定(服务器)

当服务器需要绑定一个本地端口时,可以使用:

1
2
3
4
5
asio::ip::tcp::acceptor acceptor(io_context);
asio::ip::tcp::endpoint endpoint(asio::ip::tcp::v4(), 8080);
acceptor.open(endpoint.protocol());
acceptor.bind(endpoint);
acceptor.listen();

此处,endpoint 表示监听在 IPv4 地址的所有网络接口(0.0.0.0)上的端口 8080

4. 解析客户端或服务器端地址

  • 客户端连接时,解析服务器端点:
    1
    2
    asio::ip::tcp::resolver resolver(io_context);
    auto endpoints = resolver.resolve("example.com", "80");
  • 在服务器端,从已接受的连接中获取对端的 IP 和端口:
    1
    2
    3
    4
    5
    asio::ip::tcp::socket socket(io_context);
    acceptor.accept(socket);
    asio::ip::tcp::endpoint remote_endpoint = socket.remote_endpoint();
    std::cout << "Connected to " << remote_endpoint.address().to_string()
    << ":" << remote_endpoint.port() << std::endl;

5. 转换为字符串

1
2
asio::ip::tcp::endpoint endpoint(asio::ip::make_address("192.168.1.1"), 12345);
std::cout << "Endpoint: " << endpoint.to_string() << std::endl;

输出:

1
Endpoint: 192.168.1.1:12345

重要成员函数

构造函数

  1. 默认构造

    • 创建一个未指定地址和端口的端点(0.0.0.0:0[::]:0)。
      1
      asio::ip::tcp::endpoint();
  2. 地址和端口构造

    • 使用指定的 asio::ip::address 和端口初始化。
      1
      asio::ip::tcp::endpoint(const asio::ip::address& addr, unsigned short port);
  3. 协议构造

    • 使用协议(IPv4/IPv6)和端口初始化,地址为未指定。
      1
      asio::ip::tcp::endpoint(asio::ip::tcp::v4(), 8080);

成员函数

  1. 设置地址

    1
    void address(const asio::ip::address& addr);

    修改端点的 IP 地址。

  2. 获取地址

    1
    asio::ip::address address() const;

    返回端点的 IP 地址。

  3. 设置端口

    1
    void port(unsigned short port);

    修改端点的端口号。

  4. 获取端口

    1
    unsigned short port() const;

    返回端点的端口号。

  5. 转换为字符串

    1
    std::string to_string() const;

    返回端点的字符串表示形式,例如 127.0.0.1:8080


典型用例

客户端

1
2
3
4
5
asio::io_context io_context;
tcp::resolver resolver(io_context);
auto endpoints = resolver.resolve("127.0.0.1", "8080");
tcp::socket socket(io_context);
asio::connect(socket, endpoints);

服务器

1
2
3
4
5
6
7
asio::io_context io_context;
tcp::acceptor acceptor(io_context, tcp::endpoint(tcp::v4(), 8080));
tcp::socket socket(io_context);
acceptor.accept(socket);

tcp::endpoint remote_endpoint = socket.remote_endpoint();
std::cout << "Connected from " << remote_endpoint.to_string() << std::endl;

总结

asio::ip::tcp::endpoint 是 TCP 通信中的一个核心组件,用于描述通信双方的网络位置。它不仅简单易用,而且与其他 Asio 组件(如 tcp::resolvertcp::socket)紧密配合,为构建高效的网络程序提供了强大支持。

C++简介

  • C++融合了三种不同的编程方式:
    • C语言代表的过程性语言
    • C++在C语言基础上添加的类代表的面向对象语言
    • C++模板支持的泛型编程

C++简史

  • 汇编语言,依赖于计算机的内部机器语言。

  • 它是低级语言(low-level),即直接操作硬件,例如直接访问CPU寄存器和内存单元。因此,汇编语言针对于特定的计算机处理器,要将汇编程序移植到另一种计算机上,必须使用不同的汇编语言重新编写程序

  • 高级语言(high-level),致力于解决问题,而不针对特定的硬件。

  • 一种被称为编译器的特殊程序将高级语言翻译成特定计算机的内部语言。这样,就可以通过对每个平台使用不同的编译器来在不同的平台上使用同一个高级语言程序了

  • 一般来说,计算机语言要处理两个概念–数据和算法

    • 数据,是程序使用和处理的信息
    • 算法,是程序使用的方法
  • 结构化编程,将分支(决定接下来应执行哪个指令)限制为一小组行为良好的结构。

  • C语言的词汇表中就包含了这些结构:for循环,while循环,do while循环,if else语句

  • 结构化编程技术反映了过程性编程的思想,根据执行的操作来构思一个程序

  • 面向对象编程(OOP),与强调算法的过程性编程不同的是,OOP强调的是数据。它不像过程性编程那样,试图使问题满足语言的过程性方法,而是试图让语言来满足问题的要求。其理念是设计与问题的本质特性相对应的数据格式。

  • 在C++中,类是一种规范,它描述了这种新型数据格式,对象是根据这种规范构造的特定数据结构。

  • OOP程序设计方法首先设计类,它们准确地表示了程序要处理的东西。类定义描述了对每个类可执行的操作,然后便可以设计一个使用这些类的对象的程序。

  • 从低级组织(如类)到高级组织(如程序)的处理过程,叫做自下向上(bottom-up)的编程

  • OOP编程并不仅仅是将数据和方法合并为类定义。

    • 它还有助于创建可重用的代码,这将减少大量的工作。
    • 信息隐藏可以保护数据
    • 多态能够为运算符和函数创建多个定义,通过编程上下文来确定使用哪个定义
    • 继承能够使用旧类派生出新类
  • OOP引入了很多新的理念,使用的编程方法不同于过程性编程。它不是将重点放在任务上,而是放在表示概念上。

  • 泛型编程(generic programming)是C++支持的另一种编程模式。它与OOP的目标相同,即使重用代码和抽象通用概念的技术更简单。

  • 不过OOP强调的是编程的数据方便,而泛型编程强调的是独立于特定数据类型。它们的侧重点不同。

  • OOP是一个管理大型项目的工具,而泛型编程提供了执行常见任务(例如对数据排序或合并链表)的工具。

  • 术语泛型(generic)指的是,创建独立于类型的代码。

    • C++的数据表示有多种类型–整数,小数,字符,字符串,用户定义的,由多种类型组成的符合结构。
    • 例如,要对不同类型的数据进行排序,通常必需为每种类型创建一个排序函数。
    • 泛型编程需要对语言进行扩展,以便可以只编写一个泛型(即不是特定类型的)函数,并将其用于各种实际类型。
    • C++模板提供了完成这种任务的机制。

第二章 开始学习C++

  • 语句,是要执行的操作。

  • 为理解源代码,编译器需要直到一条语句何时结束,另一条语句何时开始。有些语句使用语句分隔符。

  • C++与C一样,使用终止符(terminator),而不是分隔符。

  • 终止符是一个分号,它是语句的结束标记,是语句的组成部分,而不是语句之间的标记

  • 结论:在C++中,不能省略分号

  • 通常,C++函数可被其他函数激活或调用

  • 函数头描述了函数与调用它的函数之间的接口。

  • 位于函数名前面的部分叫做函数返回类型,它描述的是从函数返回给调用它的函数的信息

  • 函数名后括号中的部分叫做形参列表(argument list)或参数列表(parameter list)。它描述的是从调用函数传递给被调用的函数的信息。

  • C++注释以双斜杠(//)打头,到行尾结束。注释可以位于单独的一行上,也可以和代码位于同一行

  • C-风格注释,包括在符号/**/之间。由于C-风格注释以*/结束,而不是到行尾结束,因此可以跨越多行。事实上,C99标准也在C语言中添加了//注释

  • 源代码中的标记和空白

    • 一行代码中不可分隔的元素叫做标记(token)。
    • 通常,必需用空格,制表符或回车将两个标记分开。空格,制表符和回车统称为空白(white space)。
  • C++源代码风格

    • 每条语句占一行
    • 每个函数都有一个开始花括号和一个结束花括号,这两个花括号各占一行
    • 函数中的语句都相对于花括号进行缩进
    • 与函数名称相关的圆括号周围没有空白。
  • C++程序是一组函数,而每个函数又是一组语句

  • 计算机是一种精确的,有条理的机器。要将信息项存储在计算机中,必须指出信息的存储位置和所需的内存空间。

  • 在C++中,完成这种任务的一种相对简便的方法,是使用声明语句来指出存储类型并提供位置标签。

  • 程序中的声明语句叫做定义声明(defining declaration)语句,简称为定义(definition)。这意味着它将导致编译器为变量分配内存空间。在较为复杂的情况下,还可能有引用声明(reference declaration)

  • 总结

    1. C++程序由一个或多个被称为函数的模块组成。程序从main()函数开始执行,因此该函数必不可少。函数由函数头和函数体组成。函数头指出函数的返回值的类型和函数期望通过参数传递给它的信息的类型。函数体由一系列位于花括号{}中的C++语句组成
    2. 有多种类型的C++语句,包括:
      • 声明语句,定义函数中使用的变量的名称和类型
      • 赋值语句,使用赋值运算符=给变量赋值
      • 消息语句,将消息发送给对象,激发某种行为
      • 函数调用,执行函数。被调用的函数执行完毕后,程序返回到函数调用语句后面的语句
      • 函数原型,声明函数的返回类型,函数接受的参数数量和类型
      • 返回语句,将一个值从被调用的函数那里返回到调用函数中。

第三章 处理数据

  • 计算机内存的基本单元是位(bit)。
  • 可以将位看作电子开关,可以开,也可以关。关表示值0,开表示值1。
  • 8为的内存块可以设置出256中不同的组合,因为每一位都可以有两种设置,所以8位的总组合数为256。
  • 字节(byte),通常指的是8位的内存单元。从这个意义上来说,字节指的就是描述计算机内存量的度量单位,1KB等于1024字节,1MB等于1024KB。

第四章 复合类型

指针与C++基本原理

  • 面向对象编程与传统的过程性编程的区别在于,OOP强调的是在运行阶段(而不是编译阶段)进行决策
    • 运行阶段,指的是程序正在运行时
    • 编译阶段,指的是编译器将程序组合起来时。
  • 运行阶段决策,就好比度假时,选择参观那些景点取决于天气和当时的心情;而编译阶段决策更像不管在什么条件下,都坚持预先设定的日程安排。
  • 运行阶段决策提供了灵活性,可以根据当时的情况进行调整

指针小结

  • 声明指针
  • 给指针赋值。应将内存地址赋给指针。可以对变量名应用&运算符,来获得被命名的内存的地址,new运算符返回未命名的内存的地址
  • 对指针解除引用。
    • 对指针解除引用意味着获得指针指向的值。对指针应用解除引用或间接值运算符(*)来解除引用。
    • 另一种对指针解除引用的方法是使用数组表示法。例如,pn[0]*pn是一样的。一定不要对未被初始化为适当地址的指针解除引用。
  • 区分指针和指针所指向的值
  • 数组名。在多数情况下,C++将数组名视为数组的第一个元素的地址。一种例外情况是,将sizeof运算符用于数组名时,此时将返回整个数组的长度(单位为字节)
  • 指针算术
  • 数组的动态联编和静态联编
    • 使用数组声明来创建数组时,将采用静态联编,即数组的长度在编译时设置
    • 使用new[]运算符创建数组时,将采用动态联编(动态数组),即将在运行时为数组分配空间,其长度也将在运行时设置。使用完这种数组后,应该使用delete[]释放其占用的内存
  • 数组表示法和指针表示法
    • 使用方括号数组表示法等同于对指针解除引用
    • 数组名和指针变量都是如此,因此对于指针和数组名,即可以使用指针表示法,也可以使用数组表示法

自动存储,静态存储和动态存储

  • 自动存储

    • 在函数内部定义的常规变量使用自动存储空间,被称为自动变量(automatic variable),这意味着它们在所属的函数被调用时自动产生,在该函数结束时小王
    • 实际上,自动变量是一个局部变量,其作用域为包含它的代码块。代码块是被包含在花括号中的一段代码。
    • 自动变量通常存储在栈中。这意味着执行代码块时,其中的变量将依次加入到栈中,而在离开代码块时,将按相反的顺序释放这些变量,这被称为后进先出(LIFO)。因此,在程序执行过程中,栈将不断地增大和缩小。
  • 静态存储

    • 静态存储是整个程序执行期间都存在的存储方式。
    • 使变量成为静态的方式有两种:一种是在函数外面定义它,另一种是在声明变量时使用关键字static
  • 动态存储

    • new和delete运算符提供了一种比自动变量和静态变量更灵活的方法。
    • 它们管理了一个内存池,这在C++中被称为自由存储空间(free store)或堆(heap)。
    • 该内存池同用于静态变量和自动变量的内存是分开的。new和delete能够在一个函数中分配内存,而在另一个函数中释放它。因此数据的生命周期不完全受程序或函数的生存时间控制。
  • 栈,堆和内存泄漏

    • 如果使用new运算符在自由存储空间(或堆)上创建变量后,没有调用delete,则即使包含指针的内存由于作用域规则和对象生命周期的原因而被释放,在自由存储空间上动态分配的变量或结构也将继续存在。
    • 实际上,将会无法访问自由存储空间中的结构,因为指向这些内存的指针无效。这将导致内存泄漏。被泄漏的内存将在程序的整个生命周期内都不可使用;这些内存被分配出去,但无法收回。
    • 极端情况(不过不常见)是,内存泄漏可能会非常严重,以致于应用程序可用的内存被耗尽,出现内存耗尽错误,导致程序崩溃。另外,这种泄漏还会给一些操作系统或在相同的内存空间中运行的应用程序带来负面影响,导致它们崩溃。

第八章

  • 内联函数是C++为提供程序运行速度所做的一项改进。常规函数和内联函数之间的主要区别不在于编写方式,而在于C++编译器如何将它们组合到程序中。

  • 编译过程的最终产品是可执行程序–由一组机器语言指令组成。运行程序时,操作系统将这些指令载入到计算机内存中,因此每条指令都有特定的内存地址。计算机随后将逐步执行这些指令。

  • 引用变量

    • 引用是已定义的变量的别名(另一个名称)
    • 引用变量的主要用途是用作函数的形参。通过将引用变量用作参数,函数将使用原始数据,而不是其副本。这样除指针之外,引用也为函数处理大型结构提供了一种非常方便的途径,同时对于设计类来说,引用也是必不可少的
    • int rats; int & rodents = rats; // make rodents an alias for rate
    • 其中,&不是地址运算符,而是类型标识符的一部分。就像声明中的char*指的是指向char的指针一样,int&指的是指向int的引用。
    • 引用经常被用作函数参数,使得函数中的变量名称为调用程序中的变量的别名,这种传递参数的方法称为按引用传递。按引用传递允许被调用的函数能够访问调用函数中的变量

实例化和具体化

  • 为进一步了解模板,必需理解术语:实例化和具体化
  • 谨记:在代码中包含函数模板本身并不会生成函数定义,它只是一个用于生成函数定义的方案。编译器使用模板为特定类型生成函数定义时,得到的是模板实例(instantiation)
    • 例如,函数调用Swap(i, j)导致编译器生成Swap()的一个实例,该实例使用int类型。模板并非函数定义,但是使用int的模板实例是函数定义。**这种实例化方式被称为隐式实例化(implicit instantiation),因为编译器之所以知道需要进行定义,是由于程序调用Swap()函数时提供了int参数。
    • 最初,编译器只能通过隐式实例化,来使用模板生成函数定义,但是现在C++还允许显式实例化(explicit instantiation)。这意味着可以直接命令编译器创建特定的实例,例如Swap<int>()。其语法是,声明所需要的种类–使用<>符号指示类型,并在声明前加上关键字templatetemplate void Swap<int>(int, int); // explicit instantiation

小结

  • 引用变量是一种伪装指针,它允许为变量创建别名(另一个名称)。引用变量主要被用作处理结构和类对象的函数的参数。

  • 通常,被声明为特定类型引用的标识符只能指向这种类型的数据;然而,如果一个类是从另一个类派生出来的,则基类引用可以指向派生类对象

  • 函数的特征标是它的参数列表。程序员可以定义两个同名函数,只要其特征标不同。这被称为函数多态或函数重载。

  • 通常,通过重载函数来为不同的数据类型提供相同的服务

  • 函数模板自动完成重载函数的过程。只需使用泛型和具体算法来定义函数,编译器将为程序中使用的特定参数类型生成正确的函数定义

第九章 内存模型和名称空间

头文件管理

  • 在同一个文件中只能将同一个头文件包含一次。记住这个规则很容易,但很可能在不知情的情况下将头文件包含多次。
    • 例如,可能使用包含了另一个头文件的头文件。有一种标准的C/C++技术可以避免多次包含同一个文件。
  • 它是基于预处理编译指令#ifndef(即if not defined)

自动变量和栈

  • 了解典型的C++编译器如何实现自动变量,有助于更深入地了解自动变量

  • 由于自动变量的数目随函数的开始和结束而增减,因此程序必须在运行时对自动变量进行管理。

  • 常用的方法是留出一段内存,并将其视为栈,以管理变量的增减。之所以被称为栈,是由于新数据被象征性地放在原有数据的上面(也就是说,在相邻的内存单元中,而不是在同一个内存单元中),当程序使用完后,将其从栈中删除。栈的默认长度取决于实现,但编译器通常提供改变栈长度的选项。

  • 程序使用两个指针来跟踪栈,一个指针指向栈底–栈的开始位置,另一个指针指向栈顶–下一个可用内存单元。当函数被调用时,其自动变量将被加入到栈中,栈顶指针指向变量后面的下一个可用的内存单元。函数结束时,栈顶指针被重置为函数被调用前的值,从而释放新变量使用的内存。

  • 栈是LIFO(后进先出)的,即最后加入到栈中的变量首先被弹出。这种设计简化了参数传递。函数调用将其参数的值放在栈顶,然后重新设置栈顶指针。被调用的函数根据其形参描述来确定每个参数的地址。

寄存器变量

  • 关键字register最初是由C语言引入的,它建议编译器使用CPU寄存器来存储自动变量,这样的目的是–提高访问变量的速度。

说明符和限定符

  • 有些被称为存储说明符(storage class specifier)或cv-限定符(cv-qualifier)的C++关键字提供了其他有关存储的信息

  • 存储说明符

    1. auto(在C++11中不再是说明符)
      • 在C++11之前,可以在声明中使用关键字auto指出变量为自动变量
      • 在C++11中,auto用于自动类型推断。
    2. register
      • 用于在声明中指示寄存器存储,
      • 在C++11中,它只是显式地指出变量是自动的
    3. static
      • 它被用在作用域为整个文件的声明中时,表示内部链接性
      • 被用于局部声明中,表示局部变量的存储持续性为静态的
    4. extern
      • 它表明是引用声明,即声明引用在其他地方定义的变量
    5. thread_local(C++11新增加的,可与staticextern结合使用)
      • 它指出变量的持续性与其所属线程的持续性相同
      • thread_local变量之于线程,犹如常规静态变量之于整个程序
    6. mutable
      • 它的含义将根据const来解释
      • 可以用它来指出,即时结构(或类)变量为const,其某个成员也可以被修改
  • cv-限定符(cv表示const volatile)

    1. const
      • 它是最常用的cv-限定符,它表明–内存被初始化后,程序便不能再对它进行修改
    2. volatile
      • 它表明,即时程序代码没有对内存单元进行修改,其值也可能发生变化;该关键字的作用是为了改善编译器的优化能力
      • 例如,假设编译器发现,程序在几条语句中两次使用了某个变量的值,则编译器可能不是让程序查找这个值两次,而是将这个值缓存到寄存器中。这种优化假设变量的值在这两次使用之间不会变化。如果不将变量声明为volatile,则编译器将进行这种优化;将变量声明为volatile,相当于告诉编译器,不要进行这种优化。

小结

  • C++鼓励程序员在开发程序时使用多个文件。一种有效的组织策略是,使用头文件来定义用户类型,为操纵给用户类型的函数提供函数原型;并将函数定义放在一个独立的源代码文件中。头文件和源代码文件一起定义和实现了用户定义的类型及其使用方式。最后,将main()和其他使用这些函数的函数放在第三个文件中

第十章 对象和类

  • 过程性编程方法 – 首先考虑要遵循的步骤,然后考虑如何表示这些数据(并不需要程序一直运行,用户可能希望能够将数据存储在一个文件中,然后从这个文件中读取数据)

  • OOP方法 – 首先从用户的角度考虑对象,描述对象所需的数据以及描述用户与数据交互所需的操作。完成对接口的描述后,需要确定如何实现接口和数据存储。最后,使用新的设计方案创建出程序。

  • 指定基本类型完成了三项工作

    • 决定数据对象需要的内存数量
    • 决定如何解释内存中的位(long和float在内存中占用的位数相同,但将它们转换为数值的方法不同)
    • 决定可使用数据对象执行的操作或方法
  • 类,是一种将抽象转换为用户定义类型的C++工具,它将数据表示和操纵数据的方法组合成一个整洁的包

  • 类规范由两个部分组成

    • 类声明:以数据成员的方式描述数据部分,以成员函数(被称为方法)的方式描述公有接口
    • 类方法定义:描述如何实现类成员函数
    • 简单地说,类声明提供了类的蓝图,而方法定义则提供了细节

什么是接口?

  • 接口,是一个共享框架,供两个系统(例如在计算机和打印机之间或者用户和计算机程序之间)交互时使用

  • 对于类,我们说公共接口。在这里,公共(public)是使用类的程序,交互系统由类对象组成,而接口由编写类的人提供的方法组成。

  • 接口,让程序员能够编写与类对象交互的代码,从而让程序能够使用类对象。

  • 为开发一个类并编写一个使用它的程序,需要完成多个步骤。这里将开发过程分成多个阶段,而不是一次性完成。

  • 通常,C++程序员将接口(类定义)放在头文件中,并将实现(类方法的代码)放在源代码文件中。

访问控制

  • 使用类对象的程序都可以直接访问公有部分,但只能通过公有成员函数来访问对象的私有成员。

  • 因此,公有成员函数是程序和对象的私有成员之间的桥梁,提供了对象和程序之间的接口。防止程序直接访问数据被称为数据隐藏

  • 类设计尽可能将公有接口与实现细节分开。

    • 公有接口表示设计的抽象组件。将实现细节放在一起并将它们与抽象分开被称为封装。
    • 数据隐藏(将数据放在类的私有部分中)是一种封装,将实现的细节隐藏在私有部分中,也是一种封装
    • 封装的另一个例子是,将类函数定义和类声明放在不同的文件中
  • 类和结构

    • 类描述看上去很像是包含成员函数以及public和private可见性标签的结构声明。
    • 实际上,C++对结构进行了扩展,使之具有与类相同的特性。它们之间唯一的区别是,结构的默认访问类型是public,而类为private。C++程序员通常使用类来实现类描述,而把结构限制为只表示纯粹的数据对象(常被称为普通老式数据结构)

实现类成员函数

  • 创建类描述的第二个部分:为那些由类声明中的原型表示的成员函数提供代码。

  • 成员函数定义与常规函数定义非常相似,它们有函数头和函数体,也可以有返回类型和参数。但是它们还有两个特殊的特征:

    • 定义成员函数时,使用作用域解析运算符(::)来表示函数所属的类
    • 类方法可以访问类的private组件
  • 成员函数的函数头使用作用域运算符解析(::)来指出函数所属的类。因此,作用域解析运算符确定了方法定义对应的类的身份。

内联方法

  • 定义位于类声明中的函数都将自动成为内联函数

  • 内联函数的特殊规则要求在每个使用它们的文件中都对其进行定义。确保内联定义对多文件程序中的所有文件都可用的,最简便的方法是:将内联定义放在定义类的头文件中(有些开发系统包含智能链接程序,允许将内联定义放在一个独立的实现文件)

方法使用哪个对象

  • 所创建的每个新对象都有自己的存储空间,用于存储其内部变量和类成员;但同一个类的所有对象共享同一组类方法,即每种方法只有一个副本。

    • 例如,假设kate和joe都是Stock对象,则kate.shares将占据一个内存块,而joe.shares占用另一个内存块,但kate.show()joe.show()都调用同一个方法,也就是说,它们将执行同一个代码块,只是将这些代码用于不同的数据。
  • 在OOP中,调用成员函数被成为发送消息,因此将同样的消息发送给两个不同的对象将调用同一个方法,但是该方法被用于两个不同的对象。

类作用域

  • 在类中定义的名称(如类数据成员名和类成员函数名)的作用域都为整个类,作用域为整个类的名称只在该类中是已知的,在类外是不可知的。

  • 另外,类作用域意味着不能从外部直接访问类的成员,公有成员函数也是如此。也就是说,要调用公有成员函数,必需通过对象

  • 同样,在定义成员函数时,必需使用作用域解析运算符

  • 总之,在类声明或成员函数定义中,可以使用未修饰的成员名称。

  • 构造函数名称在被调用时,才能被识别,因为它的名称与类名相同

  • 其他情况下,使用类成员名时,必需根据上下文使用直接成员运算符(.),间接成员运算符(->)或作用域解析运算符(::)

小结

  • 面向对象编程强调的是程序如何表示数据。使用OOP方法解决编程问题的第一步是根据它与程序之间的接口来描述数据,从而指定如何使用数据。然后,设计一个类来实现该接口。一般来说,私有数据成员存储信息,公有成员函数(又称方法)提供访问数据的唯一途径。类将数据和方法组合成一个单元,其私有性实现数据隐藏

  • 类是用户定义的类型,对象是类的实例。这意味着对象是这种类型的变量。例如由new按类描述分配的内存

第十一章 使用类

  • 学习C++的难点之一是需要记住大量的东西,但在拥有丰富的实践经验之前,根本不可能全部记住这些东西。

  • 掌握知识的好的方法是,在自己开发的C++程序中使用其中的新特性。对这些新特性有了充分的认知后就可以添加其他C++特性

  • 正如C++创始人Bjarne Stroustrup在一次C++专业程序员大会上所建议的:轻松地使用这种语言。不要觉得必须使用所有的特性,不要在第一次学习时就试图使用所有的特性

第十二章 类和动态内存分配

指针和对象小结

  • 使用常规表示法来声明指向对象的指针 – String* gla;

  • 可以将指针初始化为指向已有的对象 – String* first = &saying[0];

  • 可以使用new来初始化指针,这将创建一个新的对象 – String* favorite = new String(sayings[choice])

  • 对类使用new将调用相应的类构造函数来初始化新创建的对象

  • 可以使用->运算符通过指针访问类方法 – shortest->length()

  • 可以对对象指针应用解除引用运算符(*)来获得对象 – first = &saying[i];

第十三章 类继承

  • 希望同一个方法在派生类和基类中的行为是不同的。换句话来说,方法的行为应该取决于调用方法的对象。这种较复杂的行为称为多态–具有多种形态,即同一个方法的行为随上下文而异。

  • 有两种重要的机制可用于实现多态公有继承:

    • 在派生类中重新定义基类的方法
    • 使用虚方法
  • 注意

    • 如果要在派生类中重新定义基类的方法,通常应将基类方法声明为虚的。这样,程序将根据对象类型而不是引用或指针的类型来选择方法版本。
    • 为基类声明一个虚析构函数也是一种惯例

静态联编和动态联编

  • 程序调用函数时,将使用哪一个可执行代码块? 编译器负责回答这个问题

  • 将源代码中的函数调用解释为执行特定的函数代码块被称为函数名联编(binding)

  • 在C语言中,这非常简单,因为每个函数名都对应一个不同的函数

  • 在C++中,由于函数重载的远古,这项任务更复杂。编译器必须查看函数参数及函数名才能确定使用哪一个函数。

  • C/++编译器可以在编译过程完成这种联编。在编译过程中进行联编被称为静态联编(static binding),又称为早期联编(early binding)

  • 然而,虚函数使这项工作变得更困难。因为虚函数使得 – 使用哪一个函数是不能在编译时确定的。因为编译器不知道用户将选择那种类型的对象,所以,编译器必须生成能够在程序运行时选择正确的虚函数的代码,这被称为动态联编(dynamic binding),又称为晚期联编(late binding)

  • 将派生类引用或指针转转为基类引用或指针 被称为向上强制转换(upcasting),这使公有继承不需要进行显式类型转换。

  • 相反的过程 – 将基类指针或引用转换为派生类指针或引用 被称为向下强制转换(downcasting)

  • 编译器对非虚方法使用静态联编

  • 为什么有两种类型的联编以及为什么默认为静态联编? – 效率和概念模型

  • 效率

    • 为了使程序能够在运行阶段进行决策,必须采取一些方法来跟踪基类指针或引用指向的对象类型,这增加了额外的处理开销
    • C++的指导原则之一是 – 不要为不使用的特性付出代价(内存或处理时间)。仅当程序设计确实需要虚函数时,才适用它们
  • 虚函数的工作原理

    1. 通常,编译器处理虚函数的方法是:给每个对象添加一个隐藏成员。隐藏成员中保存了一个指向函数地址数组的指针。这种数组称为虚函数表(virtual function table, vtbl)。
    2. 虚函数表中存储了为类对象进行声明的虚函数的地址。
    • 例如,基类对象包含了一个指针,该指针指向基类中所有虚函数的地址表。派生类对象将包含一个指向独立地址的指针。如果派生类提供了虚函数的新定义,该虚函数表将保存新函数的地址;如果派生类没有重新定义虚函数,该vtbl将保存函数原始版本的地址。如果派生类定义了新的虚函数,则该函数的地址也将被添加到vtbl中
    • 调用虚函数时,程序将查看存储在对象中的vtbl地址,然后转向相应的函数地址表。如果使用类声明中定义的第一个虚函数,则程序将使用数组中的第一个函数地址,并执行具有该地址的函数。如果使用类声明中的第三个虚函数,程序将使用地址为数组中第三个元素的函数。
  • 总之,使用虚函数时,在内存和执行速度方面有一定的成本,包括

    • 每个对象都将增大,增大量为存储地址的空间
    • 对于每个类,编译器都要创建一个虚函数地址表(数组)
    • 对于每个函数调用,都需要执行一项额外的操作,即在表中查找地址
  • 有关虚函数注意事项

    • 在基类方法的声明中使用关键字virtual可以使该方法在基类以及所有的派生类(包括从派生类派生出来的类)中是虚的
    • 如果使用指向对象的引用或指针来调用虚方法,程序将使用为对象类型定义的方法,而不使用为引用或指针类型定义的方法。这称为动态联编或晚期联编。这种行为非常重要。因为这样基类指针或引用可以指向派生类对象
    • 如果定义的类将被用作基类,则应将那些要在派生类中重新定义的类方法声明为虚的。
  • 当基类和派生类都采用动态内存分配时,派生类的析构函数,复制构造函数,赋值运算符都必须使用相应的基类方法来处理基类元素。

  • 这种要求是要通过三种不同的方式来满足的。对于析构函数,这是自动完成的;对于构造函数,这是通过在初始化成员列表中调用基类的复制构造函数来完成的;如果不这样做,将自动调用基类的默认构造函数。对于赋值运算符,这是通过使用作用域解析运算符显式地调用基类的赋值运算符来完成的。

小结

  • 继承,通过使用已有的类(基类)定义新的类(派生类),使得能够根据需要修改编程代码。

C++中的代码重用

  • C++的一个主要目标是促进代码重用。公有继承是实现这种目标的机制之一,但并不是唯一的机制。

  • 方法之一是使用这样的类成员–本身是另一个类的对象。这种方法称为包含(containment), 组合(composition)或层次化(layering)

  • 另一种方法是使用私有或保护继承。通常,包含,私有继承和保护继承用于实现has-a关系,即新的类将包含另一个类的对象。

  • C++和约束

    • C++包含让程序员能够限制程序结构的特性–使用explicit防止单参数构造函数的隐式转换,使用const限制方法修改数据,等等。
    • 这样做的根本原因是:**在编译阶段出现错误优于在运行阶段出现错误
  • 使用包含还是私有继承?

    • 由于即可以使用包含,也可以使用私有继承来建立has-a关系,那么应该使用那种方式?
    • 通常,应该使用包含来建立has-a关系;
    • 如果新类需要访问原有类的保护成员,或者需要重新定义虚函数,则应该使用私有继承

类模板

  • template <class Type>
  • 关键字template告诉编译器,将要定义一个模板。尖括号中的内容相当于函数的参数列表。可以把关键字class看作是变量的类型名,该变量接受类型作为其值,把Type看作是该变量的名称。
  • 这里使用class并不意味着Type必须是一个类;而只是表明Type是一个通用的类型说明符,在使用模板时,将使用实际的类型替换它。较新的C++实现允许在这种情况下使用不太容易混淆的关键字typename代替class
    • template <typename Type> // newer choice
  • 可以使用自己的泛型名代替Type,其命名规则与其他标识符相同。**当前流行的选项包括TType
  • 当模板被调用时,Type将被具体的类型值(例如int, string)取代。在模板定义中,可以使用泛型名来表示要存储在栈中的类型。

小结

  • C++提供了几种重用代码的手段

  • 公有继承能够建立is-a关系,这样派生类可以重用基类的代码

  • 私有继承和保护继承也使得能够重用基类的代码,但是建立的是has-a关系

    • 使用私有继承时,基类的公有成员和保护成员将称为派生类的私有成员
    • 使用保护继承时,基类的公有成员和保护成员将成为派生类的保护成员
  • 无论使用哪一种继承,基类的公有接口都将成为派生类的内部接口,这有时候被称为继承实现,但并不继承接口,因为派生类对象不能显式地使用基类的接口。因此,不能将派生对象看作是一种基类对象。由于这个原因,在不进行显式类型转换的情况下,基类指针或引用将不能指向派生类对象。

  • 还可以通过开发包含对象成员的类来重用类代码,这种方法被称为包含,层次化或组合。

  • 它建立的是has-a关系。与私有继承和保护继承相比,包含更容易实现和使用,所以通常优先采用这种方式。

  • 然而,私有继承和保护继承比包含有一些不同的功能。例如,继承允许派生类访问基类的保护成员;还允许派生类重新定义从基类那里继承的徐函数。因为包含不是继承,所以通过包含来重用类代码时,不能使用这些功能

  • 另一个方面,如果需要使用某个类的几个对象,则用包含更加合适。

  • 所有这些机制的目的都是为了让程序员能够重用经过测试的代码,而不用手工复制它们,这样可以简化编程工作,提供程序的可靠性。

第十五章 友元,异常和其他

  • 在C++中,可以将类声明放在另一个类中。在另一个类中声明的类称为嵌套类(nested class),它通过提供新的类型类作用域来避免名称混乱。
  • 包含的类的成员函数可以创建和使用被嵌套类的对象;而仅当声明位于公有部分,才能在包含类的外面使用嵌套类,而且必须使用作用域解析运算符。

异常

  • 如果其中一个参数是另一个参数的负值,则调用abort()函数。Abort()的函数原型位于头文件cstdlib中,其典型实现是向标准错误流(即cerr使用的错误流)发送消息abnormal program termination(程序异常终止),然后终止程序。它还返回一个随实现而异的值,告诉操作系统(如果程序是由另一个程序调用的,则告诉父进程)处理失败。
  • abort()是否刷新文件缓冲区(用于存储读写到文件中的数据的内存区域)取决于实现。

返回错误码

  • 一种比异常终止更灵活的方法是,使用函数的返回值来指出问题

异常机制

  • C++异常是对程序运行过程中发生的异常情况的一种响应。

  • 异常提供了将控制权从程序的一个部分传递到另一个部分的途径。

  • 对异常的处理有3个组成部分:

    • 引发异常;
    • 使用处理程序捕获异常
    • 使用try块
  • 程序在出现问题时将引发异常。**throw语句实际上是跳转,即命令程序跳到另一条语句**。

  • throw关键字表示引发异常,紧随其后的值(例如字符串或对象)指出了异常的特征。

  • 程序使用异常处理程序(exception handler)来捕获异常,异常处理程序位于要处理问题的程序中。

  • catch关键字表示捕获异常。处理程序以关键字catch开头,随后是位于括号中的类型声明,它指出了异常处理程序要响应的异常类型;然后是一个用花括号括起来的代码块,指出要采取的措施。

  • catch关键字和异常类型用作标签,指出当异常被引发时,程序应该跳到这个位置执行。异常处理程序也被称为catch

  • try块标识其中特定的异常可能被激活的代码块,它后面跟一个或多个catch块。try块是由关键字try指示的,关键字try的后面是一个由花括号括起来的代码块,表明需要注意这些代码引发的异常。

  • 通常,引发异常的函数将传递一个对象。这样做的重要优点之一是,可以使用不同的异常类型来区分不同的函数在不同的情况下引发的异常。

  • 另外,对象可以携带信息,程序员可以根据这些信息来确定引发异常的原因。同时,catch块可以根据这些信息来决定采取什么样的措施

栈解退

  • C++通常是如何处理函数调用和返回的。

  • C++通常通过将信息放在栈中来处理函数调用。具体地说,程序将调用函数的指令的地址(返回地址)放到栈中。当被调用的函数执行完毕后,程序将使用该地址来确定从哪里开始继续执行。另外,函数调用将函数参数放到栈中。在栈中,这些函数参数都被视为自动变量。如果被调用的函数创建了新的自动变量,则这些变量也将被添加到栈中。如果被调用的函数调用了另一个函数,则后者的信息将被添加到栈中,依次类推。

  • 当函数结束时,程序流程将跳到该函数被调用时存储的地址处,同时栈顶的元素被释放。因此,函数通常都返回到调用它的函数,依次类推,同时每个函数都在结束时释放其自动变量。如果自动变量是类对象,则类的析构函数将被调用。

  • 现在假设函数由于出现异常(而不是由于返回)而终止,则程序也将释放栈中的内存,但不会在释放栈的第一个返回地址后停止,而是继续释放栈,直到找到一个位于try块中的返回地址。随后,控制权将转到块尾的异常处理程序,而不是函数调用后面的第一条语句。这个过程被称为栈解退

第十六章 string类和标准模板库

  • STL提供了一组表示容器,迭代器,函数对象和算法的模板。

  • 容器是一个与数组类似的单元,可以存储若干个值。 STL容器是同质的,即存储的值的类型相同

  • 算法是完成特定任务(如对数组进行排序或在链表中查找特定值)的处方

  • 迭代器能够用来遍历容器的对象,与能够遍历数组的指针类似,是广义指针;

  • 函数对象是类似于函数的对象,可以是类对象或函数指针(包含函数名,因为函数名被用作指针)。

  • STL使得能够构造各种容器(包括数组,队列和链表)和执行各种操作(包括搜索,排序和随机排列)

泛型编程

  • STL是一种泛型编程(generic programming)。面向对象编程关注的编程的数据方面,而泛型编程关注的是算法。它们之间的共同点是抽象和创建可重用代码,但它们的理念绝然不同。

  • 泛型编程旨在编写独立于数据类型的代码。在C++中,完成通用程序的工具是模板。当然,模板使得能够按泛型定义函数或类,而STL通过通用算法更进了一步。模板让这一切成为可能,但必须对元素进行仔细地设计。为了解模板和设计是如何协同工作的,来看一看需要迭代器的原因。

  • 理解迭代器是理解STL的关键所在。模板使得算法独立于存储的数据类型,而迭代器使得算法独立于使用的容器类型。因此,它们都是STL通用方法的重要组成部分。

  • 泛型编程旨在使用同一个find函数来处理数组,链表或任何其他容器类型。即函数不仅独立于容器中存储的数据类型,而且独立于容器本身的俄数据结构。模板提供了存储在容器中的数据类型的通用表示,因此还需要遍历容器中的值的通用表示,迭代器正是这样的通用表示。

  • 每个容器类(vector list deque等)定义了相应的迭代器类型。对于其中的某个类,迭代器可能是指针;而对于另一个类,则可能是对象。不管实现方式如何,迭代器都将提供所需的操作。

  • 其次,每个容器类都有一个超尾标记,当迭代器递增到超越容器的最后一个值后,这个值将被赋给迭代器。每个容器类都有begin()end()方法,它们分别返回一个指向容器的第一个元素和超尾位置的迭代器。每个容器都有++操作,让迭代器从指向第一个元素逐步指向超尾位置,从而遍历容器中的每一个元素。

  • 使用容器类时,无需知道其迭代器是如何实现的,也无需知道超尾是如何实现的,而只需要知道它有迭代器,其begin()返回一个指向第一个元素的迭代器,end()返回一个指向超尾位置的迭代器即可。

  • 总结一下STL方法。首先是处理容器的算法,应尽可能通用的术语来表达算法,使之独立于数据类型和容器类型。为使通用算法能够适用于具体情况,应定义能够满足算法需求的迭代器,并把要求加到容器设计上。即基于算法的要求,设计基本迭代器的特征和容器特征。

如何理解迭代??

  • 迭代,是一个重复的过程,每次重复都是基于上一次的结果而继续的,单纯的重复并不是迭代,例如,A + B =》 E ,生成的过程就是迭代,克隆就不是迭代

  • 迭代器,指的是迭代取值的工具。而涉及到把多个值循环取出来的类型有:列表,字符串,元组,字典,集合,打开的文件对象

  • 实现一个简单的迭代取值(基于索引)的方式,只适用于有索引的数据类型:列表,字符串,字典

  • 为了解决基于索引迭代取值的局限性,就必须提供一种能够不依赖索引的取值方式,这就是迭代

  • 在一个迭代器取值干净的情况下,再对其取值,取不到,必须再调用一次迭代器才能取值


  • STL定义了五种迭代器,并根据所需要的迭代器类型对算法进行了描述。这五种迭代器分别是输入迭代器,输出迭代器,正向迭代器,双向迭代器和随机访问迭代器。
    • 输入迭代器 – 被程序用来读取容器中的信息,是单向迭代器,可以递增,但不能倒退
    • 输出迭代器 – 用于将信息从程序传输到容器的迭代器,因此程序的输出就是容器的输入。
    • 正向迭代器 – 每次沿容器向前移动一个元素,并总是按照相同的顺序遍历一系列值
    • 双向迭代器 – 具有正向迭代器的所有特性,同时支持两种(前缀和后缀)递减运算符
    • 随机访问迭代器 – 具有双向迭代器的所有特性,同时添加了支持随机访问的操作和用于对元素进行排序的关系运算符

概念,改进和模型

  • STL有若干个用C++语言无法表达的特性,例如迭代器种类。因此,虽然可以设计具有正向迭代器特征的类,但不能让编译器将算法限制为只使用这个类。

  • 原因在于,正向迭代器是一系列要求,而不是类型。所设计的迭代器类可以满足这种要求,常规指针也能满足这种要求。

  • STL算法可以使用任何满足其要求的迭代器实现。STL文献使用术语概念(concept)来描述一系列的要求。因此,存在输入迭代器概念,正向迭代器概念等。

  • 概念,可以具有类似继承的关系。例如,双向迭代器继承了正向迭代器的功能。然而,不能将C++继承机制用于迭代器。例如,可以将正向迭代器实现为一个类,而将双向迭代器实现为一个常规指针。

  • 因此,对C++而言,这种双向迭代器是一种内置类型,不能从类派生而来。然而,从概念上看,它确实能够继承。有些STL文献使用术语**改进(refinement)**来表示这种概念上的继承,因此,双向迭代器是对正向迭代器概念的一种改进。

  • **概念的具体实现被称为模型(model)**。因此,指向int的常规指针是一个随机访问迭代器模型,也是一个正向迭代器模型,因为它满足该概念的所有要求。


关联容器

  • 关联容器(associative container)是对容器概念的另一个改进。关联容器将值与键关联在一起,并使用键来查找值。

  • 关联容器的优点在于,它提供了对元素的快速访问。与序列相似,关联容器也允许插入新元素,但不能指定元素的插入位置。原因是关联容器通常有用于确定数据放置位置的算法,以便能够快速检索信息。

  • 关联容器通常是使用某种树实现的。

  • STL提供了四种关联容器:set, multiset, map, multimap。前两种是在头文件set中定义的,后两种是在头文件map中定义的


函数对象

  • 很多STL算法都是用函数对象 – 也叫函数符(functor)。函数符是可以以函数方式与()结合使用的任意对象。这包括函数名,指向函数的指针和重载了()运算符的类对象(即定义了函数operator()的类)

总结

  • C++提供了一组功能强大的库,这些库提供了很多常见编程问题的解决方案以及简化其他问题的工具。string类为将字符串作为对象来处理提供了一种方便的方法。string类提供了自动内存管理功能以及众多处理字符串的方法和函数

  • STL是一个容器类模板,迭代器类模板,函数对象模板和算法函数模板的集合,它们的设计是一致的,都是基于泛型编程原则的。算法通过使用模板,从而独立于所存储的对象的类型:通过使用迭代器接口,从而独立于容器的类型。迭代器是广义指针

  • STL使用术语概念来描述一组要求

  • 有些算法被表示为容器类方法,但大量算法都被表示为通用的,非成员函数,这是通过将迭代器作为容器和算法之间的接口得以实现的

  • 容器和算法都是由其提供或需要的迭代器类型表征的。

第十七章 输入, 输出和文件

  • 多数计算机语言的输入和输出是以语言本身为基础实现的。但是C和C++都没有将输入和输出建立在语言中。这两种语言的关键字包括for和if,但不包括与I/O有关的内容。C语言最初把I/O留给了编译器实现人员。这样做的一个原因是为了让实现人员能够自由的设计I/O函数,使之最适合于目标计算机的硬件要求。实际上,多数实现人员都把I/O建立在最初为UNIX环境开发的库函数的基础之上。ANSI C 正式承认这个I/O软件包时,将其称为标准输入/输出包,并将其作为标准C库不可或缺的组成部分。C++也认可这个软件包,因此如果熟悉stdio.h文件中声明的C函数系列,则可以在C++程序中使用它们,较新的实现使用头文件cstdio来支持这些函数。

  • C++依赖于C++的I/O解决方案,而不是C语言的I/O解决方案,前者是在头文件iostream中定义一组类

流和缓冲区

  • C++程序把输入和输出看作字节流。输入时,程序从输入流中抽取字节;输出时,程序将字节插入到输出流中。对于面向文本的程序,每个字节代表一个字符,更通俗地说,字节可以构成字符或数值数据的二进制表示。

  • 输入流中的字节可能来自键盘,也可能来自存储设备(例如硬盘)或其他程序。同样,输出流中的字节可以流向屏幕,打印机,存储设备或者其他程序。流充当了程序和流源或流目标之间的桥梁

  • 这使得C++程序可以以相同的方式对待来自键盘的输入和来自文件的输入。C++程序只是检查字节流,而不需要知道字节来自何方。同理,通过使用流,C++程序处理输出的方式将独立于其去向。因此管理输入包含两部:

    • 将流和输入去向的程序关联起来
    • 将流和文件连接起来
  • 换句话说,输入流需要两个连接,每端各一个。文件端部连接提供了流的来源,程序端连接将流的流出部分转储到程序中(文件端连接可以是文件,也可以是设备)。同样,对输出的管理包括将输出流连接到程序以及将输出目标与流关联起来。

  • 通常,通过使用缓冲区可以更高效地处理输入和输出。

  • 缓冲区,是用作中介的内存块,它是将信息从设备传输到程序或从程序传输给设备的临时存储工具

  • 缓冲区帮忙匹配两种不同的信息传输速率。

  • 输出时,程序首先填满缓冲区,然后把整块数据传输给硬盘,并清空缓冲区,以备下一批输出使用,这被称为刷新缓冲区(flushing the buffer)

文件输入和输出

  • 大多数计算机程序都使用了文件。字处理程序创建文档文件;数据库程序创建和搜索信息文件;编译器读取源代码文件并生成可执行文件。
  • 文件本身是存储在某种设备(磁盘,光盘,软盘或硬盘)上的一系列字节。通常,操作系统管理文件,跟踪它们的位置,大小,创建时间等。

小结

  • 流,是进出程序的字节流。
  • 缓冲区是内存中的临时存储区域,是程序与文件或其他I/O设备之间的桥梁。
  • 信息在缓冲区和文件之间传输时,将使用设备(例如磁盘驱动器)处理效率最高的尺寸以大块数据的方式进行传输。
  • 信息在缓冲区和程序之间传输时,是逐字节传输的。这种方式对于程序中的处理操作更为方便。
  • C++通过将一个被缓冲流同程序及其输入源相连接来处理输入。同样,C++也通过将一个被缓冲流和程序及其输出目标相连来处理输出。
  • iostreamfstream文件构成了I/O类库,该类库定义了大量用于管理流的类。

探讨C++新标准

  • 如果仔细阅读了本书,则应很好地掌握了C++的规则,然而,这仅仅是学习这种语言的开始,接下来需要学习如何高效地使用该语言,这样的路更长。更好的情况是,工作或学习环境能够接触到优秀的C++代码和程序员。

  • 另外,了解C++后,便可以阅读一些介绍高级主体和面向对象编程的书记,附录H列出了一些这样的资源。

  • OOP有助于开发大型项目,并提高其可靠性。OOP方法的基本活动之一是发明能够表示正在模拟的情况(被称为问题域(problem domain))的类。

  • 由于实际问题通常很复杂,因此找到适当的类富有挑战性。创建复杂的系统时,从空白开始通常不可行,最好采用逐步迭代的方式。为此,该领域的实践者开发了多种技术和策略。具体地说,重要的是在分析和设计阶段完成尽可能多的迭代工作,而不要不断地修改实际代码

  • 除了加深对C++的总体理解外,还可能需要学习特定的类库

附录H

  • C++常见问题解答,第二版 – Cline, Marshall, Greg Lomow and Mike Girou. C++FAQ, Second Edition

  • C++标准库教程和参考手册 – Josuttis, Nicolai M. The C++ Standard Library:A Tutorial and Reference

  • Meyers, Scott. Effective C++:55 Specific Ways to Improve Your Programs and Designs, Third Edition.

    • 本书针对的是了解C++的程序员,提供了55条规定和指南。其中一些是技术性的,例如解释何时应该定义复制构造函数和赋值运算符;其他一些更为通用,例如对is-a has-a关系的讨论
  • Stroustrup,Bjarne. The C++ Programming Language. Third Edition.

  • http://webstore.ansi.org

  • www.iso.org

  • http://www.parashift.com/C++-faq-lite

简介

引言

  • C++ 是一个用户群体相当大的语言。从 C++98 的出现到 C++11 的正式定稿经历了长达十年多之久的积累。

  • C++14/17 则是作为对 C++11 的重要补充和优化,C++20 则将这门语言领进了现代化的大门,所有这些新标准中扩充的特性,给 C++ 这门语言注入了新的活力。

  • 那些还在坚持使用传统 C++ (本书把 C++98 及其之前的 C++ 特性均称之为传统 C++)而未接触过现代 C++ 的 C++ 程序员在见到诸如 Lambda 表达式这类全新特性时,甚至会流露出『学的不是同一门语言』的惊叹之情

  • 现代 C++ (本书中均指 C++11/14/17/20) 为传统 C++ 注入的大量特性使得整个 C++ 变得更加像一门现代化的语言。现代 C++ 不仅仅增强了 C++ 语言自身的可用性,auto 关键字语义的修改使得我们更加有信心来操控极度复杂的模板类型。同时还对语言运行期进行了大量的强化,Lambda 表达式的出现让 C++ 具有了『匿名函数』的『闭包』特性,而这一特性几乎在现代的编程语言(诸如 Python/Swift/… )中已经司空见惯,右值引用的出现解决了 C++ 长期以来被人诟病的临时对象效率问题等等

  • C++17 则是近三年依赖 C++ 社区一致推进的方向,也指出了 现代C++ 编程的一个重要发展方向。尽管它的出现并不如 C++11 的分量之重,但它包含了大量小而美的语言与特性(例如结构化绑定),这些特性的出现再一次修正了我们在 C++ 中的编程范式。

  • 现代 C++ 还为自身的标准库增加了非常多的工具和方法,诸如在语言自身标准的层面上制定了 std::thread,从而支持了并发编程,在不同平台上不再依赖于系统底层的 API,实现了语言层面的跨平台支持;std::regex 提供了完整的正则表达式支持等等。C++98 已经被实践证明了是一种非常成功的『范型』,而现代 C++ 的出现,则进一步推动这种范型,让 C++ 成为系统程序设计和库开发更好的语言。Concept 提供了对模板参数编译期的检查,进一步增强了语言整体的可用性。

第一章 迈向现代C++

1.1 被弃用的特性

  • 注意:弃用并非彻底不能用,只是用于暗示程序员这些特性将从未来的标准中消失,应该尽量避免使用。但是,已弃用的特性依然是标准库的一部分,并且出于兼容性的考虑,大部分特性其实会『永久』保留。
  • 在学习现代 C++ 之前,我们先了解一下从 C++11 开始,被弃用的主要特性:
    • 不再允许字符串字面值常量赋值给一个 char *。如果需要用字符串字面值常量赋值和初始化一个 char *,应该使用 const char * 或者 auto。
      • char *str = "hello world!"; // 将出现弃用警告
    • C++98 异常说明、 unexpected_handler、set_unexpected() 等相关特性被弃用,应该使用 noexcept。
    • auto_ptr 被弃用,应使用 unique_ptr。
    • register 关键字被弃用,可以使用但不再具备任何实际含义。
    • bool 类型的 ++ 操作被弃用
    • 如果一个类有析构函数,为其生成拷贝构造函数和拷贝赋值运算符的特性被弃用了。
    • C 语言风格的类型转换被弃用(即在变量前使用 (convert_type)),应该使用 static_cast、reinterpret_cast、const_cast 来进行类型转换。
    • 特别地,在最新的 C++17 标准中弃用了一些可以使用的 C 标准库,例如 <ccomplex><cstdalign><cstdbool><ctgmath>
    • ……等等
    • 还有一些其他诸如参数绑定(C++11 提供了 std::bind 和 std::function)、export 等特性也均被弃用

1.2 与C的兼容性

  • 出于一些不可抗力、历史原因,我们不得不在 C++ 中使用一些 C 语言代码(甚至古老的 C 语言代码),例如 Linux 系统调用。在现代 C++ 出现之前,大部分人当谈及『C 与 C++ 的区别是什么』时,普遍除了回答面向对象的类特性、泛型编程的模板特性外,就没有其他的看法了,甚至直接回答『差不多』,也是大有人在
  • 从现在开始,你的脑子里应该树立『C++ 不是 C 的一个超集』这个观念(而且从一开始就不是,后面的进一步阅读的参考文献中给出了 C++98 和 C99 之间的区别)。
  • 在编写 C++ 时,也应该尽可能的避免使用诸如 void* 之类的程序风格。而在不得不使用 C 时,应该注意使用 extern "C" 这种特性,将 C 语言的代码与 C++代码进行分离编译,再统一链接这种做法

第二章 语言可用性的强化

  • 当我们声明、定义一个变量或者常量,对代码进行流程控制、面向对象的功能、模板编程等这些都是运行时之前,可能发生在编写代码或编译器编译代码时的行为。
  • 为此,我们通常谈及语言可用性,是指那些发生在运行时之前的语言行为。

2.1 常量

nullptr

  • nullptr 出现的目的是为了替代 NULL。在某种意义上来说,传统 C++ 会把 NULL、0 视为同一种东西,这取决于编译器如何定义 NULL,有些编译器会将 NULL 定义为 ((void*)0),有些则会直接将其定义为 0。
  • C++ 不允许直接将 void * 隐式转换到其他类型。但如果编译器尝试把 NULL 定义为 ((void*)0),那么在下面这句代码中:char *ch = NULL;
  • 没有了 void * 隐式转换的 C++ 只好将 NULL 定义为 0。而这依然会产生新的问题,将 NULL 定义成 0 将导致 C++ 中重载特性发生混乱。考虑下面这两个 foo 函数:
    • void foo(char *);
    • void foo(int);
  • 那么 foo(NULL); 这个语句将会去调用 foo(int),从而导致代码违反直觉。
  • 为了解决这个问题,C++11 引入了 nullptr 关键字,专门用来区分空指针、0。而 nullptr 的类型为 nullptr_t,能够隐式的转换为任何指针或成员指针的类型,也能和他们进行相等或者不等的比较

constexpr

  • C++ 本身已经具备了常量表达式的概念,比如 1+2, 3*4 这种表达式总是会产生相同的结果并且没有任何副作用。如果编译器能够在编译时就把这些表达式直接优化并植入到程序运行时,将能增加程序的性能。一个非常明显的例子就是在数组的定义阶段:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    #include <iostream>
    #define LEN 10

    int len_foo() {
    int i = 2;
    return i;
    }
    constexpr int len_foo_constexpr() {
    return 5;
    }

    constexpr int fibonacci(const int n) {
    return n == 1 || n == 2 ? 1 : fibonacci(n-1)+fibonacci(n-2);
    }

    int main() {
    char arr_1[10]; // 合法
    char arr_2[LEN]; // 合法

    int len = 10;
    // char arr_3[len]; // 非法

    const int len_2 = len + 1;
    constexpr int len_2_constexpr = 1 + 2 + 3;
    // char arr_4[len_2]; // 非法
    char arr_4[len_2_constexpr]; // 合法

    // char arr_5[len_foo()+5]; // 非法
    char arr_6[len_foo_constexpr() + 1]; // 合法

    std::cout << fibonacci(10) << std::endl;
    // 1, 1, 2, 3, 5, 8, 13, 21, 34, 55
    std::cout << fibonacci(10) << std::endl;
    return 0;
    }
  • 上面的例子中,char arr_4[len_2] 可能比较令人困惑,因为 len_2 已经被定义为了常量。为什么 char arr_4[len_2] 仍然是非法的呢?这是因为 C++ 标准中数组的长度必须是一个常量表达式,而对于 len_2 而言,这是一个 const 常数,而不是一个常量表达式,因此(即便这种行为在大部分编译器中都支持,但是)它是一个非法的行为,我们需要使用接下来即将介绍的 C++11 引入的 constexpr 特性来解决这个问题;而对于 arr_5 来说,C++98 之前的编译器无法得知 len_foo() 在运行期实际上是返回一个常数,这也就导致了非法的产生

  • 注意,现在大部分编译器其实都带有自身编译优化,很多非法行为在编译器优化的加持下会变得合法,若需重现编译报错的现象需要使用老版本的编译器。

  • C++11 提供了 constexpr 让用户显式的声明函数或对象构造函数在编译期会成为常量表达式,这个关键字明确的告诉编译器应该去验证 len_foo 在编译期就应该是一个常量表达式。

  • 此外,constexpr 修饰的函数可以使用递归:

    1
    2
    3
    constexpr int fibonacci(const int n) {
    return n == 1 || n == 2 ? 1 : fibonacci(n-1)+fibonacci(n-2);
    }
  • 从 C++14 开始,constexpr 函数可以在内部使用局部变量、循环和分支等简单语句,例如下面的代码在 C++11 的标准下是不能够通过编译的:

    1
    2
    3
    4
    5
    constexpr int fibonacci(const int n) {
    if(n == 1) return 1;
    if(n == 2) return 1;
    return fibonacci(n-1) + fibonacci(n-2);
    }

2.2 变量及其初始化

if/switch 变量声明强化

  • 在传统 C++ 中,变量的声明虽然能够位于任何位置,甚至于 for 语句内能够声明一个临时变量 int,但始终没有办法在 if 和 switch 语句中声明一个临时的变量。例如:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    #include <iostream>
    #include <vector>
    #include <algorithm>

    int main() {
    std::vector<int> vec = {1, 2, 3, 4};

    // 在 c++17 之前
    const std::vector<int>::iterator itr = std::find(vec.begin(), vec.end(), 2);
    if (itr != vec.end()) {
    *itr = 3;
    }

    // 需要重新定义一个新的变量
    const std::vector<int>::iterator itr2 = std::find(vec.begin(), vec.end(), 3);
    if (itr2 != vec.end()) {
    *itr2 = 4;
    }

    // 将输出 1, 4, 3, 4
    for (std::vector<int>::iterator element = vec.begin(); element != vec.end();
    ++element)
    std::cout << *element << std::endl;
    }
  • 在上面的代码中,我们可以看到 itr 这一变量是定义在整个 main() 的作用域内的,这导致当我们需要再次遍历整个 std::vector 时,需要重新命名另一个变量。C++17 消除了这一限制,使得我们可以在 if(或 switch)中完成这一操作:
    1
    2
    3
    4
    5
    // 将临时变量放到 if 语句内
    if (const std::vector<int>::iterator itr = std::find(vec.begin(), vec.end(), 3);
    itr != vec.end()) {
    *itr = 4;
    }

初始化列表

  • 初始化是一个非常重要的语言特性,最常见的就是在对象进行初始化时进行使用。

  • 在传统 C++ 中,不同的对象有着不同的初始化方法,例如普通数组、 POD (Plain Old Data,即没有构造、析构和虚函数的类或结构体) 类型都可以使用 {} 进行初始化,也就是我们所说的初始化列表。 而对于类对象的初始化,要么需要通过拷贝构造、要么就需要使用 () 进行。 这些不同方法都针对各自对象,不能通用。

  • 例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    #include <iostream>
    #include <vector>

    class Foo {
    public:
    int value_a;
    int value_b;
    Foo(int a, int b) : value_a(a), value_b(b) {}
    };

    int main() {
    // before C++11
    int arr[3] = {1, 2, 3};
    Foo foo(1, 2);
    std::vector<int> vec = {1, 2, 3, 4, 5};

    std::cout << "arr[0]: " << arr[0] << std::endl;
    std::cout << "foo:" << foo.value_a << ", " << foo.value_b << std::endl;
    for (std::vector<int>::iterator it = vec.begin(); it != vec.end(); ++it) {
    std::cout << *it << std::endl;
    }
    return 0;
    }
  • 为解决这个问题,C++11 首先把初始化列表的概念绑定到类型上,称其为 std::initializer_list,允许构造函数或其他函数像参数一样使用初始化列表,这就为类对象的初始化与普通数组和 POD 的初始化方法提供了统一的桥梁,例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    #include <initializer_list>
    #include <vector>
    #include <iostream>

    class MagicFoo {
    public:
    std::vector<int> vec;
    MagicFoo(std::initializer_list<int> list) {
    for (std::initializer_list<int>::iterator it = list.begin();
    it != list.end(); ++it)
    vec.push_back(*it);
    }
    };
    int main() {
    // after C++11
    MagicFoo magicFoo = {1, 2, 3, 4, 5};

    std::cout << "magicFoo: ";
    for (std::vector<int>::iterator it = magicFoo.vec.begin();
    it != magicFoo.vec.end(); ++it)
    std::cout << *it << std::endl;
    }
  • 这种构造函数被叫做初始化列表构造函数,具有这种构造函数的类型将在初始化时被特殊关照

  • 初始化列表除了用在对象构造上,还能将其作为普通函数的形参,例如:

    1
    2
    3
    4
    5
    6
    7
    public:
    void foo(std::initializer_list<int> list) {
    for (std::initializer_list<int>::iterator it = list.begin();
    it != list.end(); ++it) vec.push_back(*it);
    }

    magicFoo.foo({6,7,8,9});
  • 其次,C++11 还提供了统一的语法来初始化任意的对象,例如:Foo foo2 {3, 4};

结构化绑定

  • 结构化绑定提供了类似其他语言中提供的多返回值的功能。在容器一章中,我们会学到 C++11 新增了 std::tuple 容器用于构造一个元组,进而囊括多个返回值。但缺陷是,C++11/14 并没有提供一种简单的方法直接从元组中拿到并定义元组中的元素,尽管我们可以使用 std::tie 对元组进行拆包,但我们依然必须非常清楚这个元组包含多少个对象,各个对象是什么类型,非常麻烦
  • C++17 完善了这一设定,给出的结构化绑定可以让我们写出这样的代码:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    #include <iostream>
    #include <tuple>

    std::tuple<int, double, std::string> f() {
    return std::make_tuple(1, 2.3, "456");
    }

    int main() {
    auto [x, y, z] = f();
    std::cout << x << ", " << y << ", " << z << std::endl;
    return 0;
    }

2.3 类型推导

  • 在传统 C 和 C++ 中,参数的类型都必须明确定义,这其实对我们快速进行编码没有任何帮助,尤其是当我们面对一大堆复杂的模板类型时,必须明确的指出变量的类型才能进行后续的编码,这不仅拖慢我们的开发效率,也让代码变得又臭又长
  • C++11 引入了 auto 和 decltype 这两个关键字实现了类型推导,让编译器来操心变量的类型。这使得 C++ 也具有了和其他现代编程语言一样,某种意义上提供了无需操心变量类型的使用习惯

auto

  • auto 在很早以前就已经进入了 C++,但是他始终作为一个存储类型的指示符存在,与 register 并存。在传统 C++ 中,如果一个变量没有声明为 register 变量,将自动被视为一个 auto 变量。而随着 register 被弃用(在 C++17 中作为保留关键字,以后使用,目前不具备实际意义),对 auto 的语义变更也就非常自然了

  • 使用 auto 进行类型推导的一个最为常见而且显著的例子就是迭代器。你应该在前面的小节里看到了传统 C++ 中冗长的迭代写法:

    1
    2
    3
    4
    // 在 C++11 之前
    // 由于 cbegin() 将返回 vector<int>::const_iterator
    // 所以 it 也应该是 vector<int>::const_iterator 类型
    for(vector<int>::const_iterator it = vec.cbegin(); it != vec.cend(); ++it)
  • 而有了 auto 之后可以:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    #include <initializer_list>
    #include <vector>
    #include <iostream>

    class MagicFoo {
    public:
    std::vector<int> vec;
    MagicFoo(std::initializer_list<int> list) {
    // 从 C++11 起, 使用 auto 关键字进行类型推导
    for (auto it = list.begin(); it != list.end(); ++it) {
    vec.push_back(*it);
    }
    }
    };
    int main() {
    MagicFoo magicFoo = {1, 2, 3, 4, 5};
    std::cout << "magicFoo: ";
    for (auto it = magicFoo.vec.begin(); it != magicFoo.vec.end(); ++it) {
    std::cout << *it << ", ";
    }
    std::cout << std::endl;
    return 0;
    }
  • 一些其他的常见用法:

    • auto i = 5; // i 被推导为 int
    • auto arr = new auto(10); // arr 被推导为 int *
  • 从 C++ 20 起,auto 甚至能用于函数传参,考虑下面的例子:

    1
    2
    3
    4
    5
    6
    7
    int add(auto x, auto y) {
    return x+y;
    }

    auto i = 5; // 被推导为 int
    auto j = 6; // 被推导为 int
    std::cout << add(i, j) << std::endl;
  • 注意:auto 还不能用于推导数组类型:

    1
    2
    3
    4
    auto auto_arr2[10] = {arr}; // 错误, 无法推导数组元素类型

    2.6.auto.cpp:30:19: error: 'auto_arr2' declared as array of 'auto'
    auto auto_arr2[10] = {arr};

decltype

  • decltype 关键字是为了解决 auto 关键字只能对变量进行类型推导的缺陷而出现的。它的用法和 typeof 很相似:decltype(表达式)
  • 有时候,我们可能需要计算某个表达式的类型,例如:
    1
    2
    3
    auto x = 1;
    auto y = 2;
    decltype(x+y) z;
  • 你已经在前面的例子中看到 decltype 用于推断类型的用法,下面这个例子就是判断上面的变量 x, y, z 是否是同一类型:
    1
    2
    3
    4
    5
    6
    if (std::is_same<decltype(x), int>::value)
    std::cout << "type x == int" << std::endl;
    if (std::is_same<decltype(x), float>::value)
    std::cout << "type x == float" << std::endl;
    if (std::is_same<decltype(x), decltype(z)>::value)
    std::cout << "type z == type x" << std::endl;
  • 其中,std::is_same<T, U> 用于判断 T 和 U 这两个类型是否相等。输出结果为:
    • type x == int
    • type z == type x

尾返回类型推导

  • 你可能会思考,在介绍 auto 时,我们已经提过 auto 不能用于函数形参进行类型推导,那么 auto 能不能用于推导函数的返回类型呢?还是考虑一个加法函数的例子,在传统 C++ 中我们必须这么写:

    1
    2
    3
    4
    template<typename R, typename T, typename U>
    R add(T x, U y) {
    return x+y;
    }
  • 注意:typename 和 class 在模板参数列表中没有区别,在 typename 这个关键字出现之前,都是使用 class 来定义模板参数的。但在模板中定义有嵌套依赖类型的变量时,需要用 typename 消除歧义

  • 这样的代码其实变得很丑陋,因为程序员在使用这个模板函数的时候,必须明确指出返回类型。但事实上我们并不知道 add() 这个函数会做什么样的操作,以及获得一个什么样的返回类型

  • 在 C++11 中这个问题得到解决。虽然你可能马上会反应出来使用 decltype 推导 x+y 的类型,写出这样的代码:decltype(x + y) add( T x, U y)

  • 但事实上这样的写法并不能通过编译。这是因为在编译器读到 decltype(x+y) 时,x 和 y 尚未被定义。为了解决这个问题,C++11 还引入了一个叫做尾返回类型(trailing return type),利用 auto 关键字将返回类型后置:

    1
    2
    3
    4
    template<typename T, typename U>
    auto add2(T x, U y) -> decltype(x+y){
    return x + y;
    }
  • 令人欣慰的是从 C++14 开始是可以直接让普通函数具备返回值推导,因此下面的写法变得合法:

    1
    2
    3
    4
    template<typename T, typename U>
    auto add3(T x, U y){
    return x + y;
    }
  • 可以检查一下类型推导是否正确:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // after c++11
    auto w = add2<int, double>(1, 2.0);
    if (std::is_same<decltype(w), double>::value) {
    std::cout << "w is double: ";
    }
    std::cout << w << std::endl;

    // after c++14
    auto q = add3<double, int>(1.0, 2);
    std::cout << "q: " << q << std::endl;

decltype(auto)

  • decltype(auto) 是 C++14 开始提供的一个略微复杂的用法。要理解它你需要知道 C++ 中参数转发的概念,我们会在语言运行时强化一章中详细介绍,你可以到时再回来看这一小节的内容。

  • 简单来说,decltype(auto) 主要用于对转发函数或封装的返回类型进行推导,它使我们无需显式的指定 decltype 的参数表达式。考虑看下面的例子,当我们需要对下面两个函数进行封装时:

    • std::string lookup1();
    • std::string& lookup2();
  • 在 C++11 中,封装实现是如下形式:

    1
    2
    3
    4
    5
    6
    std::string look_up_a_string_1() {
    return lookup1();
    }
    std::string& look_up_a_string_2() {
    return lookup2();
    }
  • 而有了 decltype(auto),我们可以让编译器完成这一件烦人的参数转发:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
        decltype(auto) look_up_a_string_1() {
    return lookup1();
    }
    decltype(auto) look_up_a_string_2() {
    return lookup2();
    }
    ```

    ### 2.4 控制流

    #### if constexpr

    + 正如本章开头出,我们知道了 C++11 引入了 constexpr 关键字,它将表达式或函数编译为常量结果。一个很自然的想法是,如果我们把这一特性引入到条件判断中去,让代码在编译时就完成分支判断,岂不是能让程序效率更高?C++17 将 constexpr 这个关键字引入到 if 语句中,允许在代码中声明常量表达式的判断条件,考虑下面的代码:

    #include

    template
    auto print_type_info(const T& t) {
    if constexpr (std::is_integral::value) {
    return t + 1;
    } else {
    return t + 0.001;
    }
    }
    int main() {
    std::cout << print_type_info(5) << std::endl;
    std::cout << print_type_info(3.14) << std::endl;
    }

    1
    + 在编译时,实际代码就会表现为如下:

    int print_type_info(const int& t) {
    return t + 1;
    }
    double print_type_info(const double& t) {
    return t + 0.001;
    }
    int main() {
    std::cout << print_type_info(5) << std::endl;
    std::cout << print_type_info(3.14) << std::endl;
    }

    1
    2
    3
    4

    #### 区间for迭代

    + 终于,C++11 引入了基于范围的迭代写法,我们拥有了能够写出像 Python 一样简洁的循环语句,我们可以进一步简化前面的例子:

    #include
    #include
    #include

    int main() {
    std::vector vec = {1, 2, 3, 4};
    if (auto itr = std::find(vec.begin(), vec.end(), 3); itr != vec.end()) *itr = 4;
    for (auto element : vec)
    std::cout << element << std::endl; // read only
    for (auto &element : vec) {
    element += 1; // writeable
    }
    for (auto element : vec)
    std::cout << element << std::endl; // read only
    }

    1
    2
    3
    4
    5
    6
    7
    8
    9

    ### 2.5 模板

    + C++ 的模板一直是这门语言的一种特殊的艺术,模板甚至可以独立作为一门新的语言来进行使用。模板的哲学在于将一切能够在编译期处理的问题丢到编译期进行处理,仅在运行时处理那些最核心的动态服务,进而大幅优化运行期的性能。因此模板也被很多人视作 C++ 的黑魔法之一。

    #### 外部模板

    + 传统 C++ 中,模板只有在使用时才会被编译器实例化。换句话说,只要在每个编译单元(文件)中编译的代码中遇到了被完整定义的模板,都会实例化。这就产生了重复实例化而导致的编译时间的增加。并且,我们没有办法通知编译器不要触发模板的实例化。
    + 为此,C++11 引入了外部模板,扩充了原来的强制编译器在特定位置实例化模板的语法,使我们能够显式的通知编译器何时进行模板的实例化:

    template class std::vector; // 强行实例化
    extern template class std::vector; // 不在该当前编译文件中实例化模板

    1
    2
    3
    4
    5

    #### 尖括号`">"`

    + 在传统 C++ 的编译器中,>>一律被当做右移运算符来进行处理。但实际上我们很容易就写出了嵌套模板的代码:`std::vector<std::vector<int>> matrix;`
    + 这在传统 C++ 编译器下是不能够被编译的,而 C++11 开始,连续的右尖括号将变得合法,并且能够顺利通过编译。甚至于像下面这种写法都能够通过编译:

    template
    class MagicType {
    bool magic = T;
    };

    // in main function:
    std::vector<MagicType<(1>2)>> magic; // 合法, 但不建议写出这样的代码

    1
    2
    3
    4

    #### 类型别名模板

    + 在了解类型别名模板之前,需要理解『模板』和『类型』之间的不同。仔细体会这句话:模板是用来产生类型的。在传统 C++ 中,typedef 可以为类型定义一个新的名称,但是却没有办法为模板定义一个新的名称。因为,模板不是类型。例如:

    template<typename T, typename U>
    class MagicType {
    public:
    T dark;
    U magic;
    };

    // 不合法
    template
    typedef MagicType<std::vector, std::string> FakeDarkMagic;

    1
    2
    3

    + C++11 使用 using 引入了下面这种形式的写法,并且同时支持对传统 typedef 相同的功效
    + 通常我们使用 typedef 定义别名的语法是:`typedef 原名称 新名称;`,但是对函数指针等别名的定义语法却不相同,这通常给直接阅读造成了一定程度的困难。

    typedef int (*process)(void );
    using NewProcess = int(
    )(void *);
    template
    using TrueDarkMagic = MagicType<std::vector, std::string>;

    int main() {
    TrueDarkMagic you;
    }

    1
    2
    3
    4
    5
    6

    #### 变成参数模板

    + 模板一直是 C++ 所独有的黑魔法(一起念:Dark Magic)之一。 在 C++11 之前,无论是类模板还是函数模板,都只能按其指定的样子, 接受一组固定数量的模板参数;而 C++11 加入了新的表示方法, 允许任意个数、任意类别的模板参数,同时也不需要在定义时将参数的个数固定。
    + `template<typename... Ts> class Magic;`
    + 模板类 Magic 的对象,能够接受不受限制个数的 typename 作为模板的形式参数,例如下面的定义:

    class Magic<int,
    std::vector,
    std::map<std::string,
    std::vector>> darkMagic;

    1
    2
    3
    4
    5
    6
    7
    8
    + 既然是任意形式,所以个数为 0 的模板参数也是可以的:`class Magic<> nothing;`。
    + 如果不希望产生的模板参数个数为 0,可以手动的定义至少一个模板参数:
    + `template<typename Require, typename... Args> class Magic;`
    + 变长参数模板也能被直接调整到到模板函数上。传统 C 中的 printf 函数, 虽然也能达成不定个数的形参的调用,但其并非类别安全。 而 C++11 除了能定义类别安全的变长参数函数外, 还可以使类似 printf 的函数能自然地处理非自带类别的对象。 除了在模板参数中能使用 ... 表示不定长模板参数外, 函数参数也使用同样的表示法代表不定长参数, 这也就为我们简单编写变长参数函数提供了便捷的手段,例如:
    + `template<typename... Args> void printf(const std::string &str, Args... args);`

    + 那么我们定义了变长的模板参数,如何对参数进行解包呢?
    + 首先,我们可以使用 sizeof... 来计算参数的个数,:
    template<typename... Ts>
    void magic(Ts... args) {
        std::cout << sizeof...(args) << std::endl;
    }
    
    1
    + 我们可以传递任意个参数给 magic 函数:
    magic(); // 输出0
    magic(1); // 输出1
    magic(1, ""); // 输出2
    
    1
    2
    3
    + 其次,对参数进行解包,到目前为止还没有一种简单的方法能够处理参数包,但有两种经典的处理手法:
    1. 递归模板函数
    + 递归是非常容易想到的一种手段,也是最经典的处理方法。这种方法不断递归地向函数传递模板参数,进而达到递归遍历所有模板参数的目的:
        #include <iostream>
        template<typename T0>
        void printf1(T0 value) {
            std::cout << value << std::endl;
        }
        template<typename T, typename... Ts>
        void printf1(T value, Ts... args) {
            std::cout << value << std::endl;
            printf1(args...);
        }
        int main() {
            printf1(1, 2, "123", 1.1);
            return 0;
        }
      
    1
    2
    2. 变参模板展开
    + 你应该感受到了这很繁琐,在 C++17 中增加了变参模板展开的支持,于是你可以在一个函数中完成 printf 的编写:
    template<typename T0, typename... T> void printf2(T0 t0, T... t) { std::cout << t0 << std::endl; if constexpr (sizeof...(t) > 0) printf2(t...); }
    1
    2
    3
    4
       + 事实上,有时候我们虽然使用了变参模板,却不一定需要对参数做逐个遍历,我们可以利用 std::bind 及完美转发等特性实现对函数和参数的绑定,从而达到成功调用的目的。
    3. 初始化列表展开
    + 递归模板函数是一种标准的做法,但缺点显而易见的在于必须定义一个终止递归的函数。
    + 这里介绍一种使用初始化列表展开的黑魔法:
    template<typename T, typename... Ts> auto printf3(T value, Ts... args) { std::cout << value << std::endl; (void) std::initializer_list<T>{([&args] { std::cout << args << std::endl; }(), value)...}; }
    1
    2
    3
    4
    5
    6
            + 在这个代码中,额外使用了 C++11 中提供的初始化列表以及 Lambda 表达式的特性(下一节中将提到)。
    + 通过初始化列表,(lambda 表达式, value)... 将会被展开。由于逗号表达式的出现,首先会执行前面的 lambda 表达式,完成参数的输出。 为了避免编译器警告,我们可以将 std::initializer_list 显式的转为 void

    #### 折叠表达式

    + C++ 17 中将变长参数这种特性进一步带给了表达式,考虑下面这个例子:

    #include
    template<typename … T>
    auto sum(T … t) {
    return (t + …);
    }
    int main() {
    std::cout << sum(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) << std::endl;
    }

    1
    2
    3
    4

    #### 非类型模板参数推导

    + 前面我们主要提及的是模板参数的一种形式:类型模板参数。

    template <typename T, typename U>
    auto add(T t, U u) {
    return t+u;
    }

    1
    + 其中模板的参数 T 和 U 为具体的类型。 但还有一种常见模板参数形式可以让不同字面量成为模板参数,即非类型模板参数:

    template <typename T, int BufSize>
    class buffer_t {
    public:
    T& alloc();
    void free(T& item);
    private:
    T data[BufSize];
    }

    buffer_t<int, 100> buf; // 100 作为模板参数

    1
    + 在这种模板参数形式下,我们可以将 100 作为模板的参数进行传递。 在 C++11 引入了类型推导这一特性后,我们会很自然的问,既然此处的模板参数 以具体的字面量进行传递,能否让编译器辅助我们进行类型推导, 通过使用占位符 auto 从而不再需要明确指明类型? 幸运的是,C++17 引入了这一特性,我们的确可以 auto 关键字,让编译器辅助完成具体类型的推导, 例如:

    template void foo() {
    std::cout << value << std::endl;
    return;
    }

    int main() {
    foo<10>(); // value 被推导为 int 类型
    }

    1
    2
    3
    4
    5
    6

    ### 2.6 面向对象

    #### 委托构造

    + C++11 引入了委托构造的概念,这使得构造函数可以在同一个类中一个构造函数调用另一个构造函数,从而达到简化代码的目的:

    #include
    class Base {
    public:
    int value1;
    int value2;
    Base() {
    value1 = 1;
    }
    Base(int value) : Base() { // 委托 Base() 构造函数
    value2 = value;
    }
    };

    int main() {
    Base b(2);
    std::cout << b.value1 << std::endl;
    std::cout << b.value2 << std::endl;
    }

    1
    2
    3
    4

    #### 继承构造

    + 在传统 C++ 中,构造函数如果需要继承是需要将参数一一传递的,这将导致效率低下。C++11 利用关键字 using 引入了继承构造函数的概念:

    #include
    class Base {
    public:
    int value1;
    int value2;
    Base() {
    value1 = 1;
    }
    Base(int value) : Base() { // 委托 Base() 构造函数
    value2 = value;
    }
    };
    class Subclass : public Base {
    public:
    using Base::Base; // 继承构造
    };
    int main() {
    Subclass s(3);
    std::cout << s.value1 << std::endl;
    std::cout << s.value2 << std::endl;
    }

    1
    2
    3
    4

    #### 显式虚函数重载

    + 在传统 C++ 中,经常容易发生意外重载虚函数的事情。例如:

    struct Base {
    virtual void foo();
    };
    struct SubClass: Base {
    void foo();
    };

    1
    2
    3
    4
    + `SubClass::foo` 可能并不是程序员尝试重载虚函数,只是恰好加入了一个具有相同名字的函数。另一个可能的情形是,当基类的虚函数被删除后,子类拥有旧的函数就不再重载该虚拟函数并摇身一变成为了一个普通的类方法,这将造成灾难性的后果

    + C++11 引入了 override 和 final 这两个关键字来防止上述情形的发生。
    + 当重载虚函数时,引入 override 关键字将显式的告知编译器进行重载,编译器将检查基函数是否存在这样的虚函数,否则将无法通过编译:

    struct Base {
    virtual void foo(int);
    };
    struct SubClass: Base {
    virtual void foo(int) override; // 合法
    virtual void foo(float) override; // 非法, 父类没有此虚函数
    };

    1
    + final 则是为了防止类被继续继承以及终止虚函数继续重载引入的。

    struct Base {
    virtual void foo() final;
    };
    struct SubClass1 final: Base {
    }; // 合法

    struct SubClass2 : SubClass1 {
    }; // 非法, SubClass1 已 final

    struct SubClass3: Base {
    void foo(); // 非法, foo 已 final
    };

    1
    2
    3
    4
    5
    6
    7

    #### 显式禁用默认函数

    + 在传统 C++ 中,如果程序员没有提供,编译器会默认为对象生成默认构造函数、 复制构造、赋值算符以及析构函数。 另外,C++ 也为所有类定义了诸如 new delete 这样的运算符。 当程序员有需要时,可以重载这部分函数
    + 这就引发了一些需求:无法精确控制默认函数的生成行为。 例如禁止类的拷贝时,必须将复制构造函数与赋值算符声明为 private。 尝试使用这些未定义的函数将导致编译或链接错误,则是一种非常不优雅的方式
    + 并且,编译器产生的默认构造函数与用户定义的构造函数无法同时存在。 若用户定义了任何构造函数,编译器将不再生成默认构造函数, 但有时候我们却希望同时拥有这两种构造函数,这就造成了尴尬
    + C++11 提供了上述需求的解决方案,允许显式的声明采用或拒绝编译器自带的函数。 例如:

    class Magic {
    public:
    Magic() = default; // 显式声明使用编译器生成的构造
    Magic& operator=(const Magic&) = delete; // 显式声明拒绝编译器生成构造
    Magic(int magic_number);
    }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12

    #### 强类型枚举

    + 在传统 C++中,枚举类型并非类型安全,枚举类型会被视作整数,则会让两种完全不同的枚举类型可以进行直接的比较(虽然编译器给出了检查,但并非所有),甚至同一个命名空间中的不同枚举类型的枚举值名字不能相同,这通常不是我们希望看到的结果
    + C++11 引入了枚举类(enumeration class),并使用 enum class 的语法进行声明:
    ```c++
    enum class new_enum : unsigned int {
    value1,
    value2,
    value3 = 100,
    value4 = 100
    };
  • 这样定义的枚举实现了类型安全,首先他不能够被隐式的转换为整数,同时也不能够将其与整数数字进行比较, 更不可能对不同的枚举类型的枚举值进行比较。但相同枚举值之间如果指定的值相同,那么可以进行比较:

    1
    2
    3
    4
    if (new_enum::value3 == new_enum::value4) {
    // 会输出
    std::cout << "new_enum::value3 == new_enum::value4" << std::endl;
    }
  • 在这个语法中,枚举类型后面使用了冒号及类型关键字来指定枚举中枚举值的类型,这使得我们能够为枚举赋值(未指定时将默认使用 int)

  • 而我们希望获得枚举值的值时,将必须显式的进行类型转换,不过我们可以通过重载 << 这个算符来进行输出,可以收藏下面这个代码段:

    1
    2
    3
    4
    5
    6
    7
    8
    #include <iostream>
    template<typename T>
    std::ostream& operator<<(
    typename std::enable_if<std::is_enum<T>::value,
    std::ostream>::type& stream, const T& e)
    {
    return stream << static_cast<typename std::underlying_type<T>::type>(e);
    }
  • 这时,下面的代码将能够被编译:

    • std::cout << new_enum::value3 << std::endl

总结

  • 本节介绍了现代 C++ 中对语言可用性的增强,其中笔者认为最为重要的几个特性是几乎所有人都需要了解并熟练使用的:
    • auto 类型推导
    • 范围 for 迭代
    • 初始化列表
    • 变参模板

第三章 语言运行期的强化

3.1 Lambda表达式

  • Lambda 表达式是现代 C++ 中最重要的特性之一,而 Lambda 表达式,实际上就是提供了一个类似匿名函数的特性, 而匿名函数则是在需要一个函数,但是又不想费力去命名一个函数的情况下去使用的。这样的场景其实有很多很多, 所以匿名函数几乎是现代编程语言的标配。

基础

  • Lambda 表达式的基本语法如下:

    1
    2
    3
    [捕获列表](参数列表) mutable(可选) 异常属性 -> 返回类型 {
    // 函数体
    }
  • 上面的语法规则除了 [捕获列表] 内的东西外,其他部分都很好理解,只是一般函数的函数名被略去, 返回值使用了一个 -> 的形式进行(我们在上一节前面的尾返回类型已经提到过这种写法了)

  • 所谓捕获列表,其实可以理解为参数的一种类型,Lambda 表达式内部函数体在默认情况下是不能够使用函数体外部的变量的, 这时候捕获列表可以起到传递外部数据的作用。根据传递的行为,捕获列表也分为以下几种:

    • 值捕获
      • 与参数传值类似,值捕获的前提是变量可以拷贝,不同之处则在于,被捕获的变量在 Lambda 表达式被创建时拷贝, 而非调用时才拷贝:
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        void lambda_value_capture() {
        int value = 1;
        auto copy_value = [value] {
        return value;
        };
        value = 100;
        auto stored_value = copy_value();
        std::cout << "stored_value = " << stored_value << std::endl;
        // 这时, stored_value == 1, 而 value == 100.
        // 因为 copy_value 在创建时就保存了一份 value 的拷贝
        }
    • 引用捕获
      • 与引用传参类似,引用捕获保存的是引用,值会发生变化。
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        void lambda_reference_capture() {
        int value = 1;
        auto copy_value = [&value] {
        return value;
        };
        value = 100;
        auto stored_value = copy_value();
        std::cout << "stored_value = " << stored_value << std::endl;
        // 这时, stored_value == 100, value == 100.
        // 因为 copy_value 保存的是引用
        }
    • 隐式捕获
      • 手动书写捕获列表有时候是非常复杂的,这种机械性的工作可以交给编译器来处理,这时候可以在捕获列表中写一个 & 或 = 向编译器声明采用引用捕获或者值捕获.
  • 总结一下,捕获提供了 Lambda 表达式对外部值进行使用的功能,捕获列表的最常用的四种形式可以是:

    • [] 空捕获列表
    • [name1, name2, ...] 捕获一系列变量
    • [&] 引用捕获, 让编译器自行推导引用列表
    • [=] 值捕获, 让编译器自行推导值捕获列表
  • 表达式捕获。(这部分内容需要了解后面马上要提到的右值引用以及智能指针)

  • 上面提到的值捕获、引用捕获都是已经在外层作用域声明的变量,因此这些捕获方式捕获的均为左值,而不能捕获右值。

  • C++14 给与了我们方便,允许捕获的成员用任意的表达式进行初始化,这就允许了右值的捕获, 被声明的捕获变量类型会根据表达式进行判断,判断方式与使用 auto 本质上是相同的:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    #include <iostream>
    #include <memory> // std::make_unique
    #include <utility> // std::move

    void lambda_expression_capture() {
    auto important = std::make_unique<int>(1);
    auto add = [v1 = 1, v2 = std::move(important)](int x, int y) -> int {
    return x+y+v1+(*v2);
    };
    std::cout << add(3,4) << std::endl;
    }
  • 在上面的代码中,important 是一个独占指针,是不能够被 “=” 值捕获到,这时候我们可以将其转移为右值,在表达式中初始化。

泛型Lambda

  • 上一节中我们提到了 auto 关键字不能够用在参数表里,这是因为这样的写法会与模板的功能产生冲突。 但是 Lambda 表达式并不是普通函数,所以在没有明确指明参数表类型的情况下,Lambda 表达式并不能够模板化。 幸运的是,这种麻烦只存在于 C++11 中,从 C++14 开始,Lambda 函数的形式参数可以使用 auto 关键字来产生意义上的泛型:
    1
    2
    3
    4
    5
    6
    auto add = [](auto x, auto y) {
    return x+y;
    };

    add(1, 2);
    add(1.1, 2.2);

3.2 函数对象包装器

  • 这部分内容虽然属于标准库的一部分,但是从本质上来看,它却增强了 C++ 语言运行时的能力, 这部分内容也相当重要,所以放到这里来进行介绍。

std::function

  • Lambda 表达式的本质是一个和函数对象类型相似的类类型(称为闭包类型)的对象(称为闭包对象), 当 Lambda 表达式的捕获列表为空时,闭包对象还能够转换为函数指针值进行传递,例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    #include <iostream>

    using foo = void(int); // 定义函数类型, using 的使用见上一节中的别名语法
    void functional(foo f) { // 参数列表中定义的函数类型 foo 被视为退化后的函数指针类型 foo*
    f(1); // 通过函数指针调用函数
    }

    int main() {
    auto f = [](int value) {
    std::cout << value << std::endl;
    };
    functional(f); // 传递闭包对象,隐式转换为 foo* 类型的函数指针值
    f(1); // lambda 表达式调用
    return 0;
    }
  • 上面的代码给出了两种不同的调用形式,一种是将 Lambda 作为函数类型传递进行调用, 而另一种则是直接调用 Lambda 表达式,在 C++11 中,统一了这些概念,将能够被调用的对象的类型, 统一称之为可调用类型。而这种类型,便是通过 std::function 引入的

  • C++11 std::function 是一种通用、多态的函数封装, 它的实例可以对任何可以调用的目标实体进行存储、复制和调用操作, 它也是对 C++ 中现有的可调用实体的一种类型安全的包裹(相对来说,函数指针的调用不是类型安全的), 换句话说,就是函数的容器。当我们有了函数的容器之后便能够更加方便的将函数、函数指针作为对象进行处理。 例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    #include <functional>
    #include <iostream>

    int foo(int para) {
    return para;
    }

    int main() {
    // std::function 包装了一个返回值为 int, 参数为 int 的函数
    std::function<int(int)> func = foo;

    int important = 10;
    std::function<int(int)> func2 = [&](int value) -> int {
    return 1+value+important;
    };
    std::cout << func(10) << std::endl;
    std::cout << func2(10) << std::endl;
    }

std::bind 和 std::placeholder

  • 而 std::bind 则是用来绑定函数调用的参数的, 它解决的需求是我们有时候可能并不一定能够一次性获得调用某个函数的全部参数,通过这个函数, 我们可以将部分调用参数提前绑定到函数身上成为一个新的对象,然后在参数齐全后,完成调用。 例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    int foo(int a, int b, int c) {
    ;
    }
    int main() {
    // 将参数1,2绑定到函数 foo 上,
    // 但使用 std::placeholders::_1 来对第一个参数进行占位
    auto bindFoo = std::bind(foo, std::placeholders::_1, 1,2);
    // 这时调用 bindFoo 时,只需要提供第一个参数即可
    bindFoo(1);
    }
  • 提示:注意 auto 关键字的妙用。有时候我们可能不太熟悉一个函数的返回值类型, 但是我们却可以通过 auto 的使用来规避这一问题的出现。

3.3 右值引用

  • 右值引用是 C++11 引入的与 Lambda 表达式齐名的重要特性之一。它的引入解决了 C++ 中大量的历史遗留问题, 消除了诸如 std::vector、std::string 之类的额外开销, 也才使得函数对象容器 std::function 成为了可能。

左值, 右值的纯右值,将亡值,右值

  • 要弄明白右值引用到底是怎么一回事,必须要对左值和右值做一个明确的理解。

    • 左值 (lvalue, left value),顾名思义就是赋值符号左边的值。准确来说, 左值是表达式(不一定是赋值表达式)后依然存在的持久对象。
    • 右值 (rvalue, right value),右边的值,是指表达式结束后就不再存在的临时对象。
  • 而 C++11 中为了引入强大的右值引用,将右值的概念进行了进一步的划分,分为:纯右值、将亡值。

    • 纯右值 (prvalue, pure rvalue),纯粹的右值,要么是纯粹的字面量,例如 10, true; 要么是求值结果相当于字面量或匿名临时对象,例如 1+2。非引用返回的临时变量、运算表达式产生的临时变量、 原始字面量、Lambda 表达式都属于纯右值
  • 需要注意的是,字面量除了字符串字面量以外,均为纯右值。而字符串字面量是一个左值,类型为 const char 数组。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    #include <type_traits>

    int main() {
    // 正确,"01234" 类型为 const char [6],因此是左值
    const char (&left)[6] = "01234";

    // 断言正确,确实是 const char [6] 类型,注意 decltype(expr) 在 expr 是左值
    // 且非无括号包裹的 id 表达式与类成员表达式时,会返回左值引用
    static_assert(std::is_same<decltype("01234"), const char(&)[6]>::value, "");

    // 错误,"01234" 是左值,不可被右值引用
    // const char (&&right)[6] = "01234";
    }
  • 但是注意,数组可以被隐式转换成相对应的指针类型,而转换表达式的结果(如果不是左值引用)则一定是个右值(右值引用为将亡值,否则为纯右值)。例如:

    1
    2
    3
    4
    5
    6
    7
        const char*   p   = "01234";  // 正确,"01234" 被隐式转换为 const char*
    const char*&& pr = "01234"; // 正确,"01234" 被隐式转换为 const char*,该转换的结果是纯右值
    // const char*& pl = "01234"; // 错误,此处不存在 const char* 类型的左值
    ```

    + 将亡值 (xvalue, expiring value),是 C++11 为了引入右值引用而提出的概念(因此在传统 C++ 中, 纯右值和右值是同一个概念),也就是即将被销毁、却能够被移动的值。
    + 将亡值可能稍有些难以理解,我们来看这样的代码:

    std::vector foo() {
    std::vector temp = {1, 2, 3, 4};
    return temp;
    }

    std::vector v = foo();

    1
    2
    3
    4
    5
    6
    7
    8
    9
    + 在这样的代码中,就传统的理解而言,函数 foo 的返回值 temp 在内部创建然后被赋值给 v, 然而 v 获得这个对象时,会将整个 temp 拷贝一份,然后把 temp 销毁,如果这个 temp 非常大, 这将造成大量额外的开销(这也就是传统 C++ 一直被诟病的问题)。
    + 在最后一行中,v 是左值、 foo() 返回的值就是右值(也是纯右值)。但是,v 可以被别的变量捕获到, 而 foo() 产生的那个返回值作为一个临时值,一旦被 v 复制后,将立即被销毁,无法获取、也不能修改。 而将亡值就定义了这样一种行为:临时的值能够被识别、同时又能够被移动

    + 在 C++11 之后,编译器为我们做了一些工作,此处的左值 temp 会被进行此隐式右值转换, 等价于 `static_cast<std::vector<int> &&>(temp)`,进而此处的 v 会将 foo 局部返回的值进行移动。 也就是后面我们将会提到的移动语义。

    #### 右值引用和左值引用

    + 要拿到一个将亡值,就需要用到右值引用:`T &&`,其中 T 是类型。 右值引用的声明让这个临时值的生命周期得以延长、只要变量还活着,那么将亡值将继续存活。
    + C++11 提供了 std::move 这个方法将左值参数无条件的转换为右值, 有了它我们就能够方便的获得一个右值临时对象,例如:

    #include
    #include

    void reference(std::string& str) {
    std::cout << “左值” << std::endl;
    }
    void reference(std::string&& str) {
    std::cout << “右值” << std::endl;
    }

    int main()
    {
    std::string lv1 = “string,”; // lv1 是一个左值
    // std::string&& r1 = lv1; // 非法, 右值引用不能引用左值
    std::string&& rv1 = std::move(lv1); // 合法, std::move可以将左值转移为右值
    std::cout << rv1 << std::endl; // string,

    const std::string& lv2 = lv1 + lv1; // 合法, 常量左值引用能够延长临时变量的生命周期
    // lv2 += “Test”; // 非法, 常量引用无法被修改
    std::cout << lv2 << std::endl; // string,string,

    std::string&& rv2 = lv1 + lv2; // 合法, 右值引用延长临时对象生命周期
    rv2 += “Test”; // 合法, 非常量引用能够修改临时变量
    std::cout << rv2 << std::endl; // string,string,string,Test

    reference(rv2); // 输出左值

    return 0;
    }

    1
    2
    + rv2 虽然引用了一个右值,但由于它是一个引用,所以 rv2 依然是一个左值。
    + 注意,这里有一个很有趣的历史遗留问题,我们先看下面的代码:

    #include

    int main() {
    // int &a = std::move(1); // 不合法,非常量左引用无法引用右值
    const int &b = std::move(1); // 合法, 常量左引用允许引用右值

    std::cout << a << b << std::endl;
    }

    1
    + 第一个问题,为什么不允许非常量引用绑定到非左值?这是因为这种做法存在逻辑错误:

    void increase(int & v) {
    v++;
    }
    void foo() {
    double s = 1;
    increase(s);
    }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    + 由于 int& 不能引用 double 类型的参数,因此必须产生一个临时值来保存 s 的值, 从而当 increase() 修改这个临时值时,调用完成后 s 本身并没有被修改。
    + 第二个问题,为什么常量引用允许绑定到非左值?原因很简单,因为 Fortran 需要

    #### 移动语义

    + 传统 C++ 通过拷贝构造函数和赋值操作符为类对象设计了拷贝/复制的概念,但为了实现对资源的移动操作, 调用者必须使用先复制、再析构的方式,否则就需要自己实现移动对象的接口。
    + 试想,搬家的时候是把家里的东西直接搬到新家去,而不是将所有东西复制一份(重买)再放到新家、 再把原来的东西全部扔掉(销毁),这是非常反人类的一件事情。

    + 传统的 C++ 没有区分『移动』和『拷贝』的概念,造成了大量的数据拷贝,浪费时间和空间。 右值引用的出现恰好就解决了这两个概念的混淆问题,例如:

    #include
    class A {
    public:
    int *pointer;
    A():pointer(new int(1)) {
    std::cout << “构造” << pointer << std::endl;
    }
    A(A& a):pointer(new int(*a.pointer)) {
    std::cout << “拷贝” << pointer << std::endl;
    } // 无意义的对象拷贝
    A(A&& a):pointer(a.pointer) {
    a.pointer = nullptr;
    std::cout << “移动” << pointer << std::endl;
    }
    ~A(){
    std::cout << “析构” << pointer << std::endl;
    delete pointer;
    }
    };
    // 防止编译器优化
    A return_rvalue(bool test) {
    A a,b;
    if(test) return a; // 等价于 static_cast<A&&>(a);
    else return b; // 等价于 static_cast<A&&>(b);
    }
    int main() {
    A obj = return_rvalue(false);
    std::cout << “obj:” << std::endl;
    std::cout << obj.pointer << std::endl;
    std::cout << *obj.pointer << std::endl;
    return 0;
    }

    1
    2
    3
    4
    + 在上面的代码中:
    + 首先会在 return_rvalue 内部构造两个 A 对象,于是获得两个构造函数的输出;
    + 函数返回后,产生一个将亡值,被 A 的移动构造(A(A&&))引用,从而延长生命周期,并将这个右值中的指针拿到,保存到了 obj 中,而将亡值的指针被设置为 nullptr,防止了这块内存区域被销毁。
    + 从而避免了无意义的拷贝构造,加强了性能。再来看看涉及标准库的例子:

    #include // std::cout
    #include // std::move
    #include // std::vector
    #include // std::string

    int main() {

      std::string str = "Hello world.";
      std::vector<std::string> v;
    
      // 将使用 push_back(const T&), 即产生拷贝行为
      v.push_back(str);
      // 将输出 "str: Hello world."
      std::cout << "str: " << str << std::endl;
    
      // 将使用 push_back(const T&&), 不会出现拷贝行为
      // 而整个字符串会被移动到 vector 中,所以有时候 std::move 会用来减少拷贝出现的开销
      // 这步操作后, str 中的值会变为空
      v.push_back(std::move(str));
      // 将输出 "str: "
      std::cout << "str: " << str << std::endl;
    
      return 0;
    

    }

    1
    2
    3
    4

    #### 完美转发

    + 前面我们提到了,一个声明的右值引用其实是一个左值。这就为我们进行参数转发(传递)造成了问题:

    void reference(int& v) {
    std::cout << “左值” << std::endl;
    }
    void reference(int&& v) {
    std::cout << “右值” << std::endl;
    }
    template
    void pass(T&& v) {
    std::cout << “普通传参:”;
    reference(v); // 始终调用 reference(int&)
    }
    int main() {
    std::cout << “传递右值:” << std::endl;
    pass(1); // 1是右值, 但输出是左值

    std::cout << “传递左值:” << std::endl;
    int l = 1;
    pass(l); // l 是左值, 输出左值

    return 0;
    }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    + 对于 pass(1) 来说,虽然传递的是右值,但由于 v 是一个引用,所以同时也是左值。 因此 reference(v) 会调用 reference(int&),输出『左值』。 而对于pass(l)而言,l是一个左值,为什么会成功传递给 pass(T&&) 呢?
    + 这是基于引用坍缩规则的:在传统 C++ 中,我们不能够对一个引用类型继续进行引用, 但 C++ 由于右值引用的出现而放宽了这一做法,从而产生了引用坍缩规则,允许我们对引用进行引用, 既能左引用,又能右引用。但是却遵循如下规则:

    | 函数形参类型 | 实参参数类型 | 推倒后函数形参类型 |
    | :--- | :--- | :--- |
    | T& | 左引用 | T& |
    | T& | 右引用 | T& |
    | T&& | 左引用 | T& |
    | T&& | 右引用 | T&& |

    + 因此,模板函数中使用 T&& 不一定能进行右值引用,当传入左值时,此函数的引用将被推导为左值。 更准确的讲,无论模板参数是什么类型的引用,当且仅当实参类型为右引用时,模板参数才能被推导为右引用类型。 这才使得 v 作为左值的成功传递
    + 完美转发就是基于上述规律产生的。所谓完美转发,就是为了让我们在传递参数的时候, 保持原来的参数类型(左引用保持左引用,右引用保持右引用)。 为了解决这个问题,我们应该使用 std::forward 来进行参数的转发(传递):

    #include
    #include
    void reference(int& v) {
    std::cout << “左值引用” << std::endl;
    }
    void reference(int&& v) {
    std::cout << “右值引用” << std::endl;
    }
    template
    void pass(T&& v) {
    std::cout << “ 普通传参: “;
    reference(v);
    std::cout << “ std::move 传参: “;
    reference(std::move(v));
    std::cout << “ std::forward 传参: “;
    reference(std::forward(v));
    std::cout << “static_cast<T&&> 传参: “;
    reference(static_cast<T&&>(v));
    }
    int main() {
    std::cout << “传递右值:” << std::endl;
    pass(1);

    std::cout << “传递左值:” << std::endl;
    int v = 1;
    pass(v);

    return 0;
    }

    1
    + 输出结果为:

    传递右值:
    普通传参: 左值引用
    std::move 传参: 右值引用
    std::forward 传参: 右值引用
    static_cast<T&&> 传参: 右值引用
    传递左值:
    普通传参: 左值引用
    std::move 传参: 右值引用
    std::forward 传参: 左值引用
    static_cast<T&&> 传参: 左值引用

    1
    2
    3
    4
    5
    6

    + 无论传递参数为左值还是右值,普通传参都会将参数作为左值进行转发, 所以 `std::move` 总会接受到一个左值,从而转发调用了reference(int&&) 输出右值引用。
    + 唯独 `std::forward` 即没有造成任何多余的拷贝,同时完美转发(传递)了函数的实参给了内部调用的其他函数。
    + `std::forward` 和 `std::move` 一样,没有做任何事情,`std::move` 单纯的将左值转化为右值, `std::forward` 也只是单纯的将参数做了一个类型的转换,从现象上来看, `std::forward<T>(v)` 和 `static_cast<T&&>(v)` 是完全一样的。

    + 读者可能会好奇,为何一条语句能够针对两种类型的返回对应的值, 我们再简单看一看 `std::forward` 的具体实现机制,`std::forward` 包含两个重载:

    template
    constexpr _Tp&& forward(typename std::remove_reference<_Tp>::type& __t) noexcept
    { return static_cast<_Tp&&>(__t); }

    template
    constexpr _Tp&& forward(typename std::remove_reference<_Tp>::type&& __t) noexcept
    {
    static_assert(!std::is_lvalue_reference<_Tp>::value, “template argument”
    “ substituting _Tp is an lvalue reference type”);
    return static_cast<_Tp&&>(__t);
    }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    + 在这份实现中,`std::remove_reference` 的功能是消除类型中的引用,
    + `std::is_lvalue_reference` 则用于检查类型推导是否正确,在 `std::forward` 的第二个实现中 检查了接收到的值确实是一个左值,进而体现了坍缩规则。
    + 当 `std::forward` 接受左值时,_Tp 被推导为左值,所以返回值为左值;而当其接受右值时, _Tp 被推导为 右值引用,则基于坍缩规则,返回值便成为了 && + && 的右值。 可见 `std::forward` 的原理在于巧妙的利用了模板类型推导中产生的差异。
    + 这时我们能回答这样一个问题:为什么在使用循环语句的过程中,auto&& 是最安全的方式? 因为当 auto 被推导为不同的左右引用时,与 && 的坍缩组合是完美转发

    ### 总结

    + 本章介绍了现代 C++ 中最为重要的几个语言运行时的增强,其中笔者认为本节中提到的所有特性都是值得掌握的:
    + Lambda 表达式
    + 函数对象容器 `std::function`
    + 右值引用

    ## 第四章 容器

    ### 4.1 线性容器

    #### std::array

    + 看到这个容器的时候肯定会出现这样的问题:
    + 为什么要引入 std::array 而不是直接使用 std::vector
    + 已经有了传统数组,为什么要用 std::array

    + 先回答第一个问题,与 std::vector 不同,std::array 对象的大小是固定的,如果容器大小是固定的,那么可以优先考虑使用 std::array 容器。
    + 另外由于 std::vector 是自动扩容的,当存入大量的数据后,并且对容器进行了删除操作, 容器并不会自动归还被删除元素相应的内存,这时候就需要手动运行 shrink_to_fit() 释放这部分内存:

    std::vector v;
    std::cout << “size:” << v.size() << std::endl; // 输出 0
    std::cout << “capacity:” << v.capacity() << std::endl; // 输出 0

    // 如下可看出 std::vector 的存储是自动管理的,按需自动扩张
    // 但是如果空间不足,需要重新分配更多内存,而重分配内存通常是性能上有开销的操作
    v.push_back(1);
    v.push_back(2);
    v.push_back(3);
    std::cout << “size:” << v.size() << std::endl; // 输出 3
    std::cout << “capacity:” << v.capacity() << std::endl; // 输出 4

    // 这里的自动扩张逻辑与 Golang 的 slice 很像
    v.push_back(4);
    v.push_back(5);
    std::cout << “size:” << v.size() << std::endl; // 输出 5
    std::cout << “capacity:” << v.capacity() << std::endl; // 输出 8

    // 如下可看出容器虽然清空了元素,但是被清空元素的内存并没有归还
    v.clear();
    std::cout << “size:” << v.size() << std::endl; // 输出 0
    std::cout << “capacity:” << v.capacity() << std::endl; // 输出 8

    // 额外内存可通过 shrink_to_fit() 调用返回给系统
    v.shrink_to_fit();
    std::cout << “size:” << v.size() << std::endl; // 输出 0
    std::cout << “capacity:” << v.capacity() << std::endl; // 输出 0

    1
    2
    3
    4

    + 而第二个问题就更加简单,使用 std::array 能够让代码变得更加“现代化”,而且封装了一些操作函数,比如获取数组大小以及检查是否非空,同时还能够友好的使用标准库中的容器算法,比如 std::sort

    + 使用 std::array 很简单,只需指定其类型和大小即可:

    std::array<int, 4> arr = {1, 2, 3, 4};

    arr.empty(); // 检查容器是否为空
    arr.size(); // 返回容纳的元素数

    // 迭代器支持
    for (auto &i : arr)
    {
    // …
    }

    // 用 lambda 表达式排序
    std::sort(arr.begin(), arr.end(), [](int a, int b) {
    return b < a;
    });

    // 数组大小参数必须是常量表达式
    constexpr int len = 4;
    std::array<int, len> arr = {1, 2, 3, 4};

    // 非法,不同于 C 风格数组,std::array 不会自动退化成 T*
    // int *arr_p = arr;

    1
    + 当我们开始用上了 std::array 时,难免会遇到要将其兼容 C 风格的接口,这里有三种做法:

    void foo(int *p, int len) {
    return;
    }

    std::array<int, 4> arr = {1,2,3,4};

    // C 风格接口传参
    // foo(arr, arr.size()); // 非法, 无法隐式转换
    foo(&arr[0], arr.size());
    foo(arr.data(), arr.size());

    // 使用 std::sort
    std::sort(arr.begin(), arr.end());

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12

    #### std::forward_list

    + std::forward_list 是一个列表容器,使用方法和 std::list 基本类似,因此我们就不花费篇幅进行介绍了。
    + 需要知道的是,和 std::list 的双向链表的实现不同,std::forward_list 使用单向链表进行实现, 提供了 O(1) 复杂度的元素插入,不支持快速随机访问(这也是链表的特点), 也是标准库容器中唯一一个不提供 size() 方法的容器。当不需要双向迭代时,具有比 std::list 更高的空间利用率。

    ### 4.2 无序容器

    + 我们已经熟知了传统 C++ 中的有序容器 std::map/std::set,这些元素内部通过红黑树进行实现, 插入和搜索的平均复杂度均为 O(log(size))。在插入元素时候,会根据 < 操作符比较元素大小并判断元素是否相同, 并选择合适的位置插入到容器中。当对这个容器中的元素进行遍历时,输出结果会按照 < 操作符的顺序来逐个遍历。
    + 而无序容器中的元素是不进行排序的,内部通过 Hash 表实现,插入和搜索元素的平均复杂度为 O(constant), 在不关心容器内部元素顺序时,能够获得显著的性能提升。
    + C++11 引入了的两组无序容器分别是:`std::unordered_map/std::unordered_multimap` 和 `std::unordered_set/std::unordered_multiset`
    + 它们的用法和原有的 `std::map/std::multimap/std::set/set::multiset` 基本类似, 由于这些容器我们已经很熟悉了,便不一一举例,我们直接来比较一下`std::map`和`std::unordered_map`:

    #include
    #include
    #include
    #include

    int main() {
    // 两组结构按同样的顺序初始化
    std::unordered_map<int, std::string> u = {
    {1, “1”},
    {3, “3”},
    {2, “2”}
    };
    std::map<int, std::string> v = {
    {1, “1”},
    {3, “3”},
    {2, “2”}
    };

    // 分别对两组结构进行遍历
    std::cout << “std::unordered_map” << std::endl;
    for( const auto & n : u)
    std::cout << “Key:[“ << n.first << “] Value:[“ << n.second << “]\n”;

    std::cout << std::endl;
    std::cout << “std::map” << std::endl;
    for( const auto & n : v)
    std::cout << “Key:[“ << n.first << “] Value:[“ << n.second << “]\n”;
    }

    1
    + 最终输出的结果为:

    std::unordered_map
    Key:[2] Value:[2]
    Key:[3] Value:[3]
    Key:[1] Value:[1]

    std::map
    Key:[1] Value:[1]
    Key:[2] Value:[2]
    Key:[3] Value:[3]

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13

    ### 4.3 元组

    + 了解过 Python 的程序员应该知道元组的概念,纵观传统 C++ 中的容器,除了 std::pair 外, 似乎没有现成的结构能够用来存放不同类型的数据(通常我们会自己定义结构)。 但 std::pair 的缺陷是显而易见的,只能保存两个元素。

    #### 元组基本操作

    + 关于元组的使用有三个核心的函数:
    + std::make_tuple: 构造元组
    + std::get: 获得元组某个位置的值
    + std::tie: 元组拆包

    + 示例:

    #include
    #include

    auto get_student(int id)
    {
    // 返回类型被推断为 std::tuple<double, char, std::string>

    if (id == 0)
    return std::make_tuple(3.8, ‘A’, “张三”);
    if (id == 1)
    return std::make_tuple(2.9, ‘C’, “李四”);
    if (id == 2)
    return std::make_tuple(1.7, ‘D’, “王五”);
    return std::make_tuple(0.0, ‘D’, “null”);
    // 如果只写 0 会出现推断错误, 编译失败
    }

    int main()
    {
    auto student = get_student(0);
    std::cout << “ID: 0, “
    << “GPA: “ << std::get<0>(student) << “, “
    << “成绩: “ << std::get<1>(student) << “, “
    << “姓名: “ << std::get<2>(student) << ‘\n’;

    double gpa;
    char grade;
    std::string name;

    // 元组进行拆包
    std::tie(gpa, grade, name) = get_student(1);
    std::cout << “ID: 1, “
    << “GPA: “ << gpa << “, “
    << “成绩: “ << grade << “, “
    << “姓名: “ << name << ‘\n’;
    }

    1
    2

    + std::get 除了使用常量获取元组对象外,C++14 增加了使用类型来获取元组中的对象:

    std::tuple<std::string, double, double, int> t(“123”, 4.5, 6.7, 8);
    std::cout << std::getstd::string(t) << std::endl;
    std::cout << std::get(t) << std::endl; // 非法, 引发编译期错误
    std::cout << std::get<3>(t) << std::endl;

    1
    2
    3
    4

    #### 运行期索引

    + 如果你仔细思考一下可能就会发现上面代码的问题,std::get<> 依赖一个编译期的常量,所以下面的方式是不合法的:

    int index = 1;
    std::get(t);

    1
    + 那么要怎么处理?答案是,使用 std::variant<>(C++ 17 引入),提供给 variant<> 的类型模板参数 可以让一个 variant<> 从而容纳提供的几种类型的变量(在其他语言,例如 Python/JavaScript 等,表现为动态类型):

    #include
    template <size_t n, typename… T>
    constexpr std::variant<T…> _tuple_index(const std::tuple<T…>& tpl, size_t i) {
    if constexpr (n >= sizeof…(T))
    throw std::out_of_range(“越界.”);
    if (i == n)
    return std::variant<T…>{ std::in_place_index, std::get(tpl) };
    return _tuple_index<(n < sizeof…(T)-1 ? n+1 : 0)>(tpl, i);
    }
    template <typename… T>
    constexpr std::variant<T…> tuple_index(const std::tuple<T…>& tpl, size_t i) {
    return _tuple_index<0>(tpl, i);
    }
    template <typename T0, typename … Ts>
    std::ostream & operator<< (std::ostream & s, std::variant<T0, Ts…> const & v) {
    std::visit([&](auto && x){ s << x;}, v);
    return s;
    }

    1
    + 这样我们就能:

    int i = 1;
    std::cout << tuple_index(t, i) << std::endl;

    1
    2
    3
    4
    5
    6
    7

    #### 元组合并与遍历

    + 还有一个常见的需求就是合并两个元组,这可以通过 std::tuple_cat 来实现:
    + `auto new_tuple = std::tuple_cat(get_student(1), std::move(t));`

    + 马上就能够发现,应该如何快速遍历一个元组?但是我们刚才介绍了如何在运行期通过非常数索引一个 tuple 那么遍历就变得简单了, 首先我们需要知道一个元组的长度,可以:

    template
    auto tuple_len(T &tpl) {
    return std::tuple_size::value;
    }

    1
    + 这样就能够对元组进行迭代了:

    // 迭代
    for(int i = 0; i != tuple_len(new_tuple); ++i)
    // 运行期索引
    std::cout << tuple_index(new_tuple, i) << std::endl;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20

    ### 总结

    + 本章简单介绍了现代 C++ 中新增的容器,它们的用法和传统 C++ 中已有的容器类似,相对简单,可以根据实际场景丰富的选择需要使用的容器,从而获得更好的性能。
    + std::tuple 虽然有效,但是标准库提供的功能有限,没办法满足运行期索引和迭代的需求,好在我们还有其他的方法可以自行实现。

    ## 第五章 智能指针与内存管理

    ### 5.1 RAII与引用计数

    + 引用计数这种计数是为了防止内存泄露而产生的。 基本想法是对于动态分配的对象,进行引用计数,每当增加一次对同一个对象的引用,那么引用对象的引用计数就会增加一次, 每删除一次引用,引用计数就会减一,当一个对象的引用计数减为零时,就自动删除指向的堆内存。
    + 在传统 C++ 中,『记得』手动释放资源,总不是最佳实践。因为我们很有可能就忘记了去释放资源而导致泄露。 所以通常的做法是对于一个对象而言,我们在构造函数的时候申请空间,而在析构函数(在离开作用域时调用)的时候释放空间, 也就是我们常说的 **RAII 资源获取即初始化技术**
    + 凡事都有例外,我们总会有需要将对象在自由存储上分配的需求,在传统 C++ 里我们只好使用 new 和 delete 去 『记得』对资源进行释放。而 C++11 引入了智能指针的概念,使用了引用计数的想法,让程序员不再需要关心手动释放内存。 这些智能指针包括 `std::shared_ptr/std::unique_ptr/std::weak_ptr`,使用它们需要包含头文件 `<memory>`
    + 注意:引用计数不是垃圾回收,引用计数能够尽快收回不再被使用的对象,同时在回收的过程中也不会造成长时间的等待, 更能够清晰明确的表明资源的生命周期。

    ### 5.2 std::shared_ptr

    + `std::shared_ptr` 是一种智能指针,它能够记录多少个 shared_ptr 共同指向一个对象,从而消除显式的调用 delete,当引用计数变为零的时候就会将对象自动删除。但还不够,因为使用 `std::shared_ptr` 仍然需要使用 new 来调用,这使得代码出现了某种程度上的不对称。
    + `std::make_shared` 就能够用来消除显式的使用 new,所以 `std::make_shared` 会分配创建传入参数中的对象, 并返回这个对象类型的 `std::shared_ptr` 指针
    + 例如:

    #include
    #include
    void foo(std::shared_ptr i) {
    (*i)++;
    }
    int main() {
    // auto pointer = new int(10); // illegal, no direct assignment
    // Constructed a std::shared_ptr
    auto pointer = std::make_shared(10);
    foo(pointer);
    std::cout << *pointer << std::endl; // 11
    // The shared_ptr will be destructed before leaving the scope
    return 0;
    }

    1
    2
    3
    4
    5
    6

    + `std::shared_ptr` 可以通过 `get()` 方法来获取原始指针,通过 `reset()` 来减少一个引用计数, 并通过 `use_count()` 来查看一个对象的引用计数

    ### 5.3 std::unique_str

    + `std::unique_ptr` 是一种独占的智能指针,它禁止其他智能指针与其共享同一个对象,从而保证代码的安全:

    std::unique_ptr pointer = std::make_unique(10); // make_unique 从 C++14 引入
    std::unique_ptr pointer2 = pointer; // 非法

    1
    + make_unique 并不复杂,C++11 没有提供 std::make_unique,可以自行实现:

    template<typename T, typename …Args>
    std::unique_ptr make_unique( Args&& …args ) {
    return std::unique_ptr( new T( std::forward(args)… ) );
    }

    1
    2
    3

    + 既然是独占,换句话说就是不可复制。但是,我们可以利用 `std::move` 将其转移给其他的 `unique_ptr`
    + 例如:

    #include
    #include

    struct Foo {
    Foo() { std::cout << “Foo::Foo” << std::endl; }
    Foo() { std::cout << “Foo::Foo” << std::endl; }
    void foo() { std::cout << “Foo::foo” << std::endl; }
    };

    void f(const Foo &) {
    std::cout << “f(const Foo&)” << std::endl;
    }

    int main() {
    std::unique_ptr p1(std::make_unique());
    // p1 不空, 输出
    if (p1) p1->foo();
    {
    std::unique_ptr p2(std::move(p1));
    // p2 不空, 输出
    f(*p2);
    // p2 不空, 输出
    if(p2) p2->foo();
    // p1 为空, 无输出
    if(p1) p1->foo();
    p1 = std::move(p2);
    // p2 为空, 无输出
    if(p2) p2->foo();
    std::cout << “p2 被销毁” << std::endl;
    }
    // p1 不空, 输出
    if (p1) p1->foo();
    // Foo 的实例会在离开作用域时被销毁
    }

    1
    2
    3
    4

    ### 5.4 std::weak_ptr

    + 如果你仔细思考 `std::shared_ptr` 就会发现依然存在着资源无法释放的问题。看下面这个例子:

    struct A;
    struct B;

    struct A {
    std::shared_ptr pointer;
    ~A() {
    std::cout << “A 被销毁” << std::endl;
    }
    };
    struct B {
    std::shared_ptr pointer;
    ~B() {
    std::cout << “B 被销毁” << std::endl;
    }
    };
    int main() {
    auto a = std::make_shared
    ();
    auto b = std::make_shared();
    a->pointer = b;
    b->pointer = a;
    }

    
    
  • 运行结果是 A, B 都不会被销毁,这是因为 a,b 内部的 pointer 同时又引用了 a,b,这使得 a,b 的引用计数均变为了 2,而离开作用域时,a,b 智能指针被析构,却只能造成这块区域的引用计数减一,这样就导致了 a,b 对象指向的内存区域引用计数不为零,而外部已经没有办法找到这块区域了,也就造成了内存泄露

  • 解决这个问题的办法就是使用弱引用指针 std::weak_ptrstd::weak_ptr 是一种弱引用(相比较而言 std::shared_ptr 就是一种强引用)

  • std::weak_ptr 没有 * 运算符和 -> 运算符,所以不能够对资源进行操作,它可以用于检查 std::shared_ptr 是否存在,其 expired() 方法能在资源未被释放时,会返回 false,否则返回 true

  • 除此之外,它也可以用于获取指向原始对象的 std::shared_ptr 指针,其 lock() 方法在原始对象未被释放时,返回一个指向原始对象的 std::shared_ptr 指针,进而访问原始对象的资源,否则返回 nullptr