1. 基本概念
- 核心概念与词法结构
- 程序的构成 :
- 一个 C++ 程序是由一系列文本文件(通常为头文件和源文件)组成的序列,其中包含各种声明 。
- 这些文件经过翻译后成为可执行程序,并在 C++ 实现调用其
main函数时开始执行。
- 词法元素 :
- 关键字 :具有特殊语言含义的保留字。
- 标识符 :用户自定义或标准库使用的名称。
- 注释 :在翻译阶段会被编译器忽略。
- 字面量 :直接写在代码中的固定值,其内部字符的值由字符集与编码决定。
- 转义序列 :用于在代码中表示某些特殊字符。
- 程序的构成 :
- 程序实体与宏
- C++ 实体 :
- 程序中的实体包括:值、对象、引用、结构化绑定(自 C++17 起)、结果绑定(自 C++26 起)、函数、枚举数、类型、类成员、模板、模板特化、参数包(自 C++11 起)以及命名空间 。
- 宏的例外 :
- 预处理器宏不是 C++ 实体。
- C++ 实体 :
- 声明、定义与 ODR 规则
- 声明与定义 :
- 声明 :引入实体、将其与名称关联并定义其属性。
- 定义 :提供使用该实体所需的所有属性的声明。
- 单定义规则(ODR) :
- 对于任何被 ODR使用 的非内联函数或变量,整个程序中只能包含一个定义。
- 声明与定义 :
- 语句、表达式与名称管理
- 计算与执行 :
- 函数的定义通常包含一系列语句 ,部分语句中包含表达式 ,用于指定程序要执行的具体计算。
- 名称查找与作用域 :
- 程序中遇到的名称会通过名称与引入它们的声明关联起来。
- 每个名称仅在程序的特定部分(称为其作用域)内有效。
- 链接 :
- 某些名称具有链接属性,这使得它们在不同的作用域或不同的翻译单元中出现时,能够指代同一个实体。
- 计算与执行 :
- 类型与变量
- 类型系统 :
- C++ 中的每个对象、引用、函数和表达式都关联着一个类型 。
- 类型可以是基础的、复合的、或用户定义的 ,也可以是完整的或不完整的等。
- 变量 :
- 所有已声明的对象 以及已声明的引用 (但非静态数据成员 除外)统称为变量 。
- 类型系统 :
1.1 注释
- 作用:注释充当代码内文档。当插入程序中时,它们会被编译器完全忽略,仅供阅读源代码的人类做笔记使用。
- 文档工具:尽管特定的文档格式不属于 C++ 标准,但存在许多能够解析不同文档格式注释的实用工具。
1.1.1 语法形式
/* multi-line-content */- 通常被称为"C 风格"或"多行"注释。
multi-line-content是一个不能包含字符序列*/的字符序列。
- // single-line-content`
- 通常被称为"C++ 风格"或"单行"注释。
single-line-content是一个不能包含任何换行符的字符序列。
1.1.2 编译阶段处理与行为规范
- 移除阶段 :所有注释都会在翻译阶段 3 从程序中移除,其处理方式是将每个注释替换为一个单一的空白字符 。
- C 风格注释特性 :
- 通常用于注释大段文本,但也可以用于单行。
- 不能嵌套 :例如
/* ... /* nested? */ ... */中的第一个*/会直接结束注释,导致后面的内容被编译器误认为是代码而引发语法错误。
- C++ 风格注释特性 :
- 通常用于单行注释,但多个 C++ 风格注释可以连续放置形成多行注释。
- 编译器会忽略从
//开始直到换行符之间的所有内容。
1.1.3 注意事项与代码排除机制
- 文档块惯例 :尽管不属于 C++ 标准,
/**和*/常常被用来表示文档块。这是合法的,因为第二个星号(*)在语法上只是被视为注释内容的一部分。 - 预处理器交互 :由于注释在预处理器阶段之前就被删除了,因此不能使用宏来拼凑一个注释 ,且
#include文件中未闭合的 C-style 注释也不会"溢出"并影响包含它的文件。 - 其他源码排除机制 :除了使用注释外,还可以通过以下机制来排除代码:
- 条件编译:
#if 0 ... #endif(代码甚至不会被编译)。 - 运行时条件判断:
if (false) { ... }(代码会被编译但不会被执行)。
- 条件编译:
1.2 ASCII 字符对照表
- 包含内容 :包含了所有 128 个 ASCII 字符的十进制、八进制、十六进制编码以及对应的字符。
- Unicode 映射 :在 Unicode 标准中,这个 ASCII 字符块被称为
U+0000..U+007F基本拉丁字母 。
1.2.1 ASCII 编码表结构与内容详解
在 C++ 中广泛用于字符字面量、转义处理以及底层字节操作:
- 控制字符段(0 ~ 31 以及 127) :
- 0 ~ 31 :不可打印的控制字符(Control Characters),例如
NUL(空字符,0)、BEL(响铃,7)、HT(水平制表符,9)、LF(换行符,10)、CR(回车符,13)等。 - 127 :
DEL(删除字符)。
- 0 ~ 31 :不可打印的控制字符(Control Characters),例如
- 可打印字符段(32 ~ 126) :
- 32 :
(space)空格。 - 33 ~ 47 :标点与特殊符号(如
!,",#,$,%,&,',(,),*,+,,,-,.,/)。 - 48 ~ 57 :数字
0到9。 - 58 ~ 64 :标点与符号(如
:,;,<,=,>,?,@)。 - 65 ~ 90 :大写英文字母
A到Z。 - 91 ~ 96 :标点与符号(如
[,, `]`, `^`, `_`,` ``)。 - 97 ~ 122 :小写英文字母
a到z。 - 123 ~ 126 :特殊符号(如
{,|,},~)。
- 32 :
1.3 标点符号与运算符
本节列举了 C++ 语言中所有的标点符号及复合运算符。如下:
- 预处理运算符
#(替代拼写:%:)- 引入一个预处理指令 。
- 作为字符串化预处理运算符。
##(替代拼写:%:%:)- 作为符号粘贴预处理运算符。
- 单字符运算符与标点
{和}(替代拼写分别为<%和%>)- 定界作用 :用于类定义中的成员规范、枚举定义中的枚举数列表、复合语句(函数定义、
try块、lambda 表达式中)、命名空间主体、语言链接规范、requires表达式、复合要求、导出声明(自 C++20)以及聚合初始化/列表初始化(自 C++11 起)的初始化器。
- 定界作用 :用于类定义中的成员规范、枚举定义中的枚举数列表、复合语句(函数定义、
[和](替代拼写分别为<:和:>)- 核心用途 :下标运算符、数组声明符/类型标识(如
new表达式)、new[]和delete[]运算符、lambda 表达式的捕获列表、属性说明符(自 C++11)、结构化绑定声明(自 C++17)以及参数包索引(自 C++26)。
- 核心用途 :下标运算符、数组声明符/类型标识(如
(和)- 核心用途 :表达式中的分组、函数调用运算符、函数式类型转换、显式转型(
static_cast等)、限定以下运算符的操作数(typeid、sizeof、alignof、noexcept等)、new/delete表达式中的参数、C 风格强制转型、声明或类型标识中的分组、函数声明/lambda/演绎指南/requires 表达式的形参列表、直接初始化语法、asm声明、构造函数成员初始化列表、控制流语句(if、switch、while、for等)的控制子句、异常捕获器参数、宏定义与调用参数、static_assert声明、属性参数、decltype(auto)、折叠表达式(自 C++17)以及__VA_OPT__替换(自 C++20)。
- 核心用途 :表达式中的分组、函数调用运算符、函数式类型转换、显式转型(
;- 结束标志 :指示语句、声明或成员声明、模块声明/导入、模块片段引入符、要求的结束,以及分隔
for语句的条件与语句。
- 结束标志 :指示语句、声明或成员声明、模块声明/导入、模块片段引入符、要求的结束,以及分隔
:- 核心用途 :条件运算符的一部分、标号声明、类定义中的基类子句、成员规范中的访问说明符、位域成员声明的宽度、构造函数成员初始化列表、范围
for语句中分隔项声明与范围初始化器、枚举声明的基础类型(自 C++11)、属性命名空间分隔符(自 C++17)、模块分区名称引入符,以及私有模块片段引入符(-- :private;,自 C++20)。
- 核心用途 :条件运算符的一部分、标号声明、类定义中的基类子句、成员规范中的访问说明符、位域成员声明的宽度、构造函数成员初始化列表、范围
?- 条件运算符的一部分。
.- 成员访问运算符、聚合初始化中的指示符,以及模块名称/分区名称的一部分(自 C++20)。
~(替代拼写:compl)- 一元补码运算符(按位非运算符),以及在标识符表达式中用于命名析构函数或伪析构函数。
!(替代拼写:not)- 逻辑非运算符,以及
consteval if语句的一部分(自 C++23)。
- 逻辑非运算符,以及
+/-- 一元加/减与二元加/减运算符
*- 间接寻址运算符、乘法运算符、声明符或类型标识中的指针/成员指针运算符,以及 lambda 捕获列表中按值捕获当前对象的
*this(自 C++17)。
- 间接寻址运算符、乘法运算符、声明符或类型标识中的指针/成员指针运算符,以及 lambda 捕获列表中按值捕获当前对象的
/- 除法运算符
%- 取模运算符
^(替代拼写:xor)- 按位异或运算符
&(替代拼写:bitand)- 取地址运算符、按位与运算符、声明符或类型标识中的左值引用运算符、lambda 按引用捕获指示符,以及成员函数声明中的引用限定符(自 C++11)。
|(替代拼写:bitor)- 按位或运算符
=- 简单赋值运算符(可能是拷贝/移动赋值特殊成员函数)、拷贝/列表初始化语法的一部分、函数声明中的默认参数、模板参数的默认实参、命名空间别名定义、枚举值初始化、纯虚函数声明中的纯说明符(
=0)、lambda 捕获默认值、函数的默认定义/删除定义、类型别名声明(自 C++11),以及概念定义中分隔概念名称与约束表达式(自 C++20)。
- 简单赋值运算符(可能是拷贝/移动赋值特殊成员函数)、拷贝/列表初始化语法的一部分、函数声明中的默认参数、模板参数的默认实参、命名空间别名定义、枚举值初始化、纯虚函数声明中的纯说明符(
</>- 小于/大于运算符、
static_cast等转型的定界符、模板参数列表/声明的引入与结束符,以及#include路径或__has_include、import的头文件名称引入与结束符(自 C++17/C++20)。
- 小于/大于运算符、
,- 逗号运算符以及各种声明、初始化、参数列表、属性列表、结构化绑定标识符列表(自 C++17)等场景的分隔符。
- 多字符运算符与标点
[:和:](自 C++26)- 拼接说明符的定界符。
^^(自 C++26)- 反射运算符
...- 用于变长函数/lambda/演绎指南的形参列表(表示变长参数)、
catch(...)异常捕获、变长宏定义、声明及展开参数包(自 C++11),以及包索引表达式与说明符(自 C++26)。
- 用于变长函数/lambda/演绎指南的形参列表(表示变长参数)、
::- 作用域解析运算符(限定名、成员指针声明、全局
new/delete查找),属性作用域指示符(自 C++11),以及嵌套命名空间定义的一部分(自 C++17)。
- 作用域解析运算符(限定名、成员指针声明、全局
.*/->*- 成员指针访问运算符
->- 成员访问运算符、lambda 表达式或函数声明中的尾置返回类型引入符(自 C++11)、用户定义演绎指南的结果类型引入符(自 C++17),以及复合要求中的返回类型要求引入符(自 C++20)。
- 复合赋值运算符 (
+=,-=,*=,/=,%=,^=/xor_eq,&=/and_eq,|=/or_eq)- 对应算术与位操作的复合赋值运算符及其重载。
- 比较运算符 (
==,!=/not_eq,<=,>=,<=>/ 三路比较空间舰运算符,自 C++20)- 用于对象间大小或相等关系的比较。
- 逻辑与/或运算符 (
&&/and,||/or)- 逻辑布尔运算,其中
&&还可作为右值引用运算符或成员函数的引用限定符(自 C++11)。
- 逻辑布尔运算,其中
- 移位运算符 (
<<,>>)- 位移运算符(按位左/右移或流插入/提取运算符),其对应的复合赋值形式为
<<=和>>=。
- 位移运算符(按位左/右移或流插入/提取运算符),其对应的复合赋值形式为
- 自增/自减运算符 (
++,--)- 变量自增与自减运算符及其重载。
1.4 标识符
- 基本定义:标识符是由任意长度的数字、下划线、大小写拉丁字母以及大部分 Unicode字符组成的序列。
- 首字符限制 :有效标识符的第一个字符必须是以下之一:
- 大写拉丁字母
A-Z、小写拉丁字母a-z - 下划线
_ - 具有 Unicode 属性
XID_Start的任意 Unicode 字符。
- 大写拉丁字母
- 后续字符限制 :标识符中的其余字符必须是以下之一:
- 数字
0-9、大小写拉丁字母A-Z、小写拉丁字母a-z - 下划线
_ - 具有 Unicode 属性
XID_Continue的任意 Unicode 字符。
- 数字
- 大小写敏感与规范 :标识符是大小写敏感 的(区分大小写),且每一个字符都具有实际意义。每个标识符都必须符合规范化形式 C 。
1.4.1 在声明中的应用
- 命名用途 :标识符可用于命名对象、引用、函数、枚举数、类型、类成员、命名空间、模板、模板特化、参数包(自 C++11 起) 、
goto标号以及其他实体。 - 例外情况(保留与特殊用途) :
- 关键字 :不能用于其他目的(唯一的例外是作为
attribute-token的一部分,如[[private]])。 - 运算符与标点的替代表示:不能用于其他用途。
- 具有特殊含义的标识符 :
final、import、module(自 C++20)和override在特定上下文中显式使用,其余情况作为常规标识符处理。
- 关键字 :不能用于其他目的(唯一的例外是作为
1.4.2 保留标识符与宏
- 全局命名空间中的保留规则 :
- 以单下划线开头的标识符。
- 包含双下划线或以单下划线后跟大写字母开头的标识符(预定义宏、
std::_Exit、__func__及标准库中的特定兼容宏如__alignas_is_defined等除外)。
- 含义 :如果程序员使用了这些保留标识符,程序属于语法错误 。此外,在翻译单元中
#define或#undef某些特定名称会导致未定义行为(UB) 。 - 僵尸标识符 :自 C++14 起,部分标准库中移除的标识符被称为僵尸名,它们在特定上下文中(如宏名称)仍然对以前的标准化保持保留。
1.4.3 在表达式中的应用与类型
- 表达式角色:命名变量、函数、概念特化(自 C++20)或枚举数的标识符可用作表达式。
- 值类别 :
- 若标识符命名的是函数、变量、模板形参对象或数据成员,则表达式的值类别为 lvalue 。
- 否则为 prvalue (例如枚举数、概念特化等)。
- 表达式类型:标识符表达式的类型与其所命名实体的类型相同(局部实体涉及 lambda 拷贝捕获时存在特定类成员访问转换例外)。
1.4.4 限定标识符/非限定标识符
- 未限定标识符表达式 :
- 除了常规声明的标识符外,还包括重载运算符名称、用户定义转换函数名称、用户定义字面量运算符名称、带参数列表的模板名称、以及波浪号
~后跟类名/decltype等析构函数命名方式。
- 除了常规声明的标识符外,还包括重载运算符名称、用户定义转换函数名称、用户定义字面量运算符名称、带参数列表的模板名称、以及波浪号
- 限定标识符表达式 :
- 在未限定标识符表达式前置作用域解析运算符
::,以及由其组合的命名空间、类名、枚举名或decltype等限定序列
- 在未限定标识符表达式前置作用域解析运算符
- 隐式成员访问转换 :
- 若标识符表达式
E满足特定条件(如引用类的非静态成员且为潜在求值),会被自动转换为this->E。
- 若标识符表达式
1.4.5 名称的概念
- 名称 :是指使用标识符、重载运算符名、用户定义转换函数名、用户定义字面量运算符名或带参数列表的模板名来指代某个实体的统称。所有名称均由声明引入,并通过名称查找与声明关联。
1.5 类型
- 核心作用:对象、引用、函数模板特化及表达式都具有类型属性,它既限制了对这些实体允许执行的操作,又为原本泛型的比特序列赋予了语义。
- 类型系统的主要构成 :
- 基础类型 :
void类型std::nullptr_t(自 C++11)std::meta::info(反射类型,自 C++26)- 算术类型 :
- 整型 :包括
bool、窄字符类型(普通char、signed char、unsigned char、char8_t(自 C++20))以及宽字符类型(char16_t、char32_t、wchar_t)、标准有符号/无符号整型(short、int、long、long long及其无符号版本)以及扩展整型。 - 浮点型 :标准浮点型(
float、double、long double)及扩展/固定宽度浮点型(自 C++23)。
- 整型 :包括
- 复合类型 :
- 引用类型 :左值引用(对象/函数)与右值引用(对象/函数,自 C++11)。
- 指针类型 :指向对象、函数及成员(数据成员/成员函数)的指针。
- 数组类型
- 函数类型
- 枚举类型 :无作用域枚举与有作用域枚举(自 C++11)。
- 类类型 :非联合体类型与联合体类型
- 基础类型 :
- CV 限定符 :对于除引用和函数之外的每个非 cv 限定类型,类型系统支持三种额外的 cv 限定版本:
const、volatile和const volatile。
1.5.1 其他类型分类与属性
- 对象类型 :不是函数类型、不是引用类型、也不是(可能经 cv 限定的)
void的类型。 - 标量类型 :算术类型、枚举类型、指针类型、成员指针类型、
std::nullptr_t、std::meta::info及其 cv 限定版本。 - 隐式生存期类型 :标量类型、隐式生存期类类型、数组类型及其 cv 限定版本。
- 可 trivially 复制类型 :标量类型、可 trivially 复制的类类型、此类类型的数组及其 cv 限定版本。
- 标准布局类型 :标量类型、标准布局类类型、此类类型的数组及其 cv 限定版本(自 C++11)。
- 已废弃分类:POD 类型(在 C++20 中废弃)、Trivial 类型(在 C++26 中废弃)。
- 程序定义类型 :非标准库或实现的类类型、枚举类型、非实现提供的 lambda 表达式闭包类型,以及程序定义特化的实例化。
1.5.2 类型命名与 Type-id
- 命名方式 :可以通过
class、union、enum、typedef或类型别名(using)声明来引入一个指代类型的名称。 type-id语法 :- 当需要引用没有名称的类型时使用。其语法与声明一个该类型变量或函数的语法完全相同,但省略了标识符 ,且声明语法中的
decl-specifier-seq被约束为type-specifier-seq。 - 常见应用场景 :强制转换表达式的目标类型(如
static_cast<int*>)、sizeof/alignof/new/typeid的参数、类型别名右侧、函数尾置返回类型等。
- 当需要引用没有名称的类型时使用。其语法与声明一个该类型变量或函数的语法完全相同,但省略了标识符 ,且声明语法中的
1.5.3 静态类型与动态类型
- 静态类型 :通过程序编译时分析得出的表达式类型,在程序执行期间不会改变。
- 动态类型 :若某个 glvalue 表达式指代一个多态对象 ,则其最派生对象的类型被称为动态类型。对于 prvalue 表达式,动态类型始终与静态类型相同。
1.5.4 完全类型与不完全类型
- 不完全类型 :
void(可能经 cv 限定)。- 未完全定义的对象类型:已声明(如通过前向声明
struct X;)但尚未定义的类类型、未知边界的数组、元素为不完全类型的数组,以及从声明点到确定底层类型的枚举类型。
- 完全类型 :所有其他类型。
- 要求完全类型的上下文 :当需要知道类型的大小和内存布局时(例如定义对象、作为函数返回值或参数、进行
sizeof计算、使用类成员访问运算符等),上下文要求该类型必须是完全类型。不完全类型可以在翻译单元的后续部分被补全(例如给出类定义或确定数组边界)。
1.5.5 基础类型
C++ 中的基础类型主要包括:
- 可能是 cv 限定的
void - 可能是 cv 限定的
std::nullptr_t(自 C++11) - 可能是 cv 限定的
std::meta::info(反射类型,自 C++26) - 整型
- 浮点型
特殊基础类型
void:- 具有空值集 的不完全类型,无法被补全,因此不允许存在
void类型的对象 ,也没有void的数组或引用。 - 允许存在指向
void的指针 (void*)以及返回类型为void的函数。
- 具有空值集 的不完全类型,无法被补全,因此不允许存在
std::nullptr_t(自 C++11):- 空指针字面量
nullptr的类型。它是一个独立的类型,其大小sizeof(std::nullptr_t)等于sizeof(void*)。
- 空指针字面量
std::meta::info(自 C++26):- 反射值的类型,声明在
<meta>中。
- 反射值的类型,声明在
整型与布尔型
- 标准整型 :
- 基本整型为
int。可以通过修饰符(signed、unsigned、short、long、long long)进行组合。 - 保证至少为 16 位(在主流 32/64 位系统上通常为 32 位)。
- 基本整型为
- 布尔型 :
- 能够保存
true或false值的整型。其大小由实现定义,可能不等于 1 字节。
- 能够保存
- 数据模型与整型大小 : 不同的系统架构对基本类型的字节宽度定义有所不同:
- ILP32 (Unix/Linux/macOS 32位):
int(32位),long(32位), 指针 (32位)。 - LLP64 (Windows 64位):
int(32位),long(32位), 指针 (64位)。 - LP64 (Unix/Linux/macOS 64位):
int(32位),long(64位), 指针 (64位)。
- ILP32 (Unix/Linux/macOS 32位):
字符类型:C++ 提供多种用于字符和原始内存表示的整型字符类型:
signed char/unsigned char:有符号/无符号字符表示,其中unsigned char也常用于检查对象表示(原始内存)。char:最高效处理的字符类型(其符号性取决于编译器和平台,x86/x64 通常为 signed,ARM/PowerPC 通常为 unsigned)。wchar_t:宽字符类型,在 Linux 等系统上通常为 32 位(UTF-32),在 Windows 上通常为 16 位(UTF-16)。char16_t(自 C++11)与char32_t(自 C++11):分别用于 UTF-16 和 UTF-32 字符表示。char8_t(自 C++20):用于 UTF-8 字符表示,大小和对齐方式与unsigned char相同但为独立类型。
浮点型
- 标准浮点型 :
float:单精度浮点型(通常为 IEEE-754 binary32)。double:双精度浮点型(通常为 IEEE-754 binary64)。long double:扩展精度浮点型(例如 x86 的 80 位扩展精度,或部分平台的 128 位浮点格式)。
- 特殊浮点值 :
- 无穷大(Infinity,正负)、负零(
-0.0)、非数值(NaN)。
- 无穷大(Infinity,正负)、负零(
1.6 对象
- 对象包含的属性 :
- 大小 (可通过
sizeof确定) - 对齐要求 (可通过
alignof确定) - 存储期(自动、静态、动态、线程局部)
- 生存期(受存储期或临时性约束)
- 类型
- 值(例如默认初始化的非类类型可能是不确定的)
- 可选的名称
- 大小 (可通过
- 非对象实体 :值、引用、函数、枚举数、类型、非静态类成员、模板、类或函数模板特化、命名空间、参数包以及
this。 - 变量 :是通过声明引入的对象或引用(非非静态数据成员)。
1.6.1 对象创建
- 显式创建 :通过定义、
new表达式、throw表达式、改变联合体的活动成员,以及求值需要临时对象的表达式来显式创建。 - 隐式创建 :
- 某些隐式生存期类型的对象可以通过特定操作(如调用
operator new、std::malloc、std::memcpy、std::bit_cast、std::start_lifetime_as等)在指定的存储区域中被隐式创建。
- 某些隐式生存期类型的对象可以通过特定操作(如调用
1.6.2 对象表示与值表示
- 对象表示 :完整对象类型 T 的非位域完整对象在内存中占用的 N 个
unsigned char对象的序列(其中 N=sizeof(T))。 - 值表示 :对象表示中参与表示 T 值的比特集合。
- 填充位 :不属于值表示的比特。
- TriviallyCopyable 类型:对于此类类型,内存中的字节拷贝足以产生具有相同值的另一个对象(存在 trap 表示或重叠子对象等特例除外)。
1.6.3 子对象与大小、地址
- 子对象类别 :包括成员对象、基类子对象和数组元素。未被包含在其他对象中的对象称为完整对象 。
- 零大小与空基类优化 (EBO) :满足特定条件的类子对象(如无虚函数、无非零大小子对象的类)可以具有零大小。
- 地址与嵌套:非位域且非零大小的对象,其地址是它所占用的第一个字节的地址。对象可以嵌套在其他对象内部。
1.6.4 多态对象与严格别名
- 多态对象 :声明或继承了至少一个虚函数的类类型的对象。实现会在其中存储额外的信息(通常是一个指针指针),用于在运行时通过 RTTI(
dynamic_cast和typeid)及虚函数调用确定对象的动态类型。 - 严格别名 :使用与创建对象时不同类型的表达式去访问该对象,在许多情况下属于未定义行为(UB)。
1.6.5 对齐要求
- 对齐属性 :每个对象类型都有一个非负整数的对齐要求(类型为
std::size_t,且总是 2 的幂),代表该类型对象可分配的连续地址之间的字节数。 - 超对齐 :若类型的对齐要求比
std::max_align_t更严格(更大),则被称为具有扩展对齐要求的类型。
1.7 作用域
- 可见性:C++ 程序中的每个声明仅在某些可能不连续的作用域中可见。
- 作用域的引入 :除全局作用域包含整个程序外,其他每个作用域 S 都由以下项引入:
- 一个声明
- 参数列表中的参数
- 一个语句
- 一个异常处理程序
- 一个契约断言(自 C++26)
- 包含与嵌套 :作用域 S 总是出现在另一个包含它的作用域中。包含某程序点的最小作用域称为该点的即时作用域 。
- 属属关系 :
- 声明在其 locus(声明点)处的即时作用域中栖息(inhabit)。
- 声明的目标作用域即为其栖息的作用域。
- 实体属于某作用域,当且仅当该作用域是该实体某声明的目标作用域。
1.7.1 主要作用域类型
- 块作用域 :
- 由选择语句(
if、switch)、迭代语句(for、range-for、while、do-while)、异常处理程序或复合语句引入。 - 属于块作用域的变量称为块变量 。
- 若块作用域中的声明与父作用域中的声明潜在冲突,则程序通常为谬误
- 由选择语句(
- 函数参数作用域 :
- 由参数声明引入,涵盖参数列表及函数定义体/lambda 主体/推导指引/requires 表达式。
- Lambda 作用域 :
- 由 lambda 表达式引入,自
[captures]之后开始,延伸至{ body }结束。
- 由 lambda 表达式引入,自
- 命名空间作用域 :
- 由命名空间定义引入,包含该命名空间的所有定义及相关重定义/特化的延伸部分。全局作用域即为全局命名空间的命名空间作用域。
- 类作用域 :
- 由类或类模板的声明引入,包含类定义的成员规范
- 枚举作用域 :
- 由非不透明枚举声明引入,包含枚举项列表
- 模板参数作用域 :
- 由模板声明或模板模板参数引入,包含模板参数列表和 requires 子句。
1.7.2 声明点
名称在可见之前,其声明点(locus)决定了它何时进入作用域:
- 简单声明 :变量的 locus 在其声明符之后、初始化器之前。因此内层同名变量初始化时若直接使用变量名,获取的是自身未确定的值或外层变量(取决于位置)。
- 类/类模板声明:locus 在类头中命名该类的标识符之后。类名在基类列表中已经处于作用域中。
- 枚举说明符:locus 在命名枚举的标识符之后。
- 类型别名 :locus 在别名所指代的
type-id之后。 - 枚举项 :locus 在其定义之后(与变量不同,不在初始化器之前)。
- 注入类名 :locus 紧跟在其类定义的大括号
{之后。 - 范围
for循环与模板循环 :项目声明中变量的 locus 在range-initializer(范围初始化器)之后。
1.8 对象生命周期
- 核心概念 :对象和引用的生命周期是一个运行时属性。
- 对象生命周期开始的条件 :
- 获取了符合该类型大小和对齐要求的存储空间;
- 其初始化(若有)已完成(包括通过无构造函数或
trivial default constructor进行的默认初始化)。 - 特例:联合体成员或其子对象只有在被初始化或被显式激活时,其生命周期才开始。
- 对象生命周期结束的条件 :
- 非类类型对象被销毁(可能通过伪析构函数调用);
- 类类型对象的析构函数调用开始;
- 对象占用的存储空间被释放,或被未嵌套于其中的新对象所复用。
- 引用生命周期 :自其初始化完成时开始,其结束方式类似于标量对象(引用的生命周期可能短于被引用对象的生命周期,从而产生悬空引用 )。
1.8.1 临时对象生命周期与延长
- 临时对象的创建 :当 prvalue 被材质化(materialized)以用作 glvalue 时创建(例如绑定引用、初始化
std::initializer_list、函数返回 prvalue、转型等)。 - 销毁时机 :所有临时对象都会在词法包含其创建点的完整表达式 求值的最后一步被销毁。若创建了多个临时对象,其销毁顺序与创建顺序相反。
- 生命周期延长 :
- 将临时对象绑定到引用上可以延长其生命周期。
- 结构化绑定中创建的临时对象的生命周期会延长至该结构化绑定声明结束(自 C++17)。
- 范围
for循环的range-initializer中创建的临时对象,其生命周期会延长至循环体结束(自 C++23)。
1.8.2 存储复用与透明可替换性
- 平凡可析构性 :若对象是
trivially-destructible的,程序无需显式调用其析构函数即可结束其生命周期(但需注意正确行为可能依赖析构函数)。 - 透明可替换性 : 如果对象 x 满足以下条件,它可以被新对象 y 透明替换:
- y 的存储空间精确覆盖 x 原本占用的存储位置;
- y 与 x 具有相同的类型(忽略顶层 cv 限定符);
- x 不是完整的
const对象; - x 和 y 都不是基类子对象或带有
[[no_unique_address]]的成员子对象; - x 和 y 均为完整对象(或相应的直属子对象)。
std::launder:当不满足透明可替换性条件时,若要在原地址访问新对象,必须通过指针优化屏障std::launder获取有效指针。
1.8.3 存储提供
- 作为特殊情况,对象可以创建在
unsigned char或std::byte的数组中(此时该数组称为提供存储 ):- 数组的生命周期已开始且未结束;
- 新对象的存储完全容纳于数组之内;
- 数组中没有嵌套满足这些约束的其他数组对象。
1.8.4 生命周期之外的访问
- 在对象生命周期开始之前(但存储已分配)或结束之后(但存储未释放/复用),使用标识该对象的 glvalue 表达式进行以下操作属于未定义行为 :
- 左值到右值转换
- 访问非静态数据成员或调用非静态成员函数;
- 绑定引用到虚基类子对象;
dynamic_cast或typeid表达式。
1.9 定义与单定义规则(ODR)
- 定义 :是完全定义由该声明所引入的实体的声明。
- 非定义的常见声明 :
- 没有函数体的函数声明:
int f(int); - 无初始化器的
extern声明:extern const int a; - 类内部的非内联静态数据成员声明:
static int i; - 前向类声明:
struct S; - 枚举的不透明声明:
enum Color : int; - 模板参数声明、
typedef/using别名声明、using声明、静态断言等。
- 没有函数体的函数声明:
1.9.1 单定义规则
- 翻译单元级别 :在任何一个翻译单元中,任何变量、函数、类类型、枚举类型、概念(自 C++20)或模板只允许有一个定义。
- 程序级别 :整个程序中,每个被 ODR 使用 的非内联函数或变量必须且只能有一个定义。违反该规则会导致未定义行为(通常表现为链接错误)。
- 多翻译单元中的类型/内联定义要求:类类型、枚举类型、内联函数、内联变量和模板可以在多个翻译单元中各有一个定义,但必须满足严格的一致性条件(例如:由相同的记号序列组成、名字查找发现相同的实体、常量具有相同的值等)。否则程序谬误,无诊断要求。
1.9.2 ODR 使用
- 非正式定义 :
- 对象:如果其值被读取(除非是编译期常量)或被写入、其地址被获取、或者有引用绑定到它,则该对象被 odr-used。
- 引用:如果被使用且其所指代的对象在编译期未知,则被 odr-used。
- 函数:如果对其进行了函数调用或获取了其地址,则被 odr-used。
- 形式定义要点 :
- 若变量作为潜在结果表达式的一部分且应用了 lvalue-to-rvalue 转换(且该变量在编译期可用、非 volatile 且无 mutable 子对象),则可能不构成 odr-used(例如作为编译期常量直接代换)。
- 虚函数(非纯虚函数)由于构造虚表(vtable)的需求,默认被 odr-used。
1.10 名字查找
- 定义 :名字查找是编译器将程序中遇到的名字 与引入该名字的声明关联起来的过程。
- 后续流程 :
- 函数与函数模板 :名字查找可能会关联多个声明,并结合实参依赖查找(ADL)获取更多声明;随后经由模板实参推导和重载解析选出最终使用的声明。
- 其他名字(变量、命名空间、类等) :除非声明的是同一个实体,否则名字查找必须产生唯一的一个声明,否则无法通过编译。
1.10.1 "struct hack" / 类型与非类型隐藏
- 特例:在同一个作用域内,某个名字可以同时指向一个类/结构体/联合体/枚举类型(非 typedef),以及一个变量、非静态数据成员、枚举项,或者一组重载函数/函数模板。
- 结果 :此时不会报错,但类型名会被非类型名隐藏 (必须使用详尽类型说明符
elaborated type specifier才能访问被隐藏的类型)。
1.10.2 类型
名字查找主要分为两大类:
- 限定名查找 :
- 当名字紧跟在作用域解析运算符
::的右侧(或在::之后带有解歧义关键字template)时触发。
- 当名字紧跟在作用域解析运算符
- 非限定名查找 :
- 在不使用
::限定时触发。对于函数名,非限定名查找会自动包含实参依赖查找(ADL) 。
- 在不使用
1.10.3 限定名查找
-
定义 :位于作用域解析运算符
::右侧的名字称为限定名 。它可用于引用类成员、命名空间成员或枚举项。 -
全局强制查找 :若
::左侧为空,则查找仅 考虑全局命名空间中的声明,这可用于绕过局部同名变量的遮蔽:cint main() { struct std {}; std::cout << ::fail; // 错误:std 匹配到了局部 struct ::std::cout << ::ok; // 正常:::std 显式查找命名空间 std } -
左侧先行原则 :在对
::右侧进行名字查找之前,必须先完成对其左侧 名字的查找(除非使用decltype或左侧为空)。左侧查找仅限命名空间、类类型、枚举和模板。
声明符与析构函数中的查找
- 声明符中的上下文影响:当限定名用作声明符时,该限定名之后的其他名字将在该成员的类或命名空间作用域中进行查找,而之前的名字则在当前作用域查找。
- 析构函数/伪析构函数 :若
::之后紧跟~和标识符,则该标识符将在::左侧名字相同的作域中进行查找(即寻找对应类的成员析构函数或作用域内的类型)。
不同实体的限定查找规则
- 枚举项 :若左侧查找结果为枚举(带作用域或不带作用域),则右侧查找必须产生属于该枚举的枚举项,否则程序谬误。
- 类成员 :
- 左侧为类/结构体/联合体名时,右侧名字在类及其基类作用域中查找。
- 特例:模板参数中的名字在当前作用域查找;构造函数名字(若右侧与左侧同名)仅在特定声明或继承构造函数中表示构造函数,在其他上下文中(如详尽类型说明符)通过忽略函数来解析为注入类名
- 非虚分派 :通过限定名调用的成员函数绝不会进行虚函数分派(始终为静态绑定)。
- 命名空间成员 :
- 左侧为命名空间(或为空)时,在目标命名空间及其内联命名空间中递归查找;若未找到,则进一步沿用
using-directive(using 指令)进行传递查找。
- 左侧为命名空间(或为空)时,在目标命名空间及其内联命名空间中递归查找;若未找到,则进一步沿用
1.10.4 非限定名查找
- 定义 :不出现在作用域解析运算符
::右侧的名字称为非限定名 。 - 查找机制 :名字查找会由内向外、依次检查各个作用域 ,一旦在某一个作用域中找到至少一个任意类型的声明,查找立即停止,不再检查更外层的作域(称为"遮蔽/隐藏"效应)。
- Using 指令的影响 :通过
using指令引入的命名空间中的声明,表现得就像它们被声明在同时包含该using指令和目标命名空间的最近公共祖先命名空间中一样。
各上下文中的查找规则
- 文件作用域 :
- 在全局/顶层命名空间中,按代码书写的前后顺序,检查名字出现之前的全局作用域。
- 命名空间作用域 :
- 先检查当前用户自定义命名空间中名字出现之前的代码,随后依次向外检查外层命名空间,直至全局命名空间。
- 非成员函数定义 :
- 先由内向外检查函数体内部的嵌套代码块(直至函数体作用域);若未找到,再从该函数被定义(注意:不一定是声明)所在的位置开始,向外搜索其所属的命名空间及外层命名空间。
- 类定义与成员函数定义 :
- 在类定义中(除成员函数体、默认实参、异常规范及默认成员初始化器之外),查找顺序为:
- 当前类体中名字出现之前的代码;
- 基类的整个类体(会递归检查基类的基类);
- 若为嵌套类,则检查外层类的类体及外层类的基类;
- 若为局部类,则检查定义该类的块作用域;
- 命名空间作用域(直至全局)。
- 成员函数体内 :搜索范围扩展至整个类体(不仅限于名字出现之前的位置)。
- 在类定义中(除成员函数体、默认实参、异常规范及默认成员初始化器之外),查找顺序为:
- 虚继承的支配性原则 :
- 在多重虚继承中,子对象 B 中的成员名会隐藏基类 A 中的同名成员(前提是 A 是 B 的基类子对象)。C++11 引入了更完善的基于查找集和合并规则的处理机制,避免了非静态成员在菱形继承中不必要的二义性。
特殊上下文与语言特性
- 友元函数 :
- 类内定义:查找规则与成员函数相同。
- 类外定义:查找规则与普通命名空间函数相同。
- 友元声明(跨类) :若友元声明引用了另一个类的成员函数,若名字不属于模板参数,则首先在目标成员函数的类作用域中查找。
- 默认实参与构造函数初始化列表 :
- 函数参数名在检查外层块、类或命名空间之前优先被找到。
- 枚举声明 :
- 在枚举项的初始化器中,同一枚举中先前已声明的枚举项优先于外层作用域被找到。
- 模板定义中的双阶段查找 :
- 非依赖名 :在模板定义点进行查找并绑定,不随实例化点改变。
- 依赖名 :查找被推迟 到模板实例化点,非 ADL 查找检查定义点可见的声明,而 ADL 查找同时检查定义点和实例化点可见的具有外部链接的函数声明。
1.10.5 实参依赖查找
-
定义 :实参依赖查找(简称 ADL,又称 Koenig 查找)是在函数调用表达式(包括对重载运算符的隐式调用)中,用于查找非限定函数名的一套规则。
-
核心机制 :除了常规的非限定名查找所考察的作用域和命名空间外,ADL 会额外在其实参的命名空间和类中查找该函数名。
-
经典应用(如流操作符) :
cstd::cout << "Test\n"; // 全局命名空间中没有 operator<<,但 ADL 会检查左实参 std 所在的命名空间
触发条件与排除项
- 不触发 ADL 的情况 :若常规非限定名查找的结果包含以下任何一项,则不考虑 ADL:
- 类成员的声明;
- 块作用域中的函数声明(非常规
using声明); - 任何非函数或非函数模板的声明(例如与函数同名的变量或函数对象)。
关联命名空间与类 根据函数调用中各个实参的类型,ADL 会收集一个关联的类与命名空间集合:
- 基本类型 :除
std::meta::info(自 C++26)外,基本类型的关联集合为空。 - 类类型(含联合体) :包括类本身、所有直接和间接基类、其所属的外层类(若为嵌套类),以及这些类所在的最内层封闭命名空间。
- 类模板特化:除了类本身的规则外,还会检查所有类型模板实参,以及模板模板实参所在的命名空间和类。
- 枚举类型:包括枚举定义所在的最内层封闭命名空间(若为类成员枚举,则包含该类)。
- 指针/数组/函数/成员指针类型:会递归检查其底层的指向类型、参数类型、返回型或成员所属的类。
核心规则与惯用法
-
友元函数机制 :在类内部完全定义的友元函数或函数模板,可以通过 ADL 被找到,即使它在命名空间层级从未被显式声明过(通常用于实现泛型类关联的工具函数,如
gcd)。 -
泛型编程中的
swap惯用法 :arduinousing std::swap; swap(obj1, obj2);通过引入
std::swap并进行非限定调用,可以让编译器在常规查找std::swap的同时,通过 ADL 自动找到定义在对象所属命名空间中的自定义swap函数。标准库中的std::iter_swap等算法也广泛采用此模式。
1.11 "如同"规则(As-if Rule)
- 基本原则 :C++ 允许编译器对程序进行任何形式的代码变换 ,只要这些变换不会改变程序的可观察行为 。
- 编译器的自由度:只要在给定相同输入时,程序的实际可观察行为属于该输入所对应的合法行为之一,编译器就可以对其进行任意优化。
1.11.1 可观察行为的定义
在 C++ 中,以下内容属于严格受保护的"可观察行为",编译器不能随意破坏:
- Volatile 变量 :
- 在每个序列点,所有
volatile对象的值必须是稳定的。 - 对
volatile对象的读写必须严格遵循表达式的语义,不能 在同一线程内与其他volatile访问进行重排序。
- 在每个序列点,所有
- 文件与标准输出 :
- 程序终止时,写入文件的真实数据必须与"按代码原样执行"完全一致(自 C++26 起表述为向宿主环境交付的数据)。
- 提示性文本在发送到交互式设备时,必须在程序等待输入之前显示出来。
- 浮点环境 :
- 若
#pragma STDC FENV_ACCESS设为ON,则浮点异常和舍入模式的变化必须被正确观察到。不过,编译器允许对浮点表达式的中间结果采用不同的数值范围和精度(参见FLT_EVAL_METHOD),或者在未关闭FP_CONTRACT时按无限范围和精度计算。
- 若
1.11.2 例外情况
在特定情况下,编译器被允许打破"如同"规则:
- 复制消除 :编译器可以移除移动构造函数、拷贝构造函数以及对应临时对象的析构函数调用,即使这些调用包含可观察的副作用。
- New 表达式优化 :编译器可以移除对可替换分配函数的调用,即使开发者自定义了带有可观察副作用的替换版本(自 C++14)。
- 浮点异常计数与顺序:在不影响下一次浮点操作所观察到的状态的前提下,编译器可以改变浮点异常的触发次数和顺序(例如将循环执行多次的死代码优化为单次执行)。
1.11.3 未定义行为与新标准的"定义前缀"
- 传统未定义行为 (直到 C++26) :如果某输入会导致程序产生未定义行为(UB),编译器对该输入下的所有可观察行为均不作任何保证(即便某些可观察行为发生在 UB 之前)。
- C++26 的"定义前缀"新机制 :
- 程序中可以包含可观察检查点 。
- 如果某个操作在发生 UB 之前必定能到达某个可观察检查点,则该操作属于无 UB 操作 。
- 程序的定义前缀包含了所有无 UB 的操作。
- 自 C++26 起,编译器只需保证在触发 UB 的输入下,程序定义前缀的可观察行为符合规范即可。
1.12 未定义行为
- 未定义行为(Undefined Behavior, UB) :若违反了 C++ 标准的某些规则,整个程序将变得毫无意义,标准对此时的程序行为不做任何限制。
- C++ 标准中的程序行为分类 :
- Ill-formed(语法/语义错误,需诊断) :存在语法或可诊断语义错误,编译器必须发出诊断信息(即使它通过扩展赋予了代码某种含义)。
- Ill-formed, no diagnostic required (IFNDR) :存在语义错误(如违反 ODR),但通常难以在全局检测,执行时会导致未定义行为。
- Implementation-defined behavior(实现定义行为) :行为随编译器实现而异,但 conforming 的实现必须文档化 (例如
std::size_t的类型)。 - Unspecified behavior(未指明行为) :行为随实现而异,且实现不强制要求文档化(例如求值顺序)。
- Erroneous behavior(错误行为,自 C++26) :代码本身不正确,编译器被推荐进行诊断,并允许在操作后随时终止执行。
- Undefined behavior(未定义行为) :完全没有限制,编译器无需诊断,编译出的程序无需做任何有意义的事。
1.12.1 UB 与编译优化的关系
由于合法的 C++ 程序不应包含任何未定义行为,编译器在开启优化时会激进地假定代码中不存在 UB,这往往会导致意料之外的优化结果:
- 有符号整数溢出 :若假定无溢出,
x + 1 > x恒为真,可能被直接优化为返回1。 - 数组越界访问:若越界访问会导致 UB,编译器可能直接将包含越界的循环优化掉。
- 未初始化标量:读取未初始化变量会导致 UB,编译器可能直接利用该假设输出残留的寄存器值。
- 无效标量(如布尔值篡改) :通过
reinterpret_cast破坏bool规范值(如令其为 10)后再读取,会导致 UB,编译器可能直接优化掉条件判断。 - 空指针解引用:无条件空指针解引用具有 UB,之后的代码分支可能被直接裁剪。
std::realloc后的旧指针访问 :对已被传入realloc的原指针进行读写属于 UB。- 无副作用的无限循环(自 C++20 起) :不包含 I/O、volatile 访问或同步操作的纯计算无限循环在 C++ 中属于 UB,编译器可以将其当作"永远不会执行到后续代码"来优化。
1.13 内存模型
- 核心作用:C++ 内存模型为 C++ 抽象机器定义了计算机内存存储的语义。
- 内存结构 :C++ 程序可用的内存是一个或多个连续的字节序列 ,内存中的每个字节都拥有唯一的地址。
1.13.1 字节的定义
- 最小可寻址单元:字节是内存中最小的可寻址单元。
- 容量要求 :它被定义为能够容纳以下内容的连续位(bit)序列:
- 任意 UTF-8 编码单元的值(256 个不同值);
- 基本执行字符集的任意成员;
- 基本字面量字符集的任意元素的普通字面量编码(自 C++23)。
- 大小与类型 :
- 类似 C 语言,C++ 支持 8 位及更大尺寸的字节。
- 类型
char、unsigned char和signed char在存储和值表示上都占用一个字节。 - 字节中的位数可以通过宏
CHAR_BIT或std::numeric_limits<unsigned char>::digits获取。
1.13.2 内存位置
- 形式定义 :内存位置是指由以下对象的对象表示所占用的存储空间:
- 非位域的标量类型对象;
- 或者长度非零的最大连续位域序列。
- 补充说明:语言的各种特性(如引用和虚函数)可能会引入由编译器实现管理、但程序不可直接访问的额外内存位置。
多线程执行与数据竞争
- 执行线程 :程序中的控制流,由调用特定顶层函数(如
std::thread、std::async、std::jthread等)开始。 - 内存共享:程序中的任何线程理论上都可以访问任何对象和函数(包括具有自动或线程局部存储持续时间的对象的指针/引用)。
- 多线程实现:托管实现支持多线程并发运行;独立实现则由实现定义是否支持多线程。
数据竞争
- 冲突求值 :当两个表达式求值满足以下条件时发生冲突:
- 一个修改了内存位置(或启动/结束对象生命周期),另一个读取、修改该内存位置,或者操作重叠的存储。
- 数据竞争定义 :若程序包含两个冲突的求值,且满足以下所有 条件之一,则构成数据竞争 ,否则属于未定义行为(UB):
- 两个求值在同一个线程或同一个信号处理函数中执行;
- 两个冲突的求值都是原子操作(
std::atomic); - 其中一个冲突求值发生-在(happens-before)另一个求值。
- 容器特例 :
- 标准库中的所有容器(
std::vector<bool>除外)保证:在同一个容器的不同元素上进行并发修改不会导致数据竞争。 std::vector<bool>由于底层按位压缩,对其不同元素的并发修改可能会导致数据竞争(UB)。
- 标准库中的所有容器(
进展保证与前进自由度
- 进展保证 :有效 C++ 程序中的每个线程最终会执行特定操作之一(终止、yield、I/O、volatile 访问、原子/同步操作、或继续平凡无限循环)。
- 平凡无限循环 :
- 形如
for(;;);或while(true);的空循环体无限循环被定义为有定义的(自 P2809 提案起),编译器不能将其无条件移除。 - 若循环体内包含非空语句(如
for(;;) { int x; })且无副作用,则属于未定义行为。
- 形如
- 进展保证级别 :
- 并发进展保证 :只要未终止,就会在有限时间内取得进展,不依赖其他线程。
- 并行进展保证 :线程池常用模型,在执行首个关键步骤(I/O、原子操作等)之前不强制要求取得进展。
- 弱并行进展保证 :不保证单独取得进展,但支持通过前进进展保证委派 (如标准库并行算法)来阻塞并推进任务。
1.14 字符集与编码
分类
- 翻译字符集 :
- 自 C++23 起引入,包含 Unicode 码空间中分配了码位的所有抽象字符,以及未分配抽象字符的每个 Unicode 标量值的独特字符。
- 它是基本字符集和基本字面量字符集的超集。
- 基本字符集 :
- 包含特定数量的字符(C++26 之前为 96 个,自 C++26 起增至 99 个)。
- 包含大小写英文字母、数字
0-9、各种标点符号、空格以及控制符号(如制表符、换行符等)。 - 自 C++26 起,新增了美元符号
$、at 符号@以及反引号 `````。
- 基本字面量字符集 :
- 包含基本字符集的所有字符,外加额外的控制字符:空字符(Null)、响铃符(Bell)、退格符(Backspace)和回车符(CR)。
- 执行字符集 :
- 执行字符集和执行宽字符集是基本字面量字符集的超集,其具体编码和额外元素是由区域设置(Locale-specific)决定的。
- 执行宽字符集的每个元素必须能表示为一个独立的
wchar_t代码单元。
1.14.1 代码单元与字面量编码
- 代码单元 :字符类型的整数值。字符字面量或字符串字面量中的字符会根据编码前缀被编码为一个或多个代码单元(即字面量编码)。
- 基本字符的编码保证 :
- 无论是字面量编码还是区域设置编码,基本字面量字符集中的每个元素都必须被编码为单个非负值的代码单元,且彼此不同。
- 不在基本字面量字符集中的字符可以被编码为多个代码单元。
U+0000空字符(NULL)必须被编码为值0,且翻译字符集中的其他元素不能使用值为0的代码单元。- 十字面量数字(
0-9)的代码单元值必须是连续且严格递增的。
- 现代 UTF 编码:对于 UTF-8、UTF-16 或 UTF-32 字面量,每个字符对应的 UCS 标量值严格按照 ISO/IEC 10646 标准进行编码。
1.14.2 标准术语变更 (C++23 更新)
C++23(通过 P2314R4 提案)对部分核心字符集的标准名称进行了现代化修订:
- 新名称:基本字符集 ← 旧名称:基本源字符集
- 新名称:基本字面量字符集 ← 旧名称:基本执行字符集 / 基本执行宽字符集
1.15 翻译阶段
- 核心概念 :C++ 源代码文件通过编译器的处理最终生成可执行程序,整个概念模型由 9 个连续的翻译阶段 组成。
- 翻译单元 :源码经过预处理和词法分析后形成的整体。多个翻译单元可以独立编译,随后通过链接组合在一起。
- 预处理标记 :语言在第 3 到第 6 阶段的最小词法元素。主要类别包括:
- 头文件名
- 导入与模块指令占位符
- 标识符
- 预处理数字(可以容纳非法浮点/整型形态的宽泛记号,在成功转换为字面量之前没有类型和值)
- 字符字面量与字符串字面量
- 运算符与标点符号(包括代用标记)
最大贪婪匹配规则
- 核心定义 :在将输入解析为预处理标记时,编译器总是会优先将其识别为能够组成合法预处理标记的最长字符序列(即"maximal munch"原则),哪怕这会导致后续分析失败。
- 经典示例 :
int num1 = foo+++++bar;会被错误地拆解为foo++ ++ +bar,而不是期望的foo++ + ++bar。
- 例外情况 :
- 头文件名仅在
#include、__has_include或import指令特定位置形成。 - 避免将
<::误认为替代标记<:(例如std::vector<::Foo>)。 - 避免将连续的
>误判为>>(如嵌套模板右尖括号的处理)。
- 头文件名仅在
9个翻译阶段
- 阶段 1:映射源字符
- 将源码文件的单个字节映射为基本的源码字符集。操作系统相关的换行符统一转换为标准换行
- 自 C++23 起,强制确保支持 UTF-8 格式的输入文件,并对 UTF-8 编码进行解码。
- 阶段 2:拼接行
- 删除行尾的反斜杠 ``(后跟零个或多个非换行空白字符),将物理上的多行合并为一条逻辑源码行。
- 若非空源码文件结尾没有换行符,自动为其补上一个终止换行符。
- 阶段 3:词法分析
- 将源文件分解为预处理标记、注释和空白字符。
- 注释被替换为一个空格,通用字符名(UCN)在此阶段被解析替换。
- 阶段 4:预处理
- 执行预处理器指令(如
#include、#define等)。通过#include引入的文件会递归地经历阶段 1 到 4。 - 预处理结束后,所有预处理器指令将被从源码中移除。
- 执行预处理器指令(如
- 阶段 5:确定通用字符串字面量编码
- 将字符和字符串字面量中的字符从源码字符集转换为对应的字面量编码(如 UTF-8 等)。
- 展开转义序列和通用字符名。
- 阶段 6:拼接字符串字面量
- 将相邻的多个字符串字面量拼接为一个。
- 阶段 7:编译
- 将预处理标记转换为正式的语言 Token。
- 进行语法和语义分析,将代码翻译为翻译单元对应的目标代码。
- 阶段 8:实例化模板
- 检查翻译单元,找出所有需要实例化的模板(包括隐式和显式请求),定位模板定义并执行实例化,生成实例化单元
- 阶段 9:链接
- 收集所有的翻译单元、实例化单元以及满足外部引用所需的库组件,最终打包成可执行的程序映像
1.16 主函数
在托管环境(即拥有操作系统的环境)中,C++ 程序必须包含一个名为 main 的全局函数作为指定的程序入口点。它具有以下几种标准形式:
int main() { body }:不依赖环境传入参数的main函数。int main(int argc, char* argv[]) { body }:接受环境传入参数的main函数。- 参数名称(
argc和argv)以及参数类型表示形式是任意的(例如int main(int ac, char** av)同样有效)。
- 参数名称(
int main(/* implementation-defined */) { body }:由实现定义的其他返回int的形式(标准建议将额外的可选参数放在argv之后)。
- 参数详解 :
argc:非负值,表示从运行环境中传递给程序的参数个数。argv:指向一个包含argc + 1个指针的数组首元素的指针。其中最后一个元素(argv[argc])保证为null指针,而前面的指针指向表示传入参数的以空字符结尾的多字节字符串。argv[0]:若非空(即argc > 0),则指向用于调用程序的名字或空字符串""。
1.16.1 特殊属性与行为
- 隐式返回
0:main函数的函数体不需要包含return语句。如果控制流到达main结尾而没有遇到return,其效果等同于执行了return 0;。 - 程序终止流程 :执行
return(或到达结尾的隐式返回)等价于:- 正常离开函数(销毁所有具有自动存储持续时间的局部对象,并评估
main的后置条件断言------自 C++26 起); - 以
return的返回值作为参数调用std::exit(随后std::exit会销毁静态对象并终止程序)。
- 正常离开函数(销毁所有具有自动存储持续时间的局部对象,并评估
1.16.2 严格限制
若违反以下限制,程序将变为非良构 :
- 禁止在程序中被命名 :
- 不能被递归调用。
- 不能获取其地址。
- 不能用于
typeid表达式或decltype说明符(自 C++11)。
- 禁止重载与预定义 :全局命名空间中的名字
main专属于该函数,不能被重载,且不能带有任何除"C++"之外的语言链接。 - 修饰符限制 :不能被声明为
constexpr(自 C++11)、consteval(自 C++20)、inline或static。 - 其他限制 :
- 不能被定义为删除的(
= delete,自 C++11)。 - 返回类型不能被推导(不允许
auto main(),自 C++14)。 - 不能是协程(Coroutine)。
- 不能依附于具名模块(Named module,自 C++20)。
- 不能被定义为删除的(
1.17 模块
- 模块 :C++20 引入的用于在不同翻译单元之间共享声明和定义的语言特性,旨在替代传统头文件的部分用例。
- 与命名空间的关系:模块与命名空间正交(完全独立)。
- 核心组成 :
- 模块单元 :带有模块声明的翻译单元。
- 具名模块 :具有相同模块名称的所有模块单元的集合。
- 主模块接口单元 :每个具名模块必须有且仅有一个不指定模块分区的模块接口单元,其导出的内容会在导入该模块时对外可见。
1.17.1 语法结构与核心关键字
- 模块声明 :
export(optional) module module-name module-partition(optional) attr(optional) ;- 必须是翻译单元中的第一个声明(全局模块片段除外)。
- 模块名称由一个或多个用点(
.)分隔的标识符组成(如mymodule.mysubmodule),点仅用于逻辑上的层次表示,没有内在语法含义。
- 导出声明 :
export declaration或export { declaration-seq }- 只有在模块接口单元中才能导出声明和定义。
- 导入声明 :
export(optional) import module-name / module-partition / header-name ;- 用于引入其他模块、模块分区或头文件单元。
1.17.2 模块的分类与高级片段
- 接口单元 vs 实现单元 :
- 模块接口单元 :带有
export关键字的模块单元,负责对外暴露接口。 - 模块实现单元 :不带
export的模块单元,仅用于实现内部逻辑。
- 模块接口单元 :带有
- 全局模块片段 :
- 以
module;开头,允许在模块单元中引入传统的#include指令(主要用于处理依赖宏配置的旧头文件)。 - 必须位于模块声明之前。
- 以
- 私有模块片段 :
- 以
module : private;开头,允许将模块写在单个翻译单元中,同时隐藏实现细节,防止其对导入者可见。
- 以
- 模块分区 :
- 模块内部的子单元,以冒号
:开头(如export module A:B;)。 - 仅在具名模块内部可见,不能被外部直接导入,必须由主模块接口单元通过
export import统一导出。
- 模块内部的子单元,以冒号
1.17.3 模块所有权与链接性
- 依附性:在模块声明之后出现的声明默认依附于该命名模块。
- 模块链接 :依附于具名模块且未被导出的声明具有"模块链接",只能在该模块内部访问。
- 例外情况 :带有外部链接的命名空间定义以及语言链接规范(如
extern "C++")内部的某些声明可以不依附于任何具名模块,允许在传统源文件中定义。
1.18 契约断言
- 定义:C++26 引入的契约断言允许程序员在执行过程中的特定点指定程序状态的预期属性。
- 引入方式 :通过函数契约说明符 (
pre、post)和contract_assert语句引入,每个契约断言都有一个布尔类型的谓词 。
1.18.1 评估语义
契约断言的评估采用以下四种语义之一(具体由实现定义,且同一断言在不同求值中可采用不同语义):
| 评估语义 | 是否为检查语义 | 是否为终止语义 |
|---|---|---|
| ignore(忽略) | 否 | 否 |
| observe(观察) | 是 | 否 |
| enforce(强制) | 是 | 是 |
| quick-enforce(快速强制) | 是 | 是 |
- 契约违反 的触发条件:
- 谓词评估结果为
false; - 谓词评估以异常退出;
- 在明显常量求值上下文中,谓词不是核心常量表达式。
- 谓词评估结果为
1.18.2 契约违反的处理与处理器
-
常量求值中 :
observe语义下产生诊断信息。- 终止语义下程序变为非良构。
-
常规运行中 :
-
quick-enforce语义下程序直接契约终止 。 -
enforce或observe语义下,会调用全局的契约违反处理器 :cvoid handle_contract_violation( std::contracts::contract_violation );
-
-
处理器返回后的行为 :
- 若评估语义为
observe,控制流正常继续。 - 若评估语义为
enforce,程序触发契约终止。
- 若评估语义为
1.19 拼接说明符
- 定义 :C++26 引入的反射与元编程特性,允许从反射值 (类型为
std::meta::info的转换常量表达式)中生成语言构造,即"将反射拼接回源代码中"。 - 语法形式 :
[: constant-expression :]:通用拼接说明符,具体含义取决于上下文。typename [: constant-expression :]:作为类型说明符的拼接。template [: constant-expression :]:指定模板的拼接(若不作为表达式出现,后跟模板实参列表尖括号<>,并作为作用域解析运算符::的左操作数)。
1.19.1 表达式上下文中的拼接
- 规则限制 :
- 不能跟随在
typename关键字之后,也不能后跟作用域解析运算符::。 - 未加括号的拼接表达式不能作为模板实参。
- 可指代函数、对象、数据成员、变量、结构化绑定、值或枚举值,但不能指代构造函数、析构函数、未命名的位域或会被 lambda 捕获的局部实体。
- 不能跟随在
- 存储期与可见性 :
- 指代变量或结构化绑定的拼接表达式,仅当该变量具有静态/线程存储期或在包围该表达式的作用域中声明时才有效。
- 非静态成员与基类访问 :
- 指代非静态数据成员或隐式对象成员函数的拼接,仅能用作类成员访问表达式的第二操作数、取地址运算符的操作数以形成成员指针,或在未求值操作数中使用。
- 拼接表达式
[:r:]不会 隐式转换为this->[:r:]。 - 可通过拼接描述直接基类关系(如
std::meta::bases_of的元素),用以访问可能存在歧义或不可访问的基类子对象。 - 指代函数模板或变量模板时,必须 使用
template关键字。
1.19.2 类型上下文中的拼接
- 作为类型说明符的拼接不能后跟
::,且必须前置typename或者是处于纯类型上下文中。 - 能力与演绎 :
- 可指代类型、类模板或别名模板。
- 若指代模板且无模板实参列表,则受类模板实参演绎(CTAD)的约束。
- 可在类型要求(如概念约束中)中使用。
1.19.3 作用域解析与命名空间
- 作用域要求 :作为
::左操作数的拼接若带模板实参列表,必须前置typename或template。拼接必须指代类、枚举类型或命名空间。 - 命名空间应用 :在命名空间别名定义或
using指令中,[:r:]必须指代一个非全局命名空间(或命名空间别名)。
2. 关键字
- 核心定义 :C++ 中预留的保留字,由于语言本身需要使用,这些关键字不能被用作重定义或重载。
- 属性例外:作为特例,它们在属性内部(不含属性参数列表)不被视为保留字(自 C++11 起)。
- 保留标识符规则 :
- 任何包含双下划线
__的标识符、以及以"下划线加大写字母"开头的标识符永远保留。 - 所有以单下划线开头的标识符在全局命名空间中保留作为名字使用。
- 命名空间
std专用于标准 C++ 库,posix预留为未来的顶层命名空间。
- 任何包含双下划线
2.1 全景清单(按字母序)
- A 组 :
alignas(C++11)、alignof(C++11)、and(A)、and_eq(A)、asm、atomic_cancel(TM TS)、atomic_commit(TM TS)、atomic_noexcept(TM TS)、auto(1, 3, 4, 5) - B - C 组 :
bitand(A)、bitor(A)、bool、break、case、catch、char、char8_t(C++20)、char16_t(C++11)、char32_t(C++11)、class(1)、compl(A)、concept(C++20)、const、consteval(C++20, 5)、constexpr(C++11, 3)、constinit(C++20)、const_cast、continue、contract_assert(C++26)、co_await(C++20)、co_return(C++20)、co_yield(C++20) - D - G 组 :
decltype(C++11, 2)、default(1)、delete(1)、do、double、dynamic_cast、else、enum(1)、explicit、export(1, 4)、extern(1)、false、float、for(1)、friend、goto - I - P 组 :
if(3, 5)、inline(1, 3)、int(1)、long、mutable(1)、namespace、new、noexcept(C++11)、not(A)、not_eq(A)、nullptr(C++11)、operator(1)、or(A)、or_eq(A)、private(4)、protected、public - R - S 组 :
reflexpr(Reflection TS)、register(3)、reinterpret_cast、requires(C++20)、return、short、signed、sizeof(1)、static、static_assert(C++11)、static_cast、struct(1)、switch、synchronized(TM TS) - T - Z 组 :
template、this(5)、thread_local(C++11)、throw(3, 4)、true、try、typedef、typeid、typename(3, 4)、union、unsigned、using(1, 4)、virtual、void、volatile、wchar_t、while、xor(A)、xor_eq(A)
(注:括号中的数字对应 C++ 各标准版本中含义变更或新增的说明)
- (A) --- 替代表示
- (1) --- 在 C++11 中含义改变或新增含义
- (2) --- 在 C++14 中新增含义
- (3) --- 在 C++17 中含义改变或新增含义
- (4) --- 在 C++20 中含义改变或新增含义
- (5) --- 在 C++23 中新增含义
2.2 具特殊含义的标识符与预处理符号
- 具特殊含义的标识符 (可在特定上下文中用作对象或函数名):
final(C++11)、override(C++11)transaction_safe/transaction_safe_dynamic(TM TS)import/module(C++20)pre/post(C++26 契约断言)
- 预处理器指令识别的符号 :
- 包含常规指令:
if,elif,else,endif,ifdef,ifndef,elifdef(C++23),elifndef(C++23),define,undef,include,embed(C++26),line,error,warning(C++23),pragma,defined - 包含内省/检查符:
__has_include(C++17),__has_cpp_attribute(C++20),__has_embed(C++26) - 模块相关预处理符:
export,import,module(C++20)
- 包含常规指令:
- 指令外部识别的符号 :
_Pragma(C++11)
3. 预处理器
- 执行时机:预处理器在翻译阶段 4执行,发生在实际编译之前。
- 输出结果:预处理的最终产物是一个单一的文件,随后被传递给真正的编译器进行编译。
3.1 预处理指令格式与规则
- 基本格式 :每条指令独占一行,由以下部分组成:
- 一个
#字符; - 标准定义指令名及对应参数,或者一串不属于标准指令的预处理标记(属于条件支持的实现定义扩展,如
#warning在 C++23 前为常见非标准扩展),或留空(无效果); - 一个换行符。
- 一个
- 特殊规则 :
- 自 C++20 起,
module和import指令也被视作预处理指令。 - 预处理指令绝不能来自宏展开 (例如
#define EMPTY之后写EMPTY # include <file.h>不会被识别为预处理指令)。
- 自 C++20 起,
3.2 预处理器核心功能与能力
预处理器主要提供以下源文件翻译与控制能力:
- 条件编译 :由
#if、#ifdef、#ifndef、#else、#elif、#elifdef(自 C++23)、#elifndef(自 C++23)和#endif控制。 - 文本宏替换与操作 :替换文本宏,并可结合
#(字符串化)和##(记号粘贴)运算符,由#define和#undef控制。 - 文件包含 :由
#include指令控制,并可通过__has_include(自 C++17)进行检查。 - 编译期诊断 :通过
#error触发错误,或通过#warning(自 C++23)触发编译警告。 - 实现定义行为控制 :由
#pragma指令、_Pragma运算符(自 C++11)以及部分编译器支持的非标准扩展__pragma控制。 - 行号与文件名管理 :可重置预处理器可用的文件名与行号信息,由
#line指令控制。
3.3 条件包含
预处理器支持对源文件的部分内容进行条件包含,该行为由以下指令控制:
#if constant-expression:若常量表达式求值为非零值,则包含其控制的代码块。#ifdef identifier:等价于#if defined identifier。#ifndef identifier:等价于#if !defined identifier。#elif constant-expression:当前面的#if和所有介入的#elif块被跳过,且当前常量表达式求值为非零值时包含。#elifdef identifier(自 C++23) :等价于#elif defined identifier。#elifndef identifier(自 C++23) :等价于#elif !defined identifier。#else:当前面的#if和介入的#elif块全部被跳过时,无条件包含。#endif:终止当前的#if、#elif或#else块。
3.3.1 测试条件与专用表达式
常量表达式中可以包含以下预处理专用表达式:
defined表达式 :检测某个标识符当前是否被定义为宏名(支持defined identifier或defined(identifier)形式)。__has_include(自 C++17) :检测头文件或源文件是否存在__has_cpp_attribute(自 C++20) :检测给定的属性标记是否受支持及其支持的版本。__has_embed(自 C++26) :检测资源是否可用于嵌入。
3.3.2 求值规则与宏展开
- 宏展开 :除了
defined表达式中的标识符外,常量表达式中的宏会在条件求值之前被展开。- 注意 :若宏展开生成了
defined预处理标记,或defined运算符的使用不符合规范,在 C++26 及以后会导致程序非良构 (C++26 之前为未定义行为 UB)。
- 注意 :若宏展开生成了
- 未定义标识符处理 :在所有宏展开和预处理专用表达式求值后,所有剩余的非
true/false标识符 都会被替换为预处理数字0。 - 整数类型重映射 :在条件求值和标记转换期间,所有有符号整数类型表现得如同具有
std::intmax_t的表示,所有无符号整数类型表现得如同具有std::uintmax_t的表示(自 C++11 起)。
3.4 文本宏替换
文本宏替换由预处理器支持,包括对象式宏和函数式宏。支持的指令如下:
#define identifier replacement-list------ 对象式宏(Object-like macro)#define identifier(parameters) replacement-list------ 简单函数式宏(Function-like macro)#define identifier(parameters, ...) replacement-list(自 C++11) ------ 带可变参数的函数式宏#define identifier(...) replacement-list(自 C++11) ------ 仅含可变参数的函数式宏#undef identifier------ 取消宏定义
3.4.1 宏定义与替换规则
- 重复定义限制 :若标识符已被其他
#define定义为宏,除非两个定义完全相同,否则程序非良构。 - 保留与限制 :若标识符为预定义宏名或
defined,在 C++26 起程序为非良构(此前为未定义行为 UB)。 - 可变参数与
__VA_OPT__(自 C++20) :- 可变参数可通过
__VA_ARGS__访问。 __VA_OPT__(content)序列会在__VA_ARGS__非空时被替换为content,为空时展开为空白。
- 可变参数可通过
- 扫描与防递归 :预处理器在扫描时会跟踪被替换的宏,标记其为"待忽略",从而有效防止无限递归。
3.4.2 # 与 ## 运算符
#(字符串化运算符) :- 仅在函数式宏中使用,置于标识符前会将其展开后的记号序列转换为字符字符串字面量。
- 当
#位于__VA_ARGS__之前时,整个展开后的可变参数会被整体加引号包围。
##(记号粘贴运算符) :- 位于替换列表中的两个连续标识符之间,不经宏展开直接将其结果拼接为一个单一的预处理标记。
- 拼接结果必须是合法的预处理标记(C++26 起若非法则程序非良构)。
3.4.3 预定义宏
每个翻译单元中默认预定义的宏包括:
__cplusplus:表示使用的 C++ 标准版本(如 C++20 为202002L,C++23 为202302L,C++26 为202603L)。__STDC_HOSTED__(自 C++11) :宿主环境为1,独立环境为0。__FILE__与__LINE__:分别表示当前文件名和物理行号(可被#line修改)。__DATE__与__TIME__:表示编译的日期与时间。__STDCPP_DEFAULT_NEW_ALIGNMENT__(自 C++17) :非对齐感知operator new保证的对齐值。__STDCPP_BFLOAT16_T__/__STDCPP_FLOAT16_T__等 (自 C++23) :扩展浮点数支持检测。__STDC_EMBED_NOT_FOUND__/FOUND/EMPTY(自 C++26) :支持二进制文件嵌入(#embed)的状态指示。
3.5 源文件包含
源文件包含指令用于在当前文件的指令行之后直接引入其他文件的全部内容。支持的语法形式如下:
#include < h-char-sequence >:搜索标准头文件,并将其完整内容替换指令。#include " q-char-sequence ":搜索用户/本地源文件,失败时可回退到形式1。#include pp-tokens:若前两者不匹配,则对预处理标记(pp-tokens)进行宏展开,展开后再尝试匹配1或2。__has_include ( " q-char-sequence " )/__has_include ( < h-char-sequence > )(自 C++17) :检测头文件或源文件是否存在。__has_include ( string-literal )/__has_include ( < h-pp-tokens > )(自 C++17) :若4未匹配,则对其中的标记进行宏展开后再次尝试匹配。
3.5.1 搜索规则与行为细节
- 尖括号 vs 双引号 :
- 形式1通常仅搜索标准包含目录(由编译器选项控制)。
- 形式2通常首先搜索当前文件所在的目录,若未找到或不支持则回退到形式 1的搜索路径。
- 计算与展开错误 :
- 形式3中若宏展开后的结果仍不符合前两种形式,在 C++26 及以后会导致程序非良构 (C++26 之前为未定义行为 UB)。
__has_include表达式 :- 若文件存在且搜索成功,表达式求值为
1;若失败,求值为0。 - 可在
#if和#elif表达式中展开,并被视作已定义的宏(可用于#ifdef等),但不能在其他地方使用。
- 若文件存在且搜索成功,表达式求值为
- 模块转换 (自 C++20) :
- 若被包含的头文件属于可导入头文件,实现可以自行决定是否将
#include指令直接替换为对应的import header-name ;指令。
- 若被包含的头文件属于可导入头文件,实现可以自行决定是否将
3.5.2 常见惯用法与注意事项
- 防止重复包含 :由于文件包含可能导致递归或重复展开,通常使用头文件卫士(如
#ifndef FOO_H...#define FOO_H...#endif)或非标准但广泛支持的#pragma once来避免重复处理。 __has_include的局限性 :返回1仅代表文件存在,并不保证将其包含时不会引发编译错误(例如在旧语言标准模式下引入新特性头文件)。
3.6 实现定义的行为控制
实现定义的行为主要由 #pragma 指令控制:
#pragma pragma-params------ 以实现定义的方式表现。_Pragma(string-literal)(自 C++11) ------ 剥离字符串字面量的L前缀、外部引号及首尾空白,将"转为"、\转为 ``,然后进行标记化(同翻译阶段 3),最后作为#pragma的输入来处理。
3.6.1 指令概述与非标准
- 基本作用:控制编译器的特定实现行为(如禁用警告或更改对齐要求)。所有不认识的 pragma 都会被忽略。
- 常用非标准 Pragma :ISO C++ 标准不强制要求编译器支持任何 pragma,但有几个非标准 pragma 得到广泛支持:
#pragma STDC:源自 C 标准,部分 C++ 编译器前端在不同程度上予以支持:FENV_ACCESS:指示程序将访问或修改浮点环境,禁止可能破坏标志测试和模式更改的优化。默认值通常为OFF。FP_CONTRACT:允许对浮点表达式进行缩减(例如将(x * y) + z融合成单条 FMA 指令)。默认值通常为ON。CX_LIMITED_RANGE:指示复数运算(乘、除、绝对值)可使用简化的数学公式,允许中间过程溢出。默认值通常为OFF。 (注:上述三个 pragma 仅允许出现在所有外部声明之外,或复合语句内所有显式声明和语句之前。)
#pragma once- 作用:非标准但极大多数现代编译器支持的指令。若置于头文件中,确保该文件在同一个源文件中(直接或间接)仅被解析一次。
- 优缺点 :相比传统的头文件卫士 ,它不会发生宏名冲突;但由于它是基于文件系统层面的身份来排除文件的,因此无法防范项目多处存在同名头文件的情况。
#pragma pack- 作用 :控制随后定义的类和联合体成员的最大对齐字节数(
arg须为 2 的小幂次)。 - 操作家族 :包括设置对齐(
pack(arg))、恢复默认(pack())、入栈(pack(push)/pack(push, arg))以及出栈恢复(pack(pop))。它可能会减小类的对齐要求,但无法将其变为超对齐
- 作用 :控制随后定义的类和联合体成员的最大对齐字节数(
3.7 文件名与行号信息控制
#line 指令用于在预处理器中更改源代码的行号以及(可选的)当前文件名。支持的语法形式如下:
#line lineno new-line------ 仅更改当前预处理器的行号为lineno。#line lineno "filename" new-line------ 同时更改当前预处理器的行号为lineno并将文件名更改为"filename"。#line pp-tokens new-line------ 若前两者不匹配,则对预处理标记(pp-tokens)进行宏展开,展开后再尝试匹配1或2。
3.7.1 核心规则与行为细节
- 行号范围 :
lineno必须是由十进制数字组成的序列(自 C++14 起支持在数字间插入单引号'作为分隔符)。- 若指定的十进制数
lineno为0或超过上限(C++11 之前为32767,自 C++11 起为2147483647),在 C++26 及以后会导致程序非良构 (C++26 之前为未定义行为 UB)。
- 宏展开影响 :
#line指令之后的__LINE__宏展开将从指定的lineno开始计算,加上自此之后遇到的实际源代码行数递增。- 若提供了
filename,此后的__FILE__宏展开将生成该文件名。
3.7.2 常见应用场景
- 代码生成工具 :该指令常被从其他语言生成 C++ 源代码的自动代码生成工具使用。生成的 C++ 文件中会插入
#line指令,以便将编译错误和断言(如assert失败输出)正确追溯到人类可编辑的原始源文件的行号与文件名。
3.8 诊断指令
诊断指令用于在预处理阶段输出错误或警告信息。支持的语法形式如下:
#error diagnostic-message------ 显示给定的错误消息并使程序非良构(终止编译)。#warning diagnostic-message(自 C++23) ------ 显示给定的警告消息,但不影响程序的有效性(编译继续进行)。
3.8.1 核心规则与行为细节
- 诊断消息 :可以由多个单词组成,通常不需要加引号(加了引号也会被当作文本的一部分打印)。
#error指令 :- 遇到该指令时,编译器会显示诊断信息并直接终止编译,使整个翻译单元渲染为非良构
- 常见于条件编译中,用于检查不满足的编译前置条件(如平台不匹配、标准版本过低等)。
#warning指令 :- 自 C++23 起被正式纳为标准特性(在此之前,它作为合规扩展已被各大编译器广泛支持多年)。
- 与
#error不同,它仅向开发者发出警告提示,不会中断编译流程,也不影响程序的有效性。
4 表达式
表达式是由运算符 和操作数 构成的序列,用于指定具体的计算过程。表达式的求值可能会产生结果 或产生副作用;每个 C++ 表达式都具有两个独立的属性:
- 类型
- 值类别 :包括
lvalue、rvalue、glvalue、prvalue以及自 C++11 起引入的xvalue。
4.1 主表达式
任何运算符的操作数都可以是其他表达式或主表达式。主表达式包括:
this指针- 字面量
- 标识符表达式(包括未限定标识符、限定标识符及声明中的标识符)
- 括号表达式(任何带括号的表达式都被视为原声主表达式,其优先级高于任何运算符,且完美保留原表达式的类型、值和值类别)
- 扩展特性(如自 C++11 起的 lambda 表达式,自 C++17 起的折叠表达式,自 C++20 起的 requires 表达式,以及自 C++26 起的包索引和拼接表达式等)
4.2 求值与求值上下文
- 全表达式 :不作为任何其他表达式的子表达式、且本身不属于其他全表达式一部分的表达式(或特定语境如常量表达式、声明中的初始化器等)。
- 潜在求值表达式 :默认情况下表达式都会被求值,除非它们是未求值操作数或其子表达式。
- 未求值操作数 :在编译期不被实际求值的操作数,包括:
sizeof运算符的操作数typeid运算符的应用对象(多态类类型的左值除外)noexcept运算符的操作数decltype说明符的操作数- 概念定义中的约束表达式、
requires子句及requires表达式中的要求序列 - 自 C++26 起反射运算符
(^^)的操作数
- 弃值表达式 :仅用于获取其副作用的表达式,计算出的值会被丢弃(例如表达式语句的全表达式、内置逗号运算符的左操作数、转换为
void类型的强制转换操作数)。
4.3 值类别
C++ 中的每个表达式都由两个独立属性表征:类型 和值类别 。非引用类型的表达式必定属于以下三种主要值类别之一:
prvalue(纯右值) :计算内置运算符的操作数的值,或者初始化一个对象(该对象被称为结果对象 ,如变量、new分配的对象或临时对象)。xvalue(将亡值) :指代其资源可以被重用的对象的广义左值。lvalue(左值) :确定对象或函数身份、且不属于将亡值的广义左值。
此外还有两个组合值类别:
glvalue(广义左值) :表达式的求值可确定对象或函数的身份(lvalue或xvalue)。rvalue(右值) :可被移动的表达式(prvalue或xvalue)。
4.3.1 主要值类别详解与判定
- 左值
- 特征:具备身份,不可被常规移动。
- 典型实例 :
- 变量名、函数名、数据成员名(例如
std::cin、std::endl,即使变量类型为右值引用,其名称本身也是左值表达式)。 - 返回左值引用的函数调用或重载运算符(如
std::getline、std::cout << 1、++it)。 - 内置赋值和复合赋值表达式(如
a = b、a += b)。 - 内置前自增/前自减表达式(如
++a、--a)。 - 内置间接寻址表达式
*p、下标表达式a[n]。 - 字符串字面量(如
"Hello, world!")。 - 转换为左值引用类型的强制类型转换(如
static_cast<int&>(x))。
- 变量名、函数名、数据成员名(例如
- 纯右值
- 特征:无身份,通常用于初始化结果对象或计算字面值。
- 典型实例 :
- 字面量(字符串字面量除外),如
42、true、nullptr。 - 返回非引用类型的函数调用或重载运算符(如
str.substr(1, 2)、str1 + str2、it++)。 - 内置后自增/后自减(
a++、a--)、算术、逻辑及比较表达式。 - 取地址表达式
&a。 - 转换为非引用类型的强制类型转换(如
static_cast<double>(x)、std::string{})。 this指针、枚举器、lambda 表达式、requires 表达式、概念特化。
- 字面量(字符串字面量除外),如
- 将亡值
- 特征:具备身份,且资源可被重用(通常临近生命周期结束或显式转为右值引用)。
- 典型实例 :
- 返回右值引用到对象的函数调用或重载运算符(如
std::move(x))。 - 转换为右值引用到对象类型的强制类型转换(如
static_cast<char&&>(x))。 - 临时对象经过临时物质化后产生的表达式。
- 满足移动资格的表达式(自 C++23 起统一归为右值)。
- 返回右值引用到对象的函数调用或重载运算符(如
4.3.2 特殊类别与重要机制
- 待决成员函数调用 :如
a.mf或p->mf,归为prvalue,但语法上极为受限,仅能直接作为函数调用运算符的左侧参数(如(p->*pmf)(args))。 - Void 表达式 :返回
void的函数调用、转换为void的强转以及throw表达式,归为prvalue但没有结果对象,不能用于初始化引用或作为函数参数,仅可用于弃值上下文或return void;。 - 位域 :标志位域的表达式是
glvalue,可作赋值左侧操作数,但无法取地址,也不能直接绑定非常量左值引用。 - 移动资格表达式 :出现在
return、co_return或throw中的局部变量或形参名称,在参与重载解析时会被当作右值处理,从而能够优先匹配移动构造函数。
4.4 求值顺序
在 C++ 中,表达式任何部分的求值顺序(包括函数参数的求值顺序)通常都是未指定的 。编译器可以按任何顺序求值操作数和子表达式,且在同一表达式再次求值时可能采用不同的顺序。
注意 :求值顺序与运算符的结合性 或优先级是完全独立的概念。
自 C++11 起,标准引入了顺序前于关系(替代了 C++11 前的"序列点概念):
- Sequenced before(顺序前于) :若 A 顺序前于 B,则 A 的求值必定在 B 的求值开始前完全结束。
- Unsequenced(无序/未排序) : A 与 B 可以按任意顺序执行,且指令可以交错/重叠(Interleaved)执行。
- Indeterminately sequenced(顺序未定) : A 与 B 可以按任意顺序执行,但不可重叠(一个必须在另一个开始前完全结束)。
4.4.1 顺序前于规则
- 基础与函数调用
- 全表达式:每个全表达式的求值均顺序前于下一个全表达式。
- 函数调用与实参 :
- C++17 强化 :在函数调用表达式中,指定函数的表达式(后缀表达式)顺序前于每一个参数表达式。
- 参数间关系:不同实参的求值与副作用之间是顺序未定的(即不能交错,但顺序任意)。
- 函数体执行 :所有实参及函数名称表达式的求值,均顺序前于函数体内部的任何表达式或语句。
- 常见运算符的顺序保证
- 逻辑与/或、逗号运算符 :内置的
&&、||和,运算符中,左操作数顺序前于右操作数(具备短路求值特性)。 - 三元条件运算符 :
a ? b : c中,第一个操作数a顺序前于第二个或第三个操作数。 - 赋值运算符 :在
E1 = E2或E1 @= E2中,E2(右操作数)的求值顺序前于E1(左操作数) ;且左右操作数的求值均顺序前于赋值本身的副作用(修改内存)。 - 下标与成员指针 :在
E1[E2]、E1.*E2和E1->*E2中,E1顺序前于E2。 - 移位运算符 :在
E1 << E2和E1 >> E2中,E1顺序前于E2。 - 列表初始化 :在
{arg1, arg2, ...}中,前一个元素的求值与副作用顺序前于后一个元素。
- 逻辑与/或、逗号运算符 :内置的
- 运算符重载
- 自 C++17 起,当重载运算符使用运算符语法 (如
cout << a << b或a = b)被调用时,其求值顺序遵循对应内置运算符的 sequencing 规则,而非普通函数调用的规则。
- 自 C++17 起,当重载运算符使用运算符语法 (如
4.4.2 未定义行为(Undefined Behavior, UB)
在以下情况下,程序将触发 未定义行为:
- 写写冲突 : 在没有 sequencing 保证的前提下,对同一内存位置进行多次修改副作用。
i = ++i + i++;→ UBf(++i, ++i);→ UB(C++17 之前为 UB;C++17 及以后求值顺序未指定但不再是 UB)
- 读写冲突 : 在没有 sequencing 保证的前提下,对某一内存位置进行修改的同时,又读取该位置的值(除非读取是为了计算要写入的值,如
i = i + 1)。a[i] = i++;→ UB (C++17 之前为 UB;C++17 起由于E1[E2]中E1顺序前于E2且赋值右侧先于左侧,变为明确行为)cout << i << i++;→ UB(C++17 之前为 UB;C++17 起变为明确行为)
- 生命周期冲突 : 在同一内存位置上创建/销毁对象的生命周期操作,与该位置的读写副作用是未排序的(如对同一个
union的不同成员在同一个无序表达式中同时赋值)。
4.5 常量表达式
常量表达式是指可以在编译时进行求值的表达式。
-
主要用途:非类型模板参数(NTTP)、数组大小,以及其他任何要求必须在编译期确定常量的上下文。
-
基本对比 :
cint n = 1; std::array<int, n> a1; // 错误:"n"不是常量表达式 const int cn = 2; std::array<int, cn> a2; // 正确:"cn"是常量表达式
4.5.1 核心常量表达式
核心常量表达式是指其求值不会触发以下非法行为的表达式:
- 违规使用
this指针(C++23 前通常禁止,C++23 起允许在被求值的constexpr成员函数中使用)。 - 穿透带有静态或线程存储期且不可在常量表达式中使用的块变量声明控制流。
- 调用未声明为
constexpr的函数(或构造函数)、未定义的constexpr函数、或实例化失败的模板。 - 产生任何形式的核心语言未定义行为(UB)或错误行为(自 C++26 起的 erroneous behavior)。
- 动态内存分配与释放(如
new/delete,在 C++20 引入std::allocator的 constexpr 分配前或未在同一次求值中释放时受限)。 - 抛出未被捕获的异常(自 C++26 起,若异常在表达式内部被捕获并销毁则允许)。
reinterpret_cast转换、汇编声明(asm)、va_arg宏等。
4.5.2 字面量类型
只有字面量类型的对象才能在常量表达式中被创建。字面量类型包括:
- 可能带有
cv限定的void(自 C++14 起) - 标量类型、引用类型
- 字面量类型的数组
- 满足以下条件的类类型:
- 拥有简单析构函数(C++20 前)或
constexpr析构函数(自 C++20 起)。 - 所有非静态非变体数据成员和基类均属于非易失(non-volatile)的字面量类型。
- 属于闭包类型(自 C++17 起)、合法的联合体或聚合类型。
- 拥有简单析构函数(C++20 前)或
4.5.3 转换常量表达式
在特定上下文中,表达式会被隐式转换为目标类型 T,且转换后的表达式必须是常量表达式。允许的隐式转换序列受严格限制:
constexpr用户自定义转换- 左值到右值转换、整型提升、非窄化整型转换
- 浮点提升、非窄化浮点转换
- 数组到指针、函数到指针、函数指针转换
- 限定性转换
- 来自
std::nullptr_t的空指针/空成员指针转换 - 应用场景 :
case标签、枚举器初始化、非类型模板参数(NTTP)、数组边界、new表达式的后续维度、C++26 包索引(Pack indexing)等。
4.5.4 显式常量求值上下文
某些语境下,表达式必须被常规求值(即在编译期求值),例如:
- 数组边界、位域长度、枚举初始化器、对齐要求
static_assert声明、noexcept规范constexpr变量的初始化器constexpr if条件- 概念(Concept)约束表达式、
requires子句
可以通过 std::is_constant_evaluated()(自 C++20)或 if consteval(自 C++23)在运行时检测当前代码是否处于常量求值上下文中。
4.6 运算符
4.6.1 赋值运算符
赋值运算符用于修改对象的值,主要分为两大类:
- 简单赋值 :如
a = b。 - 复合赋值 :如
a += b、a *= b等。
返回值:
- 所有内置的赋值运算符都返回指向左操作数的左值引用(
*this) ,其结果类型为target-expr的类型。 - 大多数用户自定义重载也建议返回
*this,以便支持连续赋值(如a = b = c)。
内置简单赋值 (a = b)
- 左操作数要求 :
target-expr必须是一个可修改的左值 。 - 求值与转换 :右侧的
new-value若为表达式,会被隐式转换为target-expr的无 cv 限定符类型。 - 重载性 :赋值运算符只能 被重载为非静态的成员函数 (
T& T::operator=(const T2&)等)。
内置复合赋值 (a op= b)
- 等价行为 :
a op= b的行为完全等价于a = a op b,唯一的区别是a(左操作数)仅被求值一次。 - 操作数限制 :
- 对于
+=和-=,target-expr可以是算术类型,或者是指向完全定义对象类型的指针。 - 对于所有其他复合赋值运算符(如
*=、/=、<<=、&=等),target-expr必须是算术类型(指针不支持乘除或位运算复合赋值)。
- 对于
C++11及更高版本的重要特性
- 移动赋值 (自 C++11 起):除了传统的拷贝赋值外,还引入了移动赋值,用于将
b的资源直接转移给a,避免不必要的深拷贝。 - 大括号初始化列表赋值 (自 C++11 起):
- 可以直接将大括号初始化列表赋给类类型或标量类型(例如
z = {1, 2}或n = {'b'})。 - 若发生窄化转换,编译器会直接报错。
- 可以直接将大括号初始化列表赋给类类型或标量类型(例如
4.6.2 算术运算符
算术运算符用于执行基本的数学、位运算及移位操作,所有运算符均可被重载。
- 一元算术运算符 :一元加(
+a)、一元减(-a)、按位取反(~a)。 - 加减法运算符 :二元加(
a + b)、二元减(a - b)。 - 乘除与求余运算符 :乘法(
a * b)、除法(a / b)、求余(a % b)。 - 位逻辑运算符 :按位与(
&)、按位或(|)、按位异或(^)。 - 位移运算符 :左移(
<<)、右移(>>)。
通用规则:
- 内置算术运算符不修改操作数,均返回计算结果
- 整型提升 :小于
int的整型或无作用域枚举类型在参与运算前,会先隐式提升为int或unsigned int。 - 常规算术转换 :二元运算符的两个操作数类型不同时,会被统一转换为共同的通用类型。
除法与求余
- 整型除法 :向零取整(截断小数部分)。例如
-10 / 21的结果是0。 - 求余约束 :操作数必须为整数类型。满足
(a / b) * b + a % b == a。 - 异常情况 :若除数为
0,行为未定义。当INT_MIN / -1发生溢出时,除法和求余的行为同样未定义。
指针算术
- 指针可以与整数进行加减运算(如
p + j、p - j),其偏移量以指向的对象大小为单位。 - 两个指向同一数组中元素的指针相减,结果类型为
std::ptrdiff_t,表示两者之间的元素距离。
位移运算
- 左移(
<<) :对无符号数按模 2N 溢出截断;对于非负有符号数,若结果可用无符号表示再转回有符号则合法,否则未定义(C++20 起明确了基于二补数的循环位移/截断语义)。 - 右移(
>>) :自 C++20 起,有符号数的右移强制规定为算术右移(保持符号位不变,向负无穷方向舍入)。 - 未定义行为 :右侧操作数(
rhs)为负数,或者大于等于左侧操作数的总位数时,行为未定义。
4.6.3 自增与自减运算符
自增与自减运算符用于修改对象的值(加 1 或减 1),分为前缀(Prefix)和后缀(Postfix)两类,且均可被重载:
- 前缀形式 :
++a(先增加,后返回引用)、--a(先减少,后返回引用)。 - 后缀形式 :
a++(先返回值,后增加)、a--(先返回值,后减少)。
返回值与效率差异:
- 前缀版本 返回对象的左值引用(
T&) 。 - 后缀版本 返回修改前的临时副本(按值返回,
T) 。 - 性能建议:在返回值未被使用的上下文中,前缀版本的效率通常优于后缀版本,因为后缀版本需要构造和销毁临时对象。
重载签名与哑元参数 在类中或类外重载这些运算符时,前缀与后缀的参数列表有所不同:
- 前缀重载:无额外参数,直接对对象进行修改并返回引用。
- 后缀重载 :带有一个额外的
int类型的哑元参数 。该参数仅用于区分前缀和后缀重载版本,在调用时通常自动传入0。
前缀运算符(++x / --x)
- 等价性 :
++x在逻辑上等价于x += 1。 - 布尔类型限制 :
- C++17 之前,对
bool类型使用自增是已弃用(deprecated)的(布尔值会被设为true)。 - 自 C++17 起,对
bool类型使用自增(++)会导致程序编译期报错(ill-formed) ;而自减(--)对bool类型一直是非法的。
- C++17 之前,对
- Volatile 限定符 :自 C++20 起,对
volatile签名的对象使用自增/自减已被弃用。
后缀运算符(x++ / x--)
- 求值与副作用 :后缀表达式的结果是应用了 lvalue-to-rvalue 转换后的修改前的值。
- 求值顺序 :后缀自增/自减的值计算 在严格意义上先于 对象的修改发生。
- 操作数要求 :
expression必须是算术类型(bool除外)或完整对象类型的指针构成的可修改左值。
4.6.4 逻辑运算符
逻辑运算符用于执行布尔逻辑运算,所有内置及对应关键字形式均可被重载:
- 逻辑非(Logical NOT) :
!a或not a - 逻辑与(Logical AND) :
a && b或a and b - 逻辑或(Logical OR) :
a || b或a or b
替代表示 :
- 关键字形式(
and、or、not)与符号形式(&&、||、!)完全等价,可互换使用。
短路求值
- 内置逻辑运算符 :内置的
&&和||具有短路求值 特性。- 对于
&&:若左操作数为false,则不会对右操作数进行求值。 - 对于
||:若左操作数为true,则不会对右操作数进行求值。
- 对于
- 重载运算符的差异 :用户自定义重载的逻辑运算符行为类似于普通函数调用,总是会求值所有操作数 (不具备短路特性)。因此,通常不建议重载
operator&&和operator||。
布尔上下文转换
- 操作数无需显式为
bool类型。若操作数不是bool,系统会对其进行语境转换为 bool 。 - 转换规则:仅当声明
bool t(arg)在语法上合法时,该操作数才有效(例如指针、整型、流对象等均可隐式转换为bool)。
返回值
- 所有内置逻辑运算符均返回
bool类型的纯右值(prvalue)。
4.6.5 比较运算符
比较运算符用于对两个参数进行大小或相等性比较,主要分为两类:
- 双向比较运算符 :
- 关系运算符 :
a < b、a > b、a <= b、a >= b - 相等性运算符 :
a == b、a != b
- 关系运算符 :
- 三路比较运算符(自 C++20 起) :
- 语法形式 :
a <=> b(又称 spaceship 运算符)。
- 语法形式 :
返回值与重载:
- 内置的比较运算符均返回
bool类型的纯右值(prvalue) (<=>除外,它返回诸如std::strong_ordering等排序类别对象)。 - 所有比较运算符均可被重载(包括成员函数或非成员函数形式)。
三路比较运算符(<=>)详解 :自 C++20 引入的 a <=> b 运算符会返回一个比较结果对象,其返回值遵循以下逻辑:
(a <=> b) < 0等价于a < b(a <=> b) > 0等价于a > b(a <=> b) == 0等价于a与b相等/等价
根据操作数类型返回不同的排序类别:
- 算术类型(整型) :返回
std::strong_ordering(如std::strong_ordering::equal/less/greater)。若涉及可能导致数据截断的窄化转型 ,则程序会直接编译报错。 - 浮点数类型 :返回
std::partial_ordering(支持unordered,用于处理 NaN 的情况)。 - 指针类型 :返回
std::strong_ordering。
算术比较的隐式转换
- 当两个操作数均为算术类型或枚举类型时,会执行常规算术转换 。
- 经典陷阱 :在对有符号数(如
-1)与 无符号数(如unsigned int)进行比较时,有符号数会被隐式转换为无符号数,导致-1变成一个极大的正数,从而产生与直觉相反的结果(例如-1 < 1结果为false)。
结合性陷阱
- 比较运算符按左到右顺序结合。
- 表达式
a < b < c实际会被解析为(a < b) < c,而不是数学上的区间检查(a < b) && (b < c)。正确的连续比较必须显式结合逻辑与运算符(如(a < b) && (b < c))。
指针比较规则
- 内置指针的相等和关系比较要求指针具有可比性(如指向同一数组、同一对象的成员等)。
- 指针与整型字面量(如
p > 0或p > nullptr)的直接比较在现代 C++ 标准中属于非法操作(编译报错) 。
4.6.6 成员访问与核心操作符
C++ 提供了一组用于访问对象、指针、数组成员及地址的核心操作符:
- 下标运算符 :
a[b](支持 C++23 多维下标及逗号初始化列表扩展) - 间接寻址运算符 :
*a(解引用指针或函数) - 取地址运算符 :
&a(获取对象/函数的指针,或生成成员指针) - 成员访问运算符 :
a.b(对象访问)与a->b(指针访问) - 指向成员的指针运算符 :
a.*b与a->*b
下标运算符(a[b])与等价性
- 内置行为 :内置下标表达式
E1[E2]严格等价于*(E1 + E2)。 - 对称性 :由于基于指针算术运算,
p[1]与1[p]在语法上完全等价。 - 重载特性 :
- 支持自定义重载(如
std::vector、std::map等标准库容器)。 - 自 C++23 起,支持多维下标形式(如
a[b, c]或a[{b, c}])。
- 支持自定义重载(如
成员访问与 operator-> 的递归解包机制
a->b的展开规则 :对于重载了operator->的类对象,编译器会递归调用operator->,直到返回一个原生指针,随后对该原生指针应用内置的成员访问语义。- 伪析构函数 :
- 语法形如
p->~U()或a.~U()。 - 允许对标量类型(如
int)显式调用析构函数,主要用于编写通用模板代码,本身不执行任何实际操作(无操作)。
- 语法形如
取地址运算符(&)与指针-成员运算符
- 取地址(
&) :- 对普通左值对象或函数返回
T*或函数指针。 - 若用于非静态成员(如
&C::member),会生成指向成员的指针 ,其类型为T C::*,不能直接作为普通对象指针使用。
- 对普通左值对象或函数返回
- 成员指针访问(
.*与->*) :- 用于通过指向成员的指针来操作特定对象。例如
(obj.*pm)(args)或(ptr->*pm)(args)。 - 若
pm指向虚成员函数或多重继承中的特定成员,编译器会通过对象的动态布局正确解析偏移量。
- 用于通过指向成员的指针来操作特定对象。例如
4.6.7 函数调用运算符(())
- 语法形式 :
function(arg1, arg2, ...) - 重载特性 :
- 可以被重载(作为成员函数
T::operator()(Args...)),重载后对象即成为函数对象 。 - 标准库中大量的算术、逻辑和比较仿函数(如
std::plus、std::less、std::function)均基于此运算符。
- 可以被重载(作为成员函数
- 协变返回类型 :派生类中的重写函数可以返回指向基类返回类型派生类的指针或引用。
- 返回值类别:若函数返回左值引用或函数引用,则调用表达式为左值;若返回对象右值引用,则为 xvalue;否则为 prvalue。
4.6.8 内置逗号运算符(,)
- 语法形式 :
E1 , E2 - 执行顺序与副作用 :
- 先严格评估左侧表达式
E1并将其结果丢弃,其所有副作用在评估右侧表达式E2之前完成。
- 先严格评估左侧表达式
- 类型与值类别 :整个逗号表达式的类型、值和值类别完全由右操作数
E2决定。 - 常见陷阱 :
- 函数参数列表(如
f(a, b))或初始化列表中的逗号是分隔符 而非逗号运算符。若需在其中使用逗号运算符,必须加括号(如f(a, (n++, n), c))。 - 自 C++23 起,未加括号的逗号表达式直接禁止作为下标运算符的第二个参数(如
a[b, c]会导致编译报错或等价于a.operator[](b, c))。
- 函数参数列表(如
4.6.9 条件运算符 / 三目运算符(? :)
- 语法形式 :
E1 ? E2 : E3 - 执行逻辑 :首先将
E1上下文转换为bool。若为true,计算并返回E2;否则计算并返回E3。 - 类型推导规则(多阶段处理) :
void处理 :若两端均为void,结果为void;若一端为void且另一端是throw表达式,则采用另一端的类型。- 引用与转换 :通过复杂的重载解析、标准转换序列及公共类型推导(类似
std::common_type),将E2和E3转换为兼容的公共类型。
- 左值特性 :若
E2和E3具有相同的类型和相同的值类别(且均能作为左值/glvalue),则该条件表达式可以作为左值 (例如(n == m ? n : m) = 7)。
4.6.10 sizeof
sizeof 是一个编译期运算符(返回类型为 std::size_t 的常量表达式),用于查询类型或表达式对象在内存中占用的字节数。
sizeof(type)------ 查询指定类型的对象表示的字节数。sizeof expression------ 查询表达式结果类型的对象表示的字节数(注意:若表达式无括号,其优先级高于部分二元运算符,例如sizeof a + b会被解析为(sizeof a) + b)。
核心行为与特殊保证
- 固定为 1 的类型 :
sizeof(char)、sizeof(signed char)、sizeof(unsigned char)、sizeof(std::byte)以及sizeof(char8_t)(自 C++20 起)在任何架构下恒等于1。 - 非零保证 :即使应用于空类,其
sizeof结果也永远不为零 (通常为1,除非涉及基类优化等情况)。 - 引用处理 :当应用于引用类型时,
sizeof返回的是被引用对象的大小,而非引用本身的大小。 - 类与字节对齐 :应用于类类型时,结果包含由于对齐要求而填充的空白字节。
核心限制与求值陷阱
- 禁用场景 :
sizeof不能用于函数类型 、不完整类型 或位域 glvalue。 - 未求值操作数 :
- 当
sizeof应用于表达式时,该表达式不会在运行时被实际求值(其内部的副作用如函数调用不会发生)。 - 即使表达式涉及多态对象,其结果也取决于该表达式的静态类型 。
- 针对 prvalue 参数,编译器(自 C++17 起)会形式上进行临时对象实质化,若该参数不可析构则程序会导致编译报错。
- 当
4.6.11 alignof
自 C++11 引入的 alignof 是一个编译期运算符,用于查询指定类型的对齐要求(返回类型为 std::size_t 的常量表达式)。
- 语法形式 :
alignof(type-id) - 返回值:返回该类型任何实例在内存中所要求的对齐字节数
- 支持的类型范围 :
- 完整对象类型 。
- 元素类型为完整的数组类型(此时返回的是数组元素的对齐要求)。
- 引用类型(此时返回的是被引用类型的对齐要求)。
- 对齐属性 :返回的值必须是
std::size_t类型,通常是 2 的幂次方,具体取决于目标硬件架构和类型的内部成员布局。
4.6.12 new
new 表达式用于在动态存储区创建和初始化对象或对象数组,其生命周期不受创建它的作用域限制。
- 基本语法 :
::(optional) new type new-initializer(optional)::(optional) new (placement-args) type new-initializer(optional)
- 返回结果:返回一个指向所创建对象的 prvalue 指针(若为数组,则返回指向数组首元素的指针)。
核心执行阶段 :new 表达式的执行分为三个主要阶段:
- 内存分配 :
- 调用相应的分配函数(非数组类型调用
operator new,数组类型调用operator new[])。 - 若存在
placement-args(如位置new),则将其额外传递给分配函数。 - 自 C++17 起,若对象的对齐要求超过默认对齐,会自动将对齐要求(包装为
std::align_val_t)作为参数传递给分配函数。
- 调用相应的分配函数(非数组类型调用
- 对象构造与初始化 :
- 根据
new-initializer的形式(无初始化器、括号包围的表达式列表、大括号包围的初始化列表),对单对象或数组元素进行默认初始化、直接初始化、值初始化、列表初始化或聚合初始化。
- 根据
- 异常安全与内存释放 :
- 若在对象初始化阶段抛出异常(例如构造函数抛出异常),程序会自动查找并调用匹配的反分配函数(即
operator delete)来释放已分配的内存,随后异常继续向外传播。
- 若在对象初始化阶段抛出异常(例如构造函数抛出异常),程序会自动查找并调用匹配的反分配函数(即
动态数组
- 运行时确定大小 :当
type为数组类型时,除了第一维之外的所有维数必须是编译期常量表达式,而第一维可以是在运行时确定的表达式 (如变量n)。 - 数组开销 :分配数组时,实现通常会在返回的指针之前存储一些额外信息(如数组元素个数),以便
delete[]能够正确调用所有元素的析构函数(若元素类型为自反析构,部分 ABI 允许将开销优化为零)。
最佳实践与内存管理
- 内存泄漏风险 :通过
new分配的内存必须通过匹配的delete或delete[]表达式释放。若指针丢失或在抛出异常前未释放,会导致内存泄漏。 - 智能指针接管 :在现代 C++ 中,强烈推荐使用智能指针(如
std::unique_ptr或std::shared_ptr)来自动管理通过new创建的动态对象,从而实现异常安全并避免内存泄漏。
4.6.13 delete
delete 表达式用于销毁先前由 new 表达式分配的对象或对象数组,并释放对应的内存空间。
::(optional) delete expression------ 销毁单个非数组对象。::(optional) delete[] expression------ 销毁由new[]创建的对象数组。- 返回值 :
delete表达式的结果类型始终为void。
核心执行步骤 :当对非空指针(ptr)执行 delete 时,编译器按以下顺序执行:
- 析构对象 :
- 依次调用对象的析构函数(若是数组形式,则从数组最后一个元素到第一个元素逆序调用)。
- 必须保证析构函数在删除点是可访问的。
- 释放内存 :
- 调用匹配的反分配函数(单对象调用
operator delete,数组调用operator delete[])。 - 反分配函数的查找会从对象的动态类型作用域开始向上检查类专属版本,若未找到或使用
::限定,则回退到全局作用域。 - 若类型的对齐要求超出默认值,或涉及带大小的重载,编译器会优先选择对应的对齐感知或大小感知的重载版本。
- 调用匹配的反分配函数(单对象调用
空指针与多态安全性
- 空指针安全 :若传递给
delete的指针为空指针 ,则不会调用任何析构函数,且标准保证默认的反分配函数会安全地什么都不做。 - 多态与虚析构函数 :若通过基类指针 删除一个派生类对象(该对象由
new创建),基类必须具备虚析构函数 ,否则会导致未定义行为。
关键限制与安全警示
- 类型匹配 :单对象
delete和数组delete[]必须严格配对。用delete去释放new[]分配的内存,或者反之,会导致未定义行为。 - void 指针限制 :不能直接对
void*类型的指针使用delete(因为void不是对象类型)。
4.6.14 typeid
typeid 是一个运算符,用于在编译期或运行时尚查询类型信息,其返回结果是一个左值表达式,引用具有静态存储周期的 std::type_info(或其派生类)对象的 const 限定版本。若标准库中未定义 std::type_info,程序将编译报错。
typeid(type)------ 直接查询指定类型的std::type_info。若类型带有引用,结果代表其去除 cv 限定符后的引用目标类型。typeid(expression)------ 查询表达式的类型信息。
核心行为与多态特性
- 多态对象的动态类型 :
- 若表达式是一个 glvalue 且其类型属于多态类型 (即声明或继承了至少一个虚函数的类),
typeid会在运行时 实际对该表达式进行求值,并返回其动态类型 对应的std::type_info。 - 空指针异常 :若该表达式是一个间接表达式(如解引用指针),且其求值结果为空指针 ,则会抛出
std::bad_typeid异常。
- 若表达式是一个 glvalue 且其类型属于多态类型 (即声明或继承了至少一个虚函数的类),
- 静态类型的查询 :
- 若表达式不属于多态类型,或者它是一个 prvalue(不涉及多态),则
typeid不会在运行时对表达式求值 (属于未求值操作数),其返回的std::type_info仅代表该表达式的静态类型 。
- 若表达式不属于多态类型,或者它是一个 prvalue(不涉及多态),则
cv 限定符处理与构造/析构期间行为
- 自动忽略 CV 限定 :无论是类型还是表达式,
typeid结果所代表的std::type_info均会自动去除顶层的const或volatile限定符(例如typeid(const T) == typeid(T)恒成立)。 - 构造或析构期间的行为 :若在对象的构造函数(含初始化列表、默认成员初始化器)或析构函数内部使用
typeid(*this),此时返回的std::type_info代表当前正在被构造或销毁的类,而非最终派生类。在其他未初始化或已销毁的上下文中使用会导致未定义行为。
比较与相等性保证
- 指针与对象比较 :虽然不能保证对同一类型多次使用
typeid返回的是同一个std::type_info对象地址(即&ti1 == &ti2不保证为真),但标准保证它们的值比较、哈希码以及通过std::type_index进行的比较完全等价且恒为真。
4.6.15 反射运算符
自 C++26 引入的 ^^ 运算符 (称为反射运算符)是编译期运算符,用于生成语言构造的反射值,其返回类型为 std::meta::info(编译期常量表达式)。
支持的语法形式包括
- 全局命名空间反射 :
^^:: - 反射名称 :
^^reflection-name(优先于其他形式,可包含标识符、限定符或模板 disambiguator) - 类型标识反射 :
^^type-id(如^^int、^^std::vector<int>) - 标识表达式反射 :
^^id-expression(如变量、函数、非静态数据成员等)
核心行为与求值特性
- 未求值操作数 :
^^的操作数属于未求值操作数,不会在运行时被执行或求值。 - 最大吞噬原则 :编译器会解析语法上能够成操作员操作数的最长记号序列 。若结果代表一个模板且紧跟
<,可能会导致语法解析歧义,通常需要用括号显式包裹(例如(^^X) < xv)。 - 类型别名行为 :
- 若作为
reflection-name(如^^Y指代typedef ... Y),反射结果代表该类型别名本身。 - 若作为
type-id的一部分,反射运算符则不保留类型别名,而是指向被指代的实际基础类型。 常见限制与非法场景
- 若作为
- 禁止的情况 :
- 包含占位符类型(如
auto)的type-id表达式。 - 引用了 lambda 表达式
init-capture变量、局部预定义变量(如__func__)或受 lambda 作用域隔离的局部实体。 - 非法模板后紧跟
<符号且未加括号的表达式。
- 包含占位符类型(如
4.6.16 替代运算符表示法
由于早期的非 ASCII 7-bit 字符集(如 ISO 646)缺少部分特殊符号(如 {, }, [, ], #, ``, ^, |, ~),C++ 提供了替代记号 ,允许开发者使用完全由 ISO 646 兼容字符组成的拼写。
- 行为等价性 :在语言的所有语义层面上,替代记号与主记号完全等价 ,唯一的区别仅在于拼写(唯有字符串化运算符
#可以使拼写可见)。 - 核心分类 :
- 单词替代记号 (如
and,or,not等)。 - 二字母/多字母替代记号 (如
<%,%>,&:等)。
- 单词替代记号 (如
替代记号与主记号对照表
| 主记号 | **替代记号 ** | 类别说明 |
|---|---|---|
&& |
and |
逻辑与 |
&= |
and_eq |
与赋值 |
& |
bitand |
按位与 / 取地址 |
| ` | ` | bitor |
~ |
compl |
按位取反 / 补码 |
! |
not |
逻辑非 |
!= |
not_eq |
不等于 |
| ` | ` | |
| ` | =` | or_eq |
^ |
xor |
按位异或 |
^= |
xor_eq |
异或赋值 |
{ |
<% |
左大括号 |
} |
%> |
右大括号 |
[ |
<: |
左中括号 |
] |
:> |
右中括号 |
# |
:%: |
预处理井号 |
## |
%:%: |
预处理连接号 |
三字母词历史演进
- 状态 :三字母词(如
??<表示{)在 C++17 中已被正式移除。 - 处理特性 :在早期标准中,三字母词会在识别注释和字符串字面量之前被预处理替换,这曾导致过诸如注释意外吞并下一行的经典安全隐患。
兼容性与使用注意事项
- 无需头文件 :非单词的 digraph(如
<%,<:)属于核心语言特性,无需包含任何头文件即可直接使用。 - 无
==替代 :由于=字符存在于所有支持的字符集中,C++ 中没有 为相等运算符==提供诸如eq的替代拼写。
4.7 默认比较运算符
自 C++20 起,比较运算符函数可以被显式定义为 default(即 = default),以请求编译器为类自动生成对应的默认比较实现。
- 基本条件 :
- 必须是某个类
C的非静态成员或友元函数。 - 在类
C内部或在C为完整类型的上下文中定义为default。 - 必须接受两个参数(均为
const C&或C,其中隐式对象参数视为第一个参数)。
- 必须是某个类
- 类内定义规则 :在类定义内部将比较运算符显式声明为
default的定义,必须是该函数的首次声明。
4.7.1 默认比较顺序
编译器生成默认比较时,会按严格的顺序遍历类 C 的子对象列表 :
- 直接基类子对象(按声明顺序)。
- 非静态数据成员(按声明顺序)。
- 数组展开:若成员为数组类型,则按下标递增顺序递归展开(如多维数组按行主序依次展开)。
4.7.2 三路比较运算符(operator<=>)
- 返回类型 :可使用任意返回类型,若声明为
auto,编译器会自动推导为对应的比较类别类型 :std::strong_ordering(强序:等价即全等,无不可比较值)std::weak_ordering(弱序:值可区分但等价,无不可比较值)std::partial_ordering(偏序:允许存在不可比较值,如NaN)
- 自动推导与短路求值 :编译器会依次按子对象顺序进行三路比较。一旦某个子对象比较结果
v_i != 0(即不等于相等),就会立即短路并返回该结果,后续子对象不再参与比较。
4.7.3 相等比较运算符(operator==)与隐式声明
- 显式声明 :返回类型必须为
bool。若按子对象逐个比较,一旦某个子对象比较结果为false,则立即短路并返回false。 - 隐式声明机制 :
- 若类
C未显式声明 任何名为operator==的成员或友元,但定义了默认的operator<=>,编译器会自动隐式声明一个对应的operator==(其访问权限、定义方式和作用域与该operator<=>保持一致)。
- 若类
4.7.4 次要比较运算符
- 包括
!=,<,>,<=,>=。 - 可以为类显式定义为
default(返回类型为bool)。编译器会通过重写规则(Rewritten Candidates)基于operator<=>或operator==自动合成其行为。若对应的重载解析失败或无法隐式转换为bool,该运算符会被定义为deleted。
4.8 运算符优先级与结合性
- 编译期概念 :运算符的优先级和结合性是纯粹的编译期语法概念,决定了表达式如何被无歧义地解析为语法树。它们与运行时的求值顺序完全独立。
- 无显式标准分级:C++ 标准规范本身并未显式定义数字层级的优先级,现行的数字序号通常是从文法推导出的表现形式。
优先级总表(从高到低)
| 优先级 | 运算符类别 | 具体运算符符号与说明 | 结合性 (Associativity) |
|---|---|---|---|
| 1 | 作用域解析 | a::b |
从左到右 (→) |
| 2 | 后缀/后置运算 | a++, a--, type(a), type{a} (函数式转型), a(), a[], a.b, a->b |
从左到右 (→) |
| 3 | 前缀/一元运算 | ++a, --a, +a, -a, !a, ~a, (type)a, *a, &a, ^^a (C++26反射), sizeof, co_await, new/new[], delete/delete[] |
从右到左 (←) |
| 4 | 成员指针访问 | a.*b, a->*b |
从左到右 (→) |
| 5 | 乘除与取模 | a * b, a / b, a % b |
从左到右 (→) |
| 6 | 加减运算 | a + b, a - b |
从左到右 (→) |
| 7 | 移位运算 | a << b, a >> b |
从左到右 (→) |
| 8 | 三路比较 | a <=> b (C++20) |
从左到右 (→) |
| 9 | 关系运算符 | a < b, a <= b, a > b, a >= b |
从左到右 (→) |
| 10 | 相等运算符 | a == b, a != b |
从左到右 (→) |
| 11 | 按位与 | a & b |
从左到右 (→) |
| 12 | 按位异或 | a ^ b |
从左到右 (→) |
| 13 | 按位或 | `a | b` |
| 14 | 逻辑与 | a && b |
从左到右 (→) |
| 15 | 逻辑或 | `a | |
| 16 | 条件/赋值/跳转 | a ? b : c (三目), throw, co_yield, a = b 及所有复合赋值 (+=, -=, *=, <<= 等) |
从右到左 (←) |
| 17 | 逗号运算符 | a, b |
从左到右 (→) |
关键规则与常见陷阱
- 优先级不因重载改变 :重载运算符不会改变其原有的优先级与结合性(例如
std::cout << a ? b : c;会被解析为(std::cout << a) ? b : c;,因为移位运算符优先级高于条件运算符)。 - 结合性特例 :
- 一元操作符:前缀一元运算符固定为从右到左,后缀一元运算符固定为从左到右。
- 赋值与三目运算 :属于同一优先级(级别 16),采用从右到左 结合(如
a = b = c解析为a = (b = c))。
- 解析边界陷阱 :
sizeof (int) * p会被无歧义地解析为(sizeof(int)) * p(而不是sizeof((int)*p))。- 三目运算符中间的表达式(
?和:之间)视为自带隐式括号,其优先级相对?:整体被隔离。
4.9 类型转换
4.9.1 隐式转换
当某种类型的表达式 T1 被用于不接受该类型、但期望另一种类型 T2 的上下文时,编译器会自动执行隐式转换。
- 主要触发上下文 :
- 函数调用时传入的实参与形参类型不一致。
- 运算符操作数期望特定类型。
- 初始化新对象或函数的
return语句。 switch语句控制表达式(要求整型)。if语句、循环控制表达式或逻辑运算符(要求bool)。
- 唯一性要求 :程序要能成功编译,必须存在且仅存在一个无歧义的隐式转换序列 从
T1转换到T2。
隐式转换序列的构成顺序:完整的隐式转换序列最多由以下三个阶段按顺序组合而成
- 零个或一个 标准转换序列
- 零个或一个 用户自定义转换(如转换构造函数或转换函数)。
- 零个或一个 标准转换序列(仅当使用了用户自定义转换时才允许)。
标准转换序列:严格顺序包含以下几步
- 左值到右值、数组到指针、函数到指针转换 (如
lvalue-to-rvalue、array-to-pointer decay等)。 - 数值提升 或 数值转换 。
- 提升 :如小整型(
char)升为int,float升为double,是无精度损失的。 - 转换 :如
int转double、截断或符号转换,可能伴随精度丢失或值改变。
- 提升 :如小整型(
- 函数指针转换(自 C++17 起,如非抛出函数指针转为可抛出函数指针)。
- 限定资格转换 :如添加
const或volatile修饰。
关键转换类型细分
- 指针与成员指针转换 :
- 空指针常量(如
0或nullptr)可隐式转换为任意指针或成员指针类型。 - 派生类指针/引用可隐式转换为可访问且无歧义的公共基类指针/引用。
- 空指针常量(如
- 布尔转换 :
- 算术类型、枚举、指针及成员指针均可隐式转换为
bool(零值/空指针为false,其余为true)。
- 算术类型、枚举、指针及成员指针均可隐式转换为
- 临时对象实质化 :
- 自 C++17 起,当 prvalue 出现在期望 glvalue 的上下文中时,会触发临时对象实质化,将 prvalue 转为 xvalue(绑定到临时对象)。
限定资格转换与"指针安全"
- 指针 cv 限定符添加 :可以将
T*隐式转换为带更多 cv 限定的指针(如const T*)。 - 多级指针安全性防范 :C++ 通过严格的类型相似性与
cv_i规则,防止了 C 语言中经典的多级指针转换漏洞(例如防止通过const char** p = &pc;间接修改底层const常量内存)。
4.9.2 显式类型转换
显式类型转换允许使用显式和隐式转换的组合在不同类型之间进行转换。主要分为两大类:
- C 风格强制类型转换 :
- 语法 :
(type-id) unary-expression
- 语法 :
- 函数风格强制类型转换 :
- 语法 :
simple-type-specifier ( expression-list )或其花括号初始化形式(如type{val})。
- 语法 :
C 风格强制类型转换的解析顺序 :当编译器遇到 C 风格强制类型转换 (type-id)expr 时,它会按固定顺序依次尝试以下几种转换(选择第一个满足要求的,即使该转换最终格式错误也会被选中):
const_cast<type-id>(expr)static_cast<type-id>(expr)(包含 C 风格扩展:允许跨越private等不可访问的继承边界在指针/引用间转换)static_cast后面紧跟const_castreinterpret_cast<type-id>(expr)reinterpret_cast后面紧跟const_cast
函数风格转换的执行结果
- 单个表达式 :形式如
T(expr)且括号内只有一个表达式时,等价于对应的 C 风格转换。 void类型 :若目标类型为void,则产生一个不进行初始化的void类型的 prvalue。- 引用类型 :若目标类型
T为引用,则其效果等同于对一个由指定初始化器直接初始化的临时变量进行引用绑定,结果返回左值(或 xvalue)。 - 对象类型:通过指定的初始化器直接初始化并返回一个临时对象(prvalue/xvalue)。
经典语法歧义与规避规则:C++ 语法中存在多处经典的"最令人烦恼的解析"或结构歧义
- 声明与表达式歧义 :若某个语句既可以被解析为函数风格的表达式语句,又可以被解析为对象声明,编译器强制优先将其解析为声明 (例如
M(m);会被视为声明一个名为m的类型为M的变量)。- 规避方法 :为初始化器外层多加一对括号(如
S y((int)a);),或者使用赋值初始化(如S z = int(a);)。
- 规避方法 :为初始化器外层多加一对括号(如
- 类型标识(Type-Id)歧义 :在某些上下文(如
sizeof或表达式内)中,若某结构既可被解析为合法的type-id(如函数声明类型),又可被解析为表达式,标准规定优先将其解释为type-id。
4.9.3 用户自定义转换函数
用户自定义转换函数允许将一个类类型 隐式或显式转换为其他类型。它必须是类的非静态成员函数 或成员函数模板。
- 基本语法 :
operator conversion-type-id();(隐式与显式转换均可参与)explicit operator conversion-type-id();(自 C++11 起,仅参与直接初始化与显式转换)explicit( expression ) operator conversion-type-id();(自 C++20 起,条件显式)
- 语法限制 :
- 没有显式返回类型(连
void也不写)。 - 无参数。
conversion-type-id中不能包含函数和数组操作符[]或(), 也不能直接代表数组或函数类型(但可以通过typedef或类型别名间接转换指针/引用)。
- 没有显式返回类型(连
参与位置与重载冲突
- 在隐式转换序列的第二阶段(用户自定义转换阶段),类可以同时拥有转换构造函数 和用户定义转换函数。
- 在拷贝初始化 和引用初始化 上下文中,编译器会同时考虑转换函数与构造函数,如果两者都可行且产生冲突,可能导致二义性错误 。
- 在直接初始化 上下文中(如
static_cast或直接调用),仅会考虑构造函数(视具体情况而定),转换函数在某些直接初始化语境中可能不被隐式触发。
特殊规则与调用限制
- 不可调用的特殊转换 :
- 转换到自身类(或其引用) 、基类(或其引用) 、以及
void的转换函数可以被定义,但它们不能作为常规隐式转换序列的一部分被自动执行 (防止无限递归或语义混乱),除非通过虚函数派发或通过显式成员函数调用语法来触发。
- 转换到自身类(或其引用) 、基类(或其引用) 、以及
- 显式调用语法 :
- 可以通过成员函数调用语法显式调用,例如
x.operator int&()。 conversion-type-id在显式调用时是"贪婪的"(会匹配最长的记号序列)。
- 可以通过成员函数调用语法显式调用,例如
- 模板与继承 :
- 转换函数可以被继承、可以声明为
virtual(但不能为static)。 - 派生类的转换函数不会隐藏基类的转换函数,除非它们转换到完全相同的目标类型。
- 自 C++14 起,转换函数支持使用
auto或decltype(auto)推导返回类型(但转换函数模板不允许使用推导返回类型)。
- 转换函数可以被继承、可以声明为
4.9.4 通常算术转换
- 目的 :当二元运算符期望算术或枚举类型的操作数时,若两边类型不一致,通常算术转换会将其统一转换为一个公共类型 ,该公共类型同时也是最终表达式的结果类型。
- 执行阶段:转换过程依次通过多个阶段进行判定与类型提升。
转换执行阶段
- 阶段 1 :对两个操作数分别应用左值到右值转换 ,用得到的 prvalue 参与后续计算。
- 阶段 2&3 :针对强作用域枚举、无作用域枚举及浮点型与枚举混合的情况进行类型合法性校验(部分混合情况在 C++20/C++26 中被废弃或规定为 ill-formed)。
- 阶段 4(浮点处理) :若任一操作数是浮点型:
- 若类型相同则不转换。
- 若一个是浮点、另一个是非浮点,非浮点操作数转换为浮点类型。
- 若两边都是浮点型,则根据浮点转换 rank (以及 subrank)将 rank 较低的操作数转换为 rank 较高的类型。
- 阶段 5 (整型公共类型推导) :若两个操作数均为整型:
- 首先各自进行整型提升(Integral Promotions)得到临时类型 T1 和 T2。
- 若 T1 和 T2 相同,则为该类型。
- 若同为有符号或同为无符号,选择整型转换 rank(Integer Conversion Rank)较高者。
- 若一有符号( S)、一无符号( U):
- 若 U 的 rank ≥ S 的 rank,公共类型为 U。
- 若 S 能表示 U 的所有取值,公共类型为 S。
- 否则,公共类型为 S 对应的无符号整型版本。
整型转换 Rank:整型 rank 从高到低大致排序如下
long long>long>int>short>signed char(标准有符号整型)- 无符号整型与其对应的有符号整型 rank 相同。
- 标准整型 rank 高于同宽度的扩展整型。
bool的 rank 最低。- 字符类型(
char,char8_t,char16_t,char32_t,wchar_t)的 rank 等于其底层对应的整型。
浮点转换 Rank 与 Subrank
- Rank 排序 :标准浮点型中
long double>double>float。值集合更大的浮点型具有更高的 rank。 - Subrank :当浮点转换 rank 相同时,由扩展浮点型或固定宽度浮点型(如
std::float64_t等)的 subrank 进一步确定其全序关系。
4.9.5 static_cast
static_cast 用于在编译期进行主要依赖于类型信息的显式转换。它结合了隐式转换与用户自定义转换,且不能用于去除底层 const 或 volatile 限定符
- 基本语法 :
static_cast<target-type>(expression)
核心转换场景与规则
- 引用与指针的向下转型 :
- 允许将基类指针/引用(
Base)转换为派生类指针/引用(Derived)。 - 安全警示 :
static_cast不进行任何运行时的类型检查 (无 RTTI)。如果被转换的对象实际并不是该派生类类型,会导致未定义行为(UB) 。安全转型应使用dynamic_cast。 - 虚拟继承限制 :若
Base是Derived的虚基类(或虚基类的基类),则无法使用static_cast向下转型,程序会编译报错。
- 允许将基类指针/引用(
- 值类别转换与移动语义 :
- 可以将表达式转换为右值引用类型(如
static_cast<T&&>(v)),产生一个表示该对象的xvalue。这是标准库std::move的底层实现原理。
- 可以将表达式转换为右值引用类型(如
- 数值与枚举类型转换 :
- 支持算术类型之间的相互转换(如
double转int)。 - 支持枚举类型与整型、浮点型之间的互相转换(自 C++20 起,无作用域枚举转整型时会先转为其底层类型)。
- 支持算术类型之间的相互转换(如
void*与对象指针互转 :- 允许将
void*显式转换为其他对象指针类型。若指针原本指向正确对齐的内存或满足指针互转(如标准布局类与其首个非静态成员),则能正确还原。
- 允许将
- 逆向标准转换 :
- 允许执行某些标准转换的逆向操作(例如将基类指针显式转回派生类指针,或者把某些允许逆向的数值转换实施反向映射)。
4.9.6 dynamic_cast
dynamic_cast 用于在继承体系中进行安全的向上转型 、向下转型 和侧向转型 。它依赖于运行时类型信息(RTTI) ,要求操作的类必须是多态类型(即至少包含一个虚函数) 。
- 基本语法 :
dynamic_cast<target-type>(expression)
核心转换场景与规则
- 向上转型 :
- 将派生类指针/引用转换为基类指针/引用。
- 特点 :属于安全转换,隐式转换和
static_cast也能完成此操作。
- 向下转型与侧向转型 :
- 将基类指针/引用安全地转换为派生类指针/引用(向下),或在多重继承的平行分支之间进行转换(侧向)。
- 运行时检查 :会在运行期检查对象的真实动态类型。
- 指针转换失败 :返回
target-type对应的空指针(nullptr) 。 - 引用转换失败 :抛出抛出类型匹配
std::bad_cast的异常。
- 指针转换失败 :返回
- 转换为
void*:dynamic_cast<void*>(expr)可以将多态对象的指针转换为void*,结果指向该对象的完整最底层派生对象(Most Derived Object)的起始地址。
- 构造/析构期间的使用 :
- 在构造函数或析构函数内部,若使用
dynamic_cast操作当前正在构造或析构的对象,该对象会被视为当前类层级的最底层对象。
- 在构造函数或析构函数内部,若使用
4.9.7 const_cast
const_cast 是专门用于添加或移除表达式的 const 或 volatile 限定符、的命名强制类型转换运算符。它是唯一能够"去除 const"、的类型转换操作符。
- 基本语法 :
const_cast<target-type>(expression)
核心转换场景与规则
- 指针与成员指针转换 :
- 允许在两个相似 的对象指针或成员指针类型之间进行转换,前提是它们仅在 cv-qualification 上有所不同。
- 转换后的指针仍然指向原来的对象、对象尾后位置或数据成员,不改变其内存地址。
- 引用与值类别转换 :
- 如果指针类型
T1*可以通过const_cast<T2*>显式转换,那么对应的引用类型(如T1&转T2&或T1转T2&&)也可以进行相应的const移除或添加。
- 如果指针类型
重要限制与未定义行为警示
- 函数指针排除在外 :
const_cast不能用于函数指针或成员函数指针的 cv 限定符修改。
- 底层常量的修改是未定义行为 :
- 虽然
const_cast允许你通过去掉const获得一个指向非const的指针或引用,但如果该指针/引用原本确实指向一个真正的const对象 ,随后通过它修改该对象的内容 ,会导致未定义行为 。 - 只有当原始对象本身不是
const(只是通过const指针/引用进行访问)时,使用const_cast去除const并进行修改才是安全的。
- 虽然
4.9.8 reinterpret_cast
reinterpret_cast 是一种低级别的、重新解释底层位模式的强制类型转换。
- 基本语法 :
reinterpret_cast<target-type>(expression) - 特点 :它通常不生成任何 CPU 指令 (指针与整数互转或特殊架构除外),而纯粹是编译期的指令,指示编译器将给定的表达式按新的类型进行对待。它不能用于去除
const或volatile限定符。
核心支持的转换场景
- 相同类型转换:整型、枚举、指针或成员指针可以转换回自身的类型。
- 指针与整型互转 :
- 指针可以转换为足够大以容纳其值的整型(如
std::uintptr_t)。 - 整型或枚举值也可以转回指针类型(指针转整数再转回原指针类型能保证原值不变,但反向不保证)。
- 指针可以转换为足够大以容纳其值的整型(如
- 对象指针类型重解释 :
- 任意对象指针
T1*可以转为另一对象指针cv T2*(等价于先转为void*再用static_cast)。
- 任意对象指针
- 引用重解释 :
- 将某一类型的引用
T1转换为另一类型引用的语法(不创建临时对象,无拷贝)。
- 将某一类型的引用
- 函数指针与
void*:- 在部分实现(如 POSIX 兼容系统)中,函数指针可与
void*或其他指针互转。
- 在部分实现(如 POSIX 兼容系统)中,函数指针可与
严格别名规则与安全性风险
- 类型可访问性 :
- 程序若试图通过与其动态类型不兼容的 glvalue 读取或修改对象的值,会导致未定义行为 。
- 唯一特例:
char、unsigned char或std::byte可以作为任何对象的字节序列进行访问。
- 严格别名的目的 :
- 编译器基于此规则进行基于类型的别名分析 ,假设不相关类型的指针不会指向同一块内存。
- 安全替代方案 :
- 当需要将一个对象的底层字节解释为另一种类型的值时,应使用
std::memcpy或std::bit_cast(自 C++20 起),而不是直接用reinterpret_cast解引用。
- 当需要将一个对象的底层字节解释为另一种类型的值时,应使用
4.10 字面量
字面量是直接在源码中表示固定值的符号。C++ 支持多种类型的字面量,用于在编译期直接生成对应的 prvalue。
-
布尔字面量:布尔字面量用于在代码中直接表达布尔真假值。
- 基本语法 :
true(真)false(假)
- 类型与值类别
true和false是 C++ 的关键字。- 它们的类型均为
bool。 - 它们属于 prvalue(纯右值) 。
- 基本语法 :
-
整型字面量:整型字面量允许在源码中直接使用数值
- 类别
- 十进制(Decimal) :以非零数字开头(如
42)。 - 八进制(Octal) :以数字
0开头(如052)。 - 十六进制(Hexadecimal) :以
0x或0X开头(如0x2a)。 - 二进制(Binary,自 C++14) :以
0b或0B开头(如0b101010)。
- 十进制(Decimal) :以非零数字开头(如
- 数字分隔符(自 C++14) :可在数字之间插入单引号
'作为分隔符(如18'446'744),编译器会将其忽略。 - 整型后缀 :字面量可以附加后缀来显式指定其类型或符号性(不区分大小写,但
ll或LL必须成对且大小写一致):- 无符号后缀 :
u或U。 - 长整型后缀 :
l或L。 - 超长整型后缀(自 C++11) :
ll或LL。 - 大小/尺寸后缀(自 C++23) :
z或Z(对应std::size_t或其有符号版本)。
- 无符号后缀 :
- 字面量类型的推导规则 :字面量的类型由数值进制 、字面量后缀 以及数值能否容纳 共同决定:
- 无后缀情况:
- 十进制 :按
int->long int->long long int顺序匹配第一个能容纳该值的类型。 - 非十进制(八/十六/二进制) :按
int->unsigned int->long int->unsigned long int->long long int->unsigned long long int顺序匹配。 - 有后缀情况 :根据指定的后缀类型集合(如
u,l,ll,z及其组合),从候选类型列表中按顺序寻找第一个能够容纳该数值的类型。
- 重要注意事项
- 没有负的字面量 :像
-1这种表达式实际上是对正数字面量1应用了一元负号运算符 ,可能会引发隐式类型转换。 - 最大munch陷阱 :以字母
e或E结尾的十六进制字面量如果紧跟+或-运算符(如0xE+2.0),会被错误解析为一个预处理数字标记而导致编译错误,必须用空格或括号隔开(如0xE + 2.0)。
- 没有负的字面量 :像
- 类别
-
浮点字面量 :用于在源码中直接表示一个编译期常量浮点数。支持十进制 和十六进制(自 C++17 起)两种表示形式:
- 十进制浮点字面量 :
- 无小数点,但必须包含指数(如
1e10,1e-5L)。 - 有小数点,指数可选(如
1.,1.e-2)。 - 分数形式,小数点前后均可有数字,指数可选(如
3.14,.1f,0.1e-1L)。
- 无小数点,但必须包含指数(如
- 十六进制浮点字面量(自 C++17) :
- 以
0x或0X开头。 - 指数强制要求 :十六进制浮点字面量的指数部分(以
p或P引导)绝不能省略 (如0x1ffp10,0x1.p0,0xa.bp10l)。
- 以
- 数字分隔符(自 C++14) :允许在数字间插入单引号
'作为分隔符(如3.141'592'653f),编译时会被忽略。 - 指数与后缀规则
- 指数部分 :
- 十进制使用
e或E(基数为 10,如123e4表示 123×104)。 - 十六进制使用
p或P(基数为 2,如0x1.4p3表示十六进制分数 1.25×23=10.0)。
- 十进制使用
- 类型后缀与对应类型 :
- 无后缀 :
double f/F:floatl/L:long doublef16/F16:std::float16_t(自 C++23)f32/F32:std::float32_t(自 C++23)f64/F64:std::float64_t(自 C++23)f128/F128:std::float128_t(自 C++23)bf16/BF16:std::bfloat16_t(自 C++23)
- 无后缀 :
- 指数部分 :
- 十进制浮点字面量 :
-
字符字面量 :字符字面量使用单引号
'括起,用于表示单个字符或特定的字符编码值。根据前缀的不同,C++ 支持以下几种形式:- 普通字符 :
- 类型为
char(注意:C 语言中字符常量类型为int,而 C++ 中为char)。 - 其值对应执行字符集(或普通字面量编码)中的表示形式。
- 类型为
- UTF-8 字符 :
- 类型在 C++20 及以后为
char8_t(C++17 及之前为char)。 - 要求对应的码点能用单个 UTF-8 编码单元表示(即范围在
0x0 ~ 0x7F内)。
- 类型在 C++20 及以后为
- UTF-16 字符 :
- 类型为
char16_t。 - 码点范围必须在
0x0 ~ 0xFFFF内(能用单个 UTF-16 编码单元表示)。
- 类型为
- UTF-32 字符 :
- 类型为
char32_t。 - 值等于其对应的 ISO/IEC 10646 统一码点值。
- 类型为
- 宽字符 :
- 类型为
wchar_t。 - 值对应执行宽字符集中的编码值。
- 类型为
- 多字符字面量 :
- 属于条件支持特性,类型为
int(普通多字符)或wchar_t(宽多字符),其具体数值由实现定义(通常编译器会将各个字符填入整数的连续字节中)。
- 属于条件支持特性,类型为
- 普通字符 :
-
字符串字面量:用于在源码中表示一个字符序列,具有静态存储持续时间。
- 根据前缀及是否为原始字符串分类
- 普通字符串字面量 :
"s-char-seq",类型为const char[N]。 - 宽字符串字面量 :
L"s-char-seq",类型为const wchar_t[N]。 - UTF-8 字符串字面量(自 C++11) :
u8"s-char-seq",类型为const char[N](直到 C++20)或const char8_t[N](自 C++20 起)。 - UTF-16 字符串字面量(自 C++11) :
u"s-char-seq",类型为const char16_t[N]。 - UTF-32 字符串字面量(自 C++11) :
U"s-char-seq",类型为const char32_t[N]。 - 原始字符串字面量(Raw String,自 C++11) :带有
R前缀(如R"d-char-seq(r-char-seq)d-char-seq"),其中的任何字符都不会被转义处理,所见即所得。
- 普通字符串字面量 :
- 存储、修改与类型转换限制
- 生命周期 :字符串字面量具有静态存储期 。尝试修改字符串字面量对象的内容属于未定义行为(UB) 。
- 指针转换规则 :自 C++11 起,字符串字面量不能 隐式转换为非
const的指针(如char*),若需转换必须显式使用const_cast。 - 空字符终止 :字符串字面量会自动在末尾追加一个空字符
\0(U+0000),因此数组大小N会包含该终止符。若字面量中间包含嵌入的\0,则整个数组会包含多个字符串。
- 字符串拼接 :在翻译阶段 6(预处理之后),相邻的字符串字面量会自动合并为一个:
- 相同类型的字符串直接合并。
- 不同类型的窄字符串与非窄字符串相邻时,合并后采用非窄字符串的类型。
- 自 C++23 起,不允许进行不兼容的混合拼接(直接判定为 ill-formed)。
- 原始字符串字面量
- 使用定界符
R"d-char-seq(r-char-seq)d-char-seq"。 - 中间的反斜杠 `` 和双引号等均无需转义,非常适合编写正则表达式或包含大量特殊字符的文本。
- 使用定界符
- 根据前缀及是否为原始字符串分类
-
空指针字面量(
nullptr):自 C++11 起引入的指针字面量,用于统一和安全地表示空指针。- 基本语法 :
nullptr(它是一个关键字)。 - 类型 :
std::nullptr_t。 - 值类别 :属于 prvalue(纯右值) 。
- 隐式转换规则
nullptr可以隐式转换为任意的指针类型 或成员指针类型的空指针值。- 与传统的宏
NULL相比,nullptr拥有明确的类型,能够有效避免在函数重载(如同时重载了指针类型和整型)时产生的二义性问题。
- 基本语法 :
-
用户自定义字面量:允许为整型、浮点型、字符型和字符串字面量定义自定义后缀,从而直接生成自定义类型的对象。
- 基本语法形式 :
- 整型字面量加后缀:
12_km - 浮点字面量加后缀:
0.5_Pa - 字符字面量加后缀:
'c'_X - 字符串字面量加后缀:
"abd"_L或u"xyz"_M
- 整型字面量加后缀:
- 后缀命名规则与限制
- 下划线开头 :所有自定义后缀必须以单下划线
_开头(不带下划线的后缀属于标准库保留)。 - 禁止双下划线 :后缀中不能包含双下划线
__(如_0_等保留格式)。 - 宏与冲突处理 :如果一个记号既匹配常规字面量又匹配用户自定义字面量,编译器会优先当作常规字面量 处理(例如无法重载
123LL中的LL)。
- 下划线开头 :所有自定义后缀必须以单下划线
- 字面量运算符的重载规则 :当编译器遇到自定义后缀
X时,会通过无限定名查找寻找operator ""X。根据字面量类型,其允许的参数列表有严格限制:- 整型字面量 :优先匹配
unsigned long long int;若无,则可退化匹配原始字面量 (const char*)或数值字面量模板 (template<char...>)。 - 浮点字面量 :优先匹配
long double;若无,同样可退化匹配原始字面量 (const char*)或数值字面量模板。 - 字符字面量 :支持
char,wchar_t,char8_t(自 C++20),char16_t,char32_t。 - 字符串字面量 :调用形式为
operator ""X(str, len),参数通常为(const CharT*, std::size_t),或自 C++20 起支持的字符串字面量模板。
- 整型字面量 :优先匹配
- 常见陷阱与注意事项
- 最大 munch冲突 :
- 以
e,E,p,P结尾的整型或浮点自定义字面量,若后面紧跟+或-运算符(如1.0_E+2.0),必须加空格或括号隔开,否则会形成无效的预处理标记。 - 自定义字面量后若紧跟点运算符(如
4s.count()),也必须加空格(如4s .count()或(4s).count())。
- 以
- 字符串拼接 :在翻译阶段 6,相邻的自定义字符串字面量会进行拼接,但所有参与拼接的字面量最多只能有一个后缀,否则报错。
- 最大 munch冲突 :
- 基本语法形式 :
4.11 转义序列
转义序列用于在字符字面量 和字符串字面量中表示特殊字符。它们均以反斜杠 `` 开头。
主要转义序列分类
- 简单转义序列 :
- 用于表示不可见字符或具有特殊语法的符号。
- 常用示例:
'(单引号,ASCII 0x27)"(双引号,ASCII 0x22)\(反斜杠,ASCII 0x5c)\n(换行符 Line Feed,ASCII 0x0a)\r(回车符 Carriage Return,ASCII 0x0d)\t(水平制表符 Horizontal Tab,ASCII 0x09)\0(空字符 Null,最常用于以零结尾的字符串终止符)
- 数字转义序列 :
- 八进制 :
\nnn(1~3位八进制数)或自 C++23 起支持的\o{n...}(任意长度八进制数)。 - 十六进制 :
\xn...(任意长度十六进制数)或自 C++23 起支持的\x{n...}。十六进制没有长度限制,直到遇到非十六进制字符为止。
- 八进制 :
- 通用字符名 :
- 用于在字面量中直接表达 Unicode 字符。
- 常见形式:
\unnnn(4位十六进制,对应码点U+nnnn)\Unnnnnnnn(8位十六进制,对应码点U+nnnnnnnn)- 自 C++23 起支持的
\u{n...}和命名通用字符名\N{NAME}(例如通过标准 Unicode 字符名称直接指定)。
- 限制规则 :通用字符名不能用于表示基本源字符集中的大部分普通字符或控制字符(如
0x0-0x1F等),且代理区码点(0xD800-0xDFFF)属于非法格式。