C++ 规范参考(中)

23 阅读23分钟

C++ 规范参考:基础概念、关键字、预处理器、表达式

C++ 规范参考:声明、初始化、函数、语句

C++ 规范参考:类、模板、异常

5. 声明

  • 声明 :用于向 C++ 程序引入(或重新引入)名称。定义是足以使用该实体的特殊声明。
  • 主要形式:包括函数定义、模板声明、显式实例化/特化、命名空间定义、链接规范、属性声明、空声明、块声明及简单声明。

简单声明:是最常见的形式,用于引入、创建并可选地初始化一个或多个标识符(通常是变量):

decl-specifier-seqinit-declarator-list(optional) ;\text{decl-specifier-seq} \quad \text{init-declarator-list}_\text{(optional)} \ ;

  • decl-specifier-seq(声明说明符序列) :由一系列以空格分隔的说明符组成(顺序任意),包括:
    • 类型说明符 :如 char, int, double, 类名、枚举名、autodecltype 等。在一个序列中通常只能有一个类型说明符(特例:signed/unsigned 可与 char/int 组合,short/long 可与 int 组合,long 可与 double 或自身组合)。
    • 存储类说明符 :如 static, extern, thread_local, mutable 等。
    • 函数/其他说明符:如 inline, virtual, explicit, friend, constexpr, consteval, constinit
  • init-declarator-list(初始化声明符列表) :由逗号分隔的 init-declarator 组成,每个声明符独立生效(如 int a = 1, b = 2; 等价于两条独立声明)。

声明符:通过指针、引用、数组或函数修饰符修改基础类型,主要形式有:

  1. 名字与限定名unqualified-idqualified-id
  2. 指针* cv declarator(例如 int *p;)。
  3. 成员指针nested-name-specifier * cv declarator(例如 int C::*ptr;)。
  4. 左值引用& declarator(例如 int &r;)。
  5. 右值引用(自 C++11)&& declarator(例如 int &&rr;)。
  6. 数组noptr-declarator [ constexpr ]
  7. 函数noptr-declarator ( parameter-list )(可包含尾随返回类型)。

重要注意事项

  • 自动变量的生命周期:具有自动存储期的变量在执行到其声明语句时初始化,并在离开所在块时按初始化的相反顺序被销毁。
  • 名称隐藏 :当块声明中引入的标识符与外层块的名称相同时,外层声明在当前块的剩余部分会被隐藏。

5.1 冲突声明

在 C++ 中,除非另有说明,两个声明不能(重新)引入相同的实体。当代码中存在违背这一原则的声明时,程序会直接构成语法错误。

潜在冲突声明

  • 定义:两个声明如果对应声明了不同的实体,则它们属于“潜在冲突”。
  • 核心规则:如果在任何作用域中,一个名称被绑定到两个潜在冲突的声明 AB(其中 A 先于 B 出现,且 B 不是 C++26 引入的名称无关声明),则程序为不良构

常见冲突场景与示例:当两个声明的名称相同、相互对应,但类型或实体种类不一致时,就会触发冲突或病式错误:

  • 类型不匹配的重声明
    void g();      // #1
    int g();       // 错误:与 #1 属于同一实体,但返回类型不同(重声明冲突)
    
  • 实体类别冲突
    void h();      // #2
    namespace h {} // 错误:与 #2 对应但声明了不同的实体类别(命名空间 vs 函数)
    

5.2 存储类说明符

名称声明说明符序列的一部分。结合名称的作用域 ,它们共同控制一个名称的两个独立属性:

  1. 存储持续时间
  2. 链接性

存储类说明符关键字与限制 合法的存储类说明符包括:staticthread_local(自 C++11)、externmutable

  • auto:在 C++11 之前作为存储类说明符,自 C++11 起已转为类型推导关键字。
  • register:提示变量常驻寄存器(用法已废弃并在 C++17 中移除)。
  • mutable:不影响存储持续时间,专用于 const/volatile 上下文(允许在 const 成员函数中修改成员)。
  • 组合限制:在同一个 decl-specifier-seq 中最多只能出现一个存储类说明符,特例是 thread_local 可以与 staticextern 组合(自 C++11)。

四种存储持续时间

  • 静态存储持续时间
    • 适用于:命名空间作用域的变量,或显式声明为 static / extern 的变量。
    • 对象的生命周期贯穿整个程序运行期间
  • 线程存储持续时间
    • 适用于:所有声明为 thread_local 的变量(自 C++11)。
    • 对象的生命周期贯穿对应线程的整个生命周期(每个线程拥有独立的实例)。
  • 自动存储持续时间
    • 适用于:块作用域内未显式声明为 staticthread_localextern 的变量,以及函数形参。
    • 对象的生命周期持续到离开其创建所在的块或形参销毁为止。
  • 动态存储持续时间
    • 适用于:通过 new 表达式创建的对象或隐式创建的对象。

链接性

  • 无链接 :仅能在同一作用域内被重新声明(如未显式声明为 extern 的块作用域变量、局部类、typedef 等)。
  • 内部链接 :可在同一翻译单元的其他作用域中重新声明(如 static 命名空间变量/函数、匿名命名空间内的所有名称)。
  • 外部链接 :可在其他翻译单元中重新声明并进行跨文件链接(如普通的全局变量、非 static 函数、extern 变量)。
  • 模块链接 :自 C++20 引入,附加到具名模块且未导出的名称。

静态局部变量的线程安全初始化

  • 生命期与初始化时机:具有静态或线程存储持续时间的块变量,会在控制流第一次执行到其声明时进行初始化(若非常量初始化)。
  • 并发安全(自 C++11) :若多个线程同时尝试初始化同一静态局部变量,初始化过程保证只执行一次(底层通常采用类似于双重检查锁定的机制,具备线程安全性)。

5.3 翻译单元局部实体

  • 引入版本:C++20(伴随模块 Modules 特性引入)。
  • 核心目的:防止本应局限于单个翻译单元内部的实体(如拥有内部链接的类、函数或变量)通过接口被暴露并跨翻译单元使用,从而破坏封装或引发 ODR(单定义规则)违背。

TU-local 实体:如果满足以下条件之一,即被视为 TU-local(翻译单元局部)

  1. 拥有内部链接的类型、函数、变量或模板;或者没有链接名但被声明/在 lambda 表达式中引入于 TU-local 实体定义内部的实体。
  2. 无名类型:在类说明符、函数体或初始化器之外定义,或由仅用于声明 TU-local 实体的定义性类型说明符(如类/枚举说明符)引入的类型。
  3. 模板特化:TU-local 模板的特化、带有任意 TU-local 模板实参的模板特化,或是其声明属于“暴露”的模板特化。

暴露: 当一个非 TU-local 实体的声明(如模块接口中的导出实体)“引用/命名”了 TU-local 实体时,即构成了暴露

  • 主要违规场景:在模块接口单元(非私有模块片段内)或模块分区中,如果某个非 TU-local 实体的声明或推导指引是一个暴露,程序将直接构成为语法错误 ;在其他上下文中则会被弃用(deprecated)。
  • 跨翻译单元限制:如果一个翻译单元中的声明命名了另一个非头文件单元的翻译单元中的 TU-local 实体,程序同样为病式。

5.4 语言链接

  • 基本定义:语言链接(extern "string-literal")用于在用不同编程语言编写的程序单元之间提供链接支持(自 C++20 起也可用于解除声明与模块的绑定)。
  • 支持的语言规格:C++ 标准保证支持两种语言链接:
    1. "C++":默认的语言链接。
    2. "C":允许 C++ 程序链接到 C 语言编写的函数,或在 C++ 中定义可供 C 调用的函数。
extern "C" {
    // 声明序列(可选)
}

extern "C" declaration; // 单个声明
  • 注意:语言规范只能出现在命名空间作用域,且语言规范的大括号不建立新的作用域

底层机制与独立性

  • 每个函数类型、具有外部链接的函数名以及具有外部链接的变量名都具有语言链接属性。它封装了调用约定、名字修饰算法等。
  • 类型与名称的独立性
    • 函数名称的语言链接决定名字修饰。
    • 函数类型的语言链接决定调用约定。
    • 两者相互独立(例如,可以声明一个名称具有 C++ 链接、但其函数类型具有 C 链接的指针或函数)。

冲突规则与重声明

  • 链接不一致报错:如果一个实体的两次声明赋予了不同的语言链接,程序为语法错误
  • 继承规则:没有语言链接规范的重声明会隐式继承该实体及其类型的原有语言链接。
  • 针对 "C" 链接的特殊规则
    • 位于 extern "C" 块中的类成员、带尾随 requires 子句的友元函数或非静态成员函数,其自身的语言链接保持 "C++" (但其形参类型仍保持 "C")。
    • 具有 "C" 语言链接的函数或变量声明,如果与全局作用域中同名的同类实体(变量对变量、函数对函数)匹配,即使目标作用域不同,也可能被视为声明了同一个实体,但必须满足类型一致的严格限制。

5.5 命名空间

  • 作用:命名空间用于在大型项目中防止命名冲突。
  • 全局命名空间 :所有声明在命名空间块外部的实体都属于全局命名空间,可通过前置的 :: 显式引用。
  • 多次定义:允许多个同名命名空间块(扩展命名空间定义),它们内部的所有声明都归属于同一个命名空间作用域。

核心语法形式

  1. 具名命名空间namespace ns-name { declarations }
  2. 内联命名空间(自 C++11)inline namespace ns-name { declarations }(其内部声明对外部命名空间可见)。
  3. 无名命名空间namespace { declarations }(具有内部链接,其成员在当前翻译单元全局可见)。
  4. 嵌套命名空间(自 C++17)namespace A::B::C { ... }

Using 声明与 Using 指令

  • Using 声明(using-declaration
    • 将其他作用域中特定的名称引入到当前的声明区域中(如 using A::x;)。
    • 引入的名称遵循常规的隐藏和重载规则;在命名空间作用域中,重复引入相同的名称是允许的(自 C++17)。
  • Using 指令(using-directive
    • 形式:using namespace ns-name;
    • 不会将名称真正加入到当前声明区域中,而是使指定命名空间中的所有名称通过无名查找可见。它是可传递的(若 A 引入了 B,则 A 中可见 B 的内容)。

关键注意事项

  • 头文件规范:严禁在头文件的文件作用域(特别是全局作用域)中编写 using namespace std; 等指令,以避免污染包含该头文件的其他翻译单元并引发命名冲突。

命名空间别名

  • 作用:命名空间别名为程序员提供了一种为命名空间定义替代名称的方法。
  • 常见用途:通常用作长命名空间或深度嵌套命名空间的便捷简写形式。
  • 核心语法形式
    namespace alias_name = ns_name;                 // (1) 基础别名
    namespace alias_name = ::ns_name;               // (2) 显式全局限定别名
    namespace alias_name = nested_name::ns_name;    // (3) 嵌套命名空间别名
    namespace alias_name = splice-specifier;        // (4) 拼接指示符别名(自 C++26)
    
    • alias_name:一个必须是此前未被使用过的标识符。
    • 有效范围:别名的有效持续时间为其被引入所在作用域的整个生命周期。

5.6 引用声明

  • 基本定义:引用声明将具名变量声明为引用,即某个已有对象或函数的别名。
  • 主要形式
    1. 左值引用S& D;(声明 D 为指向类型 S 的左值引用)
    2. 右值引用S&& D;(自 C++11 起,声明 D 为指向类型 S 的右值引用)
  • 基本约束
    • 引用在定义时必须被初始化,以指向一个有效的对象或函数。
    • 不能形成“指向 void 的引用”。
    • 引用类型不能在顶层进行 cv-qualifier 修饰(如 int& const 在语法上无效或被忽略)。
    • 引用不是对象,它们不一定占用独立存储空间(除非作为类的数据成员)。因此,不存在引用数组、指向引用的指针、以及对引用的引用(但可以通过模板或 typedef 触发引用坍缩)。

引用坍缩(自 C++11):当在模板或 typedef 中通过类型操作形成“对引用的引用”时,遵循以下折叠规则:

  • 右值引用指向右值引用&& &&)折叠为右值引用&&)。
  • 所有其他组合& && &&&& &)均折叠为左值引用&)。
  • 这一规则与函数模板参数推导(T&&)共同构成了 std::forward 的底层实现基础。

左值引用与右值引用的特性

  • 左值引用
    • 用于给已有对象取别名,可绑定到左值。
    • 非 const 左值引用可用于修改对象,常用于实现按引用传参。
    • 若函数返回类型为左值引用,则该函数调用表达式为左值表达式,可被赋值。
  • 右值引用
    • 专用于绑定到右值(纯右值 prvalue 和将亡值 xvalue)。
    • 生命期延长:右值引用(以及指向 const 的左值引用)可以延长临时对象的生命周期。
    • 移动语义与完美转发:通过重载右值引用版本,能够自动识别并支持移动构造函数、移动赋值运算符等,避免不必要的拷贝。
    • 注意:具名的右值引用变量在表达式中使用时会被视为左值

转发引用:一种特殊的引用,能够保留函数实参的值类别 ,常用于泛型编程中配合 std::forward 实现完美转发。满足以下两种形式之一:

  1. 函数模板的形参,且该函数模板参数为未经 cv-qualified 修饰的类型模板参数的右值引用(如 template<class T> void f(T&& x);)。
  2. auto&& (例如 auto&& x = ...,但不包括来自花括号初始化列表的推导)。

悬空引用

  • 定义:如果所引用的对象生命周期已经结束,但引用依然保持可访问状态,则称为悬空引用。
  • 后果:解引用或读取悬空引用会导致未定义行为 。常见于从函数中返回局部变量的左值引用。

5.7 指针声明与指针类型

用于声明一个指针或成员指针类型的变量。语法形式主要分为两大类:

  1. 普通指针声明
    • 形式:S* attr(optional) cv(optional) declarator;
    • 声明 D 为指向类型 S 的指针。
  2. 成员指针声明
    • 形式:S C::* attr(optional) cv(optional) declarator;
    • 声明 D 为指向类 C 的非静态成员的指针(该类不能是匿名联合体)。
  • 基本约束
    • 不存在指向引用的指针,也不存在指向位域的指针。
    • 指针的 cv 限定符若写在 * 之后,修饰的是指针本身(如 T* const);若写在 * 之前(作为声明说明符序列的一部分),则修饰的是被指向的对象(如通道 const T*)。

指针分类:每个指针类型的值必然属于以下其中一种:

  1. 指向对象或函数的指针:代表对象内存中第一个字节的地址,或函数地址。
  2. 越界指针 :指向对象存储空间末尾之后的第一个字节的地址(可用于数组遍历,但解引用会导致未定义行为)。
  3. 空指针值 :不指向任何对象或函数。
  4. 无效指针值 (例如生命周期已结束的对象的指针)。

特殊指针类型

  • void*(空指针)
    • 任何类型的对象指针都可以隐式转换为 void*,且指针值保持不变。
    • void* 具有与 char* 相同的尺寸、表示方式和对齐要求,常用于 C 接口(如 std::malloc)。
  • 函数指针
    • 指向非成员函数或静态成员函数。
    • 函数指针是对象,可以被存入数组、拷贝或赋值(与函数引用不同)。
  • 成员指针
    • 数据成员指针:形如 int C::* p = &C::m;,通过 c.*pcp->*p 访问。
    • 成员函数指针:形如 void (C::* p)(int) = &C::f;,必须通过对象或指针结合 .*->* 进行调用。

5.8 数组声明与数组类型

  • 基本定义:数组声明用于声明一个由 NN 个连续分配的、类型为 T 的对象组成的数组对象。
  • 基本语法
    • 形式:T a[N];(其中 N 是一个整型常量表达式或自 C++14 起类型为 std::size_t 的转换常量表达式,且值大于 0)。
    • 声明符形式:noptr-declarator [expr(optional)] attr(optional)。若 noptr-declarator*&&& 开头,必须加括号,否则整个声明符会被当成指针或引用声明。
  • 基本约束
    • 元素类型可以是任意基本类型(void 除外)、指针、成员指针、类、枚举或其他已知边界的数组(构成多维数组)。
    • 不存在引用数组,也不存在函数数组。
    • 数组元素不能是未知边界的数组(因此多维数组除了第一维外,其他维度不能为空)。
    • 零大小数组:通过 new[] 表达式可以创建大小为 0 的数组(无元素,但仍需 delete[] 释放)。

不可赋值性与cv限定符

  • 无法整体赋值:数组对象作为整体不能被修改。尽管它们是左值(可以取址),但不能作为赋值运算符的左操作数a = b; 是非法的,必须通过包裹它的结构体或逐个元素赋值来完成)。
  • cv限定符:应用于数组类型(通过 typedef 或模板)时,实际上是应用到了其元素类型上。

数组到指针的隐式转换

  • 隐式退化:数组的左值或右值可以隐式转换为指向其首元素的指针。
  • 发生场景:当数组出现在期望指针而非数组的上下文中时。
  • 特例(不发生退化) :当数组作为 sizeofdecltype 的操作数,或用于取址运算符(&a),或者在函数中作为数组引用参数传递(int (&a)[3])时,不会发生退化。
  • 多维数组退化:多维数组在退化时只会进行一次退化,转换为指向其第一维子数组的指针(例如 int b[2][3] 退化为 int (*)[3],而不是 int**)。

未带边界的数组

  • 形式:如果在声明中省略了大小 expr(如 extern int x[];),则声明的是“未知边界的数组”,属于不完整类型
  • 聚合初始化特例:带有聚合初始化器的定义会自动推导边界(如 int a[] = {1, 2, 3}; 声明为 3 个 int 的数组)。
  • C++20 更新:自 C++20 起,可以从已知边界的数组或指针初始化或赋值未知边界数组的引用与指针。

5.9 结构化绑定声明

  • 基本定义:结构化绑定(自 C++17 起引入)将一组指定的名称绑定到初始化器的子对象或元素上。
  • 主要作用:类似于引用,结构化绑定是现有对象的别名,但它不需要是引用类型
  • 基本语法
    attr(optional) decl-specifier-seq ref-qualifier(optional) [ sb-identifier-list ] initializer;
    
    • decl-specifier-seq:通常包含 auto,还可以结合 constexprstaticthread_localconst 等。
    • ref-qualifier:可选的 &&&,用于控制底层隐藏变量的引用类别。
    • sb-identifier-list:由逗号分隔的标识符列表,用于命名绑定的子对象。

底层绑定过程与三大处理分支

声明会首先引入一个隐藏的变量(记为 e)来持有初始化器的值,然后根据 e 的类型 EE(即 std::remove_reference_t<decltype((e))>)采用以下三种方式之一进行绑定:

  1. 绑定数组

    • EE 是已知边界的数组类型,则每个标识符分别绑定到数组对应的元素(如同创建了左值引用)。
    • 数组如果是 cv 限定的,其元素也将带有相同的限定。
  2. 元组式绑定协议

    • EE 是非联合体类类型,且 std::tuple_size<E> 是一个包含 value 成员的完整类型,则采用此协议。
    • 通过类成员 get<I>() 或参数依赖查找(ADL)的 get<I>(e) 获取对应元素,并结合 std::tuple_element<I, E>::type 确定各个绑定的引用类型。
  3. 绑定至数据成员

    • EE 是非联合体类类型,但 std::tuple_size<E> 不是完整类型,则名称将依次绑定到 EE 的所有可访问非静态数据成员(按声明顺序)。
    • 每一个结构化绑定成为对应成员的 lvalue 引用。

关键特性与注意事项

  • 生命期延长:如果带有 ref-qualifier(如 const auto&)且初始化表达式是纯右值(prvalue),底层隐藏变量 e 会绑定到临时对象上,从而延长临时对象的生命周期
  • decltype 行为:对结构化绑定变量应用 decltype 得到的是该绑定的引用类型 。在元组式绑定中,这对应 std::tuple_element 的类型,可能不是引用。
  • 限制
    • 结构化绑定本身不能被 cv-qualifier 或概念修饰(修饰符作用于隐藏变量 e 而非绑定的名称)。
    • 在 C++20 之前,结构化绑定不能直接被 Lambda 表达式捕获(C++20 起已支持)。

5.10 枚举声明

  • 基本定义:枚举是一种独立的类型,其值受限於一个特定的整数范围(包含若幹显式命名的常量,即“枚举器”)。
  • 两大种类
    1. 无作用域枚举 :使用 enum 关键字声明。枚举器直接可见於外层作用域,且其值会隐式提升或转换为整型。
    2. 作用域枚举 :使用 enum classenum struct 关键字声明(自 C++11 起)。枚举器包含在枚举自身的作用域内,无隐式整型转换

底层类型

  • 固定底层类型:自 C++11 起,无作用域和作用域枚举都可以通过冒号显式指定底层整型类型(如 enum color : int { ... };)。
  • 非固定底层类型的默认规则:若未指定,无作用域枚举会采用一个能容纳所有枚举器值的、不大于 int(或 unsigned int)的实现定义整型。
  • 不透明枚举声明
    • 形式:enum-key attr(optional) enum-head-name enum-base(optional) ;(自 C++11 起)
    • 允许在定义枚举值之前提前声明枚举类型,使其成为已知大小的完整类型。

枚举值的计算与自增规则

  • 如果第一个枚举器没有指定 = constant-expression,其关联值默认为 0
  • 之后的枚举器若未显式赋值,其值为前一个枚举器的值加 1

列表初始化(自 C++17 起)

  • 在满足特定条件时(如使用直接列表初始化且初始化列表仅含单个非窄化元素),作用域枚举或具有固定底层类型的无作用域枚举可以直接从整数进行初始化,而无需显式 static_cast
    enum class Handle : std::uint32_t { Invalid = 0 };
    Handle h{42}; // C++17 起合法
    

using enum 声明(自 C++20 起)

  • 作用:将指定枚举类型的所有枚举器名称引入当前作用域(在类作用域中则将其添加为类成员)。
    enum class fruit { orange, apple };
    struct S {
        using enum fruit; // 在 S 中引入 orange 和 apple
    };
    

5.11 内联说明符

  • 函数内联:当 inline 用于函数的声明说明符序列时,将其声明为内联函数
    • 隐式内联
      • 定义在类/结构体/联合体内部的函数(无论是成员函数还是友元函数),除非附加到具名模块(C++20 起),否则隐式为内联函数。
      • 首次声明时带有 constexprconsteval(C++20 起)的函数隐式为内联函数。
      • 被删除的函数隐式为内联函数(允许其定义出现在多个编译单元中)。
  • 变量内联(自 C++17 起) :当 inline 用于具有静态存储期的变量(静态类成员或命名空间作用域变量)时,将其声明为内联变量
    • 首次声明时带有 constexpr 的静态数据成员隐式为内联变量。

核心属性与规则:带有外部链接的内联函数或内联变量满足以下规则:

  • 多定义规则(ODR 宽容) :允许程序中存在多个相同的定义,只要每个定义出现在不同的编译单元中。这使得头文件中定义内联函数/变量成为可能。
  • 一致性要求:必须在每一个使用它的编译单元中都声明为 inline
  • 地址唯一性:在所有编译单元中,它具有相同的地址
  • 内部静态与类型共享
    • 内联函数中的函数局部静态对象在所有编译单元中共享(指向同一个对象)。
    • 内联函数定义中定义的类型在所有编译单元中也是相同的。
  • 命名空间作用域下的 const 变量:内联 const 变量默认具有外部链接(这与普通的非内联 const 变量不同)。

关键字含义的演变

  • 原意:向优化器指示建议进行内联替换 ,以避免函数调用的开销,但这可能导致可执行文件体积增大。
  • 现代语义(自 C++98 起)inline 的核心现代意义已变为“允许在多个编译单元中存在相同的定义”,并在 C++17 扩展到了变量。
  • 编译器的自主权:内联替换与否完全由编译器优化决定(编译器可以内联未标记 inline 的函数,也可以为标记了 inline 的函数生成普通函数调用)。

主要约束与注意事项

  • 作用域限制inline 说明符不能用于块作用域(即函数内部)的函数或变量声明。
  • 重复声明限制:若函数或变量在编译单元中已被定义为非inline,则不能再用 inline 重新声明。
  • 默认实参一致性:如果在不同的编译单元中声明内联函数,其最终累积的默认实参集合必须完全相同。

5.12 汇编声明

  • 基本定义asm 声明允许在 C++ 程序中嵌入汇编语言源代码。
  • 条件支持与实现定义:该特性是条件支持的由实现定义 。这意味着它可能不被某些编译器支持,且即使提供,其具体含义和规则也没有统一的标准。
  • 常规语法(直到 C++26)
    attr(optional) asm ( string-literal ) ;
    
  • 现代语法(自 C++26 起)
    attr(optional) asm ( balanced-token-seq ) ;
    
    • balanced-token-seq:指括号、方括号和花括号完全平衡的一系列记号(Token)。
    • 字符串字面量或记号序列通常代表一段短小的汇编语言程序,在执行该声明时运行。
  • 作用域与上下文
    • 作为一个块声明,asm 声明既可以出现在块内部(如函数体或其他复合语句中),也可以出现在块外部(全局作用域)。
    • 不同的 C++ 编译器(如 GCC、Clang、MSVC 等)对于 asm 声明具有极大的差异化规则,以及与周围 C++ 代码交互的不同约定。

5.13 cv 限定符

cv 限定符:用于指定对象的常量性(const)或易变性(volatile)。任何非函数、非引用的类型都衍生出四种互相关联的 cv 版本:

  • 无限定符版本
  • const 限定版本
  • volatile 限定版本
  • const volatile 限定版本

相同表示与对齐:同一类型的三种带修饰版本与原版本具有相同的内存表示和对齐要求。数组类型的 cv 限定性由其元素类型决定。

对象语义

  • const 对象
    • 类型为 const 限定的对象或 const 对象的非 mutable 子对象。
    • 不能被修改:直接修改会导致编译错误,间接修改(通过非 const 指针/引用)会导致未定义行为(UB)
  • volatile 对象
    • 类型为 volatile 限定的对象、其子对象或 const volatile 对象的 mutable 子对象。
    • 优化屏障:通过 volatile glvalue 进行的每次读写访问都被视为可见的副作用,编译器不能将其优化掉或重排序。适用于与信号处理程序(signal handler)通信,但不适用于多线程同步(应使用 std::memory_order)。间接通过非 volatile 访问会导致 UB。
  • 限定符重数限制:在任何 cv 限定符序列中,constvolatile 各最多只能出现一次。

mutable 说明符

  • 基本作用:允许修改被声明为 mutable 的类成员,即使包含该成员的类对象被声明为 const
  • 约束条件
    • 只能用于非静态类成员
    • 不能用于引用类型,也不能与顶层的 const 组合(如 mutable int* const q 是非法的)。
  • 典型用途:用于不影响对象外部可见状态的内部实现细节(如互斥锁 std::mutex、缓存、延迟初始化等)。

限定符偏序与转换规则

  • 限制程度偏序(从小到大): unqualified<const\text{unqualified} < \text{const} unqualified<volatile\text{unqualified} < \text{volatile} unqualified<const volatile\text{unqualified} < \text{const volatile} const<const volatile\text{const} < \text{const volatile} volatile<const volatile\text{volatile} < \text{const volatile}
  • 隐式转换:指向较少限制类型的引用或指针可以隐式转换为指向较多限制(更严苛)类型的引用或指针。
  • 逆向转换:如果要将指向 cv 限定类型的指针/引用转换为指向较少限制类型的指针/引用,必须显式使用 const_cast

外部链接差异:在 C++ 中,非内联、非模板的非本地 const 变量默认具有内部链接(而 C 语言中具有外部链接)。

部分 volatile 用法弃用(自 C++20 起)

  • volatile 左值作为内置自增/自减运算符的操作数。
  • volatile 左值作为内置直接赋值运算符的左操作数(除非在未求值上下文中或丢弃值表达式中)。
  • volatile 对象类型作为函数参数类型或返回类型。
  • 结构化绑定声明中的 volatile 限定符。

5.14 constexpr

  • 基本定义constexpr 声明变量、结构化绑定(C++26 起)或函数的值/返回值可以在编译期常量表达式中被评估
  • 隐式修饰属性
    • const 属性:在对象声明或非静态成员函数(直到 C++14)中使用 constexpr 隐式推导为 const
    • inline 属性:在函数或静态数据成员(C++17 起)的首个声明中使用 constexpr 隐式推导为 inline。若函数或模板包含 constexpr 声明,则其所有声明都必须带有 constexpr

变量要求:变量或变量模板声明为 constexpr 需满足

  • 该声明必须是定义
  • 类型必须是字面类型
  • 声明时必须进行初始化,且其初始化的全表达式必须是常量表达式。
  • 常量析构
    • 不是类类型或其数组;
    • 或为类类型(或其数组),且拥有 constexpr 析构函数,并在假设的析构表达式中满足核心常量表达式要求。

演进与约束: constexpr 函数表示其在提供常量实参时可以在编译期求值,但在运行期调用时与普通函数完全一致。

  • 语法放宽演进历程
    • C++11:限制极严,函数体基本上只能包含单条 return 语句,参数和返回值必须为字面类型,不能为虚函数。
    • C++14:允许包含多条语句、循环、局部变量定义(需已初始化)以及非 const 成员函数。
    • C++20:允许虚函数(virtual)、try-block、未初始化的变量定义、动态内存分配(new/delete)、union 动态成员切换以及 asm 声明。
    • C++23:进一步放宽,允许定义非字面类型的局部变量、goto 语句、未引用的 static 局部变量等;取消了“必须存在至少一组实参使其能在编译期求值”的强约束
    • C++26:允许 constexpr 函数中存在虚继承及从 void* 转换等操作。

构造函数

  • 即使类本身不是字面类型,其构造函数也可以声明为 constexpr(例如 std::shared_ptr 的默认构造函数,以支持常量初始化)。
  • 需确保所有基类和非静态数据成员均按规则正确初始化。

析构函数(自 C++20 起):

  • 允许显式声明为 constexpr。在编译期生命周期结束的对象,其析构函数可在编译期被调用求值。

关键细节与技巧

  • 引用变量:引用可以声明为 constexpr,但其初始化实参必须是引用常量表达式(通常绑定到具有静态存储期的对象)。
  • 运行时异常与汇编:虽然 C++26 起 constexpr 函数内部允许写未捕获的 throwasm 代码,但若在编译期求值时实际执行到了这些路径,仍会导致编译期求值失败。

5.15 consteval

  • 基本定义consteval 说明符(自 C++20 起)用于声明一个立即函数
  • 强制编译期求值:任何对立即函数的潜在求值调用都必须(直接或间接地)在编译期产生一个常量表达式。如果无法在编译期求值,则会导致编译错误。

constexpr 的关系与区别

  • 继承约束:立即函数本质上也是一种 constexpr 函数,因此必须满足 constexpr 函数的所有底层要求。
  • 隐式内联:与 constexpr 类似,consteval 隐式带有 inline 属性。
  • 互斥与一致性
    • 同一个函数声明中不能同时指定 constexprconsteval
    • 该函数的任何重复声明(Redeclaration)也必须同样指定 consteval
  • 禁止的场景consteval 不能应用于析构函数内存分配函数内存释放函数

立即调用与上下文规则

  • 立即调用 :在非立即函数的上下文中对立即函数的调用,必须产生常量表达式。
  • 嵌套调用:在另一个立即函数内部调用立即函数是允许的(因为外层也在编译期求值),即使在编写时它还不是一个完整的常量表达式。
  • 指针/引用限制:可以获取立即函数的指针或引用,但它们不能逃逸出常量表达式求值(即不能将其作为最终的编译期常量结果导出给运行时使用)。

5.16 constinit

  • 基本定义constinit 说明符(自 C++20 起)用于断言变量具有静态初始化(即零初始化和常量初始化),如果实际属于动态初始化,则程序语法错误。
  • 适用对象
    • 具有静态存储期线程存储期的变量。
    • C++26 起,也可应用于结构化绑定声明(此时它作用于声明引入的隐藏底层变量)。

核心规则与一致性要求

  • 初始化声明一致性:若变量在某处声明为 constinit,则其初始化定义也必须带有 constinit。若在初始化时找不到可达的 constinit 声明,程序也是格式错误的(无需诊断)。
  • constexpr 的对比
    • constinit 不能constexpr 同时使用。
    • 引用类型:当声明为引用时,constinit 等价于 constexpr
    • 对象类型constexpr 会强制要求对象具备常量析构并具备 const 限定;而 constinit 不要求常量析构,也不自动添加 const 限定。
    • 典型应用场景:像 std::shared_ptr<T> 这种拥有 constexpr 构造函数但没有 constexpr 析构函数的类型,可以使用 constinit 声明,但不能用 constexpr

特殊用途:消除 thread_local 隐藏守卫开销

  • constinit 还可以用于非初始化声明 (例如带有 extern 的声明)。
  • 作用:向编译器保证该 thread_local 变量已经被初始化,从而在访问时消除原本为了防止重复初始化而由编译器生成的隐藏守卫变量及检查开销。

5.17 decltype

  • 基本定义decltype(自 C++11 起)用于检查一个实体(entity)的声明类型,或者一个表达式(expression)的类型及值类别(Value category),而不会实际求值该表达式。
  • 主要用途:用于声明那些使用标准符号难以或无法书写的复杂类型(如 Lambda 相关类型、依赖模板参数的返回类型等)。

两种核心推导规则decltype 的行为取决于其括号内的参数形式:

  • 实体形式decltype(entity)
    • 如果参数是未加括号的标识表达式类成员访问表达式
      • 普通实体:返回该实体声明的类型。
      • 结构化绑定:返回该绑定的引用类型 (自 C++17 起)。
      • 非类型模板参数:返回模板参数的类型(即使模板参数对象带有 const,推导出的类型也是非 const 的,自 C++20 起)。
  • 表达式形式decltype(expression)
    • 如果参数是任意其他表达式(设其类型为 TT),其推导结果由表达式的值类别(Value Category)决定:
      • xvalue(将亡值) :推导为 T&&T\&\&
      • lvalue(左值) :推导为 T&T\&
      • prvalue(纯右值) :推导为 TT

经典陷阱decltype(x)decltype((x))

  • 如果对象名称加了多余的括号(变成表达式),它会被当作一个普通的左值表达式处理。
  • 规则对比
    • decltype(x):直接获取对象 x 的声明类型(如 int)。
    • decltype((x)):由于加了括号的 (x) 是一个左值表达式,因此推导结果为左值引用类型(如 int&)。

纯右值与临时对象物化特性(自 C++17 起)

  • decltype 的参数是纯右值时(除了部分括号包裹的立即调用外),不会发生临时对象的物化
  • 优势:由于不创建临时对象,被推导的类型甚至可以是不完整类型 、没有析构函数的类型,或者是抽象类。不过,这一规则仅适用于最外层表达式(例如在 decltype(f(g())) 中,f() 的返回值类型无需完整,但作为子表达式的 g() 的返回类型必须完整)。

5.18 占位符类型

  • 基本定义:占位符类型说明符指定了一个占位类型,该类型稍后会被替换,通常是通过初始化器进行类型推导

主要种类

  1. auto(自 C++11 起):依据模板参数推导规则进行类型推导。可伴随修饰符(如 const&)。
  2. decltype(auto)(自 C++14 起):依据 decltype(expr) 规则进行精确类型推导。必须作为声明类型的唯一组成部分
  3. 受约束的占位符(自 C++20 起) :可在 auto 前添加概念名称(如 Concept auto),若推导出的类型不满足概念约束则推导失败。

主要应用上下文

  • 变量声明:从初始化器中推导变量类型(如 auto x = 5;)。
  • 函数返回类型
    • 结合尾随返回类型使用。
    • 返回类型推导(自 C++14 起) :编译器根据 return 语句推导函数返回类型。
  • 参数声明
    • 泛型 Lambda 表达式的参数(自 C++14 起)。
    • 非类型模板参数(NTTP,自 C++17 起)。
    • 简写函数模板(自 C++20 起)的参数。
  • 其他场景:结构化绑定声明(自 C++17 起)、new 表达式的类型说明符、函数风格转型(自 C++23 起)。

关键规则与限制

  • 必须初始化:使用 auto 声明变量时,必须提供非空的初始化器,以便进行类型推导。
  • 多变量声明一致性:若在一个声明语句中声明多个变量,所有变量由同一个 auto 推导,如果推导出的类型不完全相同,程序语法错误。
  • 旧语义变更:在 C++11 之前,auto 曾是存储期说明符,现已被彻底废弃并赋予类型推导语义。

5.19 typedef

  • 基本定义typedef 说明符用于为现有的(可能很复杂的)类型创建一个类型别名 ,该别名可以在任何地方替代原类型名使用。
  • 非新类型typedef 仅仅是现有类型的同义词,不会定义或产生一个全新的独立类型。

核心语法与规则

  • 位置与组合:通常出现在声明的开头,也可以位于类型说明符之间。它不能与除类型说明符之外的其他说明符组合。
  • 多重声明:可以在同一行声明多个标识符(例如同时声明基本类型和指针类型)。
  • 作用域与重声明
    • 别名仅在其可见的作用域内有效。
    • typedef 名称一旦声明,不能被重新定义去表示其他类型;如果重复声明,必须严格指向完全相同的同一类型。

禁止出现的上下文

  • 函数参数的声明中。
  • 函数定义的声明说明符序列(decl-specifier-seq)中。
  • 任何不包含声明符的声明中(例如空的 typedef struct X {}; 是非法的,必须配合具名声明或将其写为 struct X {};)。

链接名称的特殊作用

  • 如果 typedef 声明定义了一个未命名的类或枚举,则该声明中的第一个 typedef 名称将成为该类型的链接名称
  • 通过这种方式定义的未命名类或枚举会获得外部链接 (除非处于匿名命名空间中)。
  • C++20 限制:此类未命名类必须仅包含 C 兼容的构造(例如不能有成员函数、基类、默认成员初始化器或 lambda 表达式等)。

5.20 类型别名与别名模板

  • 基本定义:自 C++11 起引入,使用 using 关键字为现有类型创建一个同义词。
  • 语法形式
    using identifier = type-id;
    
  • typedef 的关系:二者在语义上完全等价,均不引入新类型,也不会改变现有类型的含义。但 using 语法更清晰,且完美支持模板。

别名模板

  • 基本定义:指代一组类型的模板(即类型的族)。
  • 语法形式
    template < template-parameter-list >
    using identifier attr(optional) = type-id;
    // 自 C++20 起可附加 requires 约束
    template < template-parameter-list > requires constraint
    using identifier attr(optional) = type-id;
    
  • 典型应用:用于简化复杂的模板类实例化,例如:
    template<class T>
    using Vec = std::vector<T, std::allocator<T>>; 
    Vec<int> v; // 等价于 std::vector<int, std::allocator<int>>
    

核心限制与注意事项

  • 禁止自引用:别名模板的 type-id 不能直接或间接地使用其自身正在定义的类型(避免递归死循环)。
  • 不可特化别名模板不能进行偏特化或显式特化 。如果需要特化行为,通常通过对底层类模板进行特化来实现。
  • 模板模板参数推导:在推导模板模板参数时,别名模板永远不会被主动推导出来。
  • 作用域限制:与所有模板声明一样,别名模板只能在类作用域或命名空间作用域中声明。
  • Lambda 表达式行为(自 C++20 起) :别名模板声明中出现的 Lambda 表达式,即使与模板参数无关,在每次模板实例化时也会产生不同的闭包类型

5.21 详述类型说明符

  • 基本定义:详述类型说明符用于引用先前声明的类名(classstructunion)或枚举名(enum),即使该名称被同名的非类型声明(如变量、数据成员)所遮蔽 。它也可以用来声明新的类。
  • 主要形式
    1. class-key class-name(类类型的详述类型说明符,如 class Tstruct Node
    2. enum enum-name(枚举类型的详述类型说明符,如 enum Color
    3. class-key attr(optional) identifier ;(仅包含详述类型说明符的声明,即**前向声明 **)

绕过遮蔽

  • 当局部变量或其他非类型实体与类型同名时,普通查找会失败或找到非类型实体。使用详述类型说明符可以强制名字查找忽略非类型名称
    int T;
    // T t;      // 错误:找到的是局部变量 T
    class T t; // 正确:忽略局部变量,找到全局类 ::T
    

名称查找与隐式声明规则

  • 查找行为:在详述类型说明符中,非类型名称(如变量、函数、数据成员等)会被直接跳过不予考虑。
  • 隐式声明(向后兼容的特殊规则)
    • 如果名字查找未找到先前声明的类型名,且该详述类型说明符由 classstructunion 引导(enum 不支持此特性),并且 class-name 是一个无限定标识符,则它会作为一个类声明 ,隐式地在最近的外层命名空间或块作用域中引入该类名。
    • 错误示例:若尝试在限定名(如 friend class ::List;)或枚举(enum Kind* kind;)中使用此特性,会导致程序编译错误。

关键限制与匹配约束

  • 禁止指向别名 / 模板参数:如果名称引用的是 typedef 名、类型别名、模板类型参数或别名模板特化,则程序语法错误
  • 关键字必须严格匹配 :详述类型说明符中的关键字必须与原定义相符:
    • enum:必须用于引用枚举类型(无论是有作用域还是无作用域枚举)。
    • union:必须用于引用联合体。
    • class / struct:必须用于引用非联合体的类类型(这两个关键字在此处可以互换)。
    • 注意:定义时用了 enum class 的枚举,在写详述类型说明符时只能使用 enum,不能写 enum class

5.22 属性说明符

  • 基本定义:自 C++11 起引入,用于为类型、对象、代码块等引入实现定义的属性,统一了如 __attribute__((...))__declspec() 等各种编译器的语言扩展。
  • 基本语法形式
    1. [[ attribute-list ]](自 C++11 起)
    2. [[ using attribute-namespace : attribute-list ]](自 C++17 起,指定一次命名空间,避免重复书写)
  • 属性项的形式
    • 简单属性:如 [[noreturn]]
    • 带命名空间的属性:如 [[gnu::unused]]
    • 带参数的属性:如 [[deprecated("reason")]]
    • 带命名空间和参数的属性

重要规则与演进特性

  • 未知属性处理:编译器会直接忽略所有不认识的非标准属性而不会报错(自 C++17 起规范化)。
  • 命名空间保留规则(自 C++20 起):无命名空间的属性,以及以 std(或 std 后跟一个或多个数字)开头的命名空间,保留给未来标准化使用。所有非标准扩展属性必须放在各编译器的命名空间中(如 [[gnu::...]][[clang::...]][[msvc::...]])。
  • 注解(自 C++26 起) :若属性说明符中的每一项都以 = 开头,则其引入的是注解。属性和注解不能在同一个属性说明符中混用。
  • 平台检测:可以使用预处理宏 __has_cpp_attribute 来检查特定平台上某个属性是否存在。

常见标准属性一览

  • [[noreturn]] (C++11):指示函数不会返回。
  • [[deprecated]] / [[deprecated("reason")]] (C++14):标记实体已过时,使用时会触发编译器警告。
  • [[fallthrough]] (C++17):用于 switch 语句,明确指示有意穿透,抑制编译器的警告。
  • [[maybe_unused]] (C++17):抑制编译器对未使用实体(如变量、函数等)发出的警告。
  • [[nodiscard]] / [[nodiscard("reason")]] (C++17 / C++20):鼓励编译器在函数的返回值被忽略时发出警告。
  • [[likely]] / [[unlikely]] (C++20):指示分支路径的执行概率,供编译器进行优化。
  • [[no_unique_address]] (C++20):指示非静态数据成员不需要与其他同类成员拥有不同的地址(用于优化空基类或无状态成员占用的空间)。
  • [[assume(expression)]] (C++23):向编译器断言表达式在指定位置始终为 true,辅助编译器优化。

5.23 alignas

  • 基本定义alignas 说明符(自 C++11 起)用于指定类型或对象的对齐要求
  • 语法形式
    1. alignas( expression )expression 必须是整型常量表达式,其求值结果必须为 0 或一个合法的对齐值(或扩展对齐值)。
    2. alignas( type-id ):等价于 alignas(alignof( type-id ))
    3. alignas( pack ... ):参数包展开,等价于对声明应用多个 alignas

适用对象与应用场景

  • 类(Class)的声明或定义。
  • 非位域的类数据成员的声明。
  • 变量的声明。
  • 禁止使用的场景:不能应用于函数参数以及 catch 子句的异常参数

核心对齐规则与约束

  • 最严格对齐原则:若声明中使用了多个 alignas,对象的最终对齐要求将等于其中最严格(数值最大)的非零表达式
  • 禁止削弱对齐
    • 如果声明上的最严格 alignas 比没有该说明符时的自然对齐要求更弱,或者比同一对象/类型的其他声明上的 alignas 更弱,则程序语法错误
    • 例如:若结构体 S 的自然对齐是 8,强行指定 struct alignas(1) U { S s; }; 会导致对齐被削弱,引发编译错误。
  • 无效与冗余对齐处理
    • 无效的非零对齐值(如 alignas(3))会导致程序格式错误。
    • 弱于同声明中其他 alignas 的有效非零对齐值会被直接忽略
    • alignas(0) 总是被忽略

5.24 static_assert

  • 基本定义static_assert 用于在编译期执行断言检查。如果断言失败,程序语法错误,并会生成诊断错误信息。
  • 语法形式
    1. static_assert( bool-constexpr , unevaluated-string );(带固定错误信息的静态断言,自 C++11 起)
    2. static_assert( bool-constexpr );(不带错误信息的静态断言,自 C++17 起)
    3. static_assert( bool-constexpr , constant-expression );(带用户动态生成的错误信息的静态断言,自 C++26 起)

参数与求值规则

  • bool-constexpr
    • 一个按上下文转换为 bool 类型的常量表达式。
    • 在 C++23 之前,不允许内置转换(非窄化整型转换为 bool 除外);自 C++23 起,放宽为“在常量表达式中按上下文转换为 bool”。
  • 错误信息
    • C++11 起:必须是未求值的字符串字面量
    • C++26 起:允许传入一个满足特定条件的 constant-expression(其 .size() 可转换为 std::size_t.data() 可转换为 const char*),从而支持在编译期动态生成或组合错误提示文本。

作用域与有效性

  • 出现位置:可以出现在命名空间作用域块作用域(作为块声明),以及类定义内部(作为成员声明)。
  • 行为表现
    • 如果 bool-constexpr 格式良好且求值为 true(或者在模板定义尚未实例化且未关联当前上下文时),该声明无任何效果。
      • 如果求值为 false,编译器会触发编译错误,并在诊断信息中包含用户提供的错误文本。

6. 初始化

  • 基本定义:变量的初始化在构造时为其提供初始值。初始化可以发生在声明符的初始化器中、new 表达式中,以及函数调用过程中的参数传递和返回值返回。
  • 默认初始化与值初始化
    • 默认初始化(无初始化器):若未指定初始化器,对象会被默认初始化。若对引用未指定初始化器,程序格式错误(Ill-formed)。
    • 值初始化(形如 (),受语法限制不能直接出现在普通变量声明中):若显式指定 () 初始化对象,对象会被值初始化。

初始化语法形式:对于每个声明符,其初始化器主要分为以下几类:

  1. 复制初始化= expression / = { initializer-list }
  2. 列表初始化(自 C++11 起){} / { initializer-list } / { designated-initializer-list }(C++20 指定初始化)
  3. 直接初始化( expression-list )( initializer-list )(C++11 前)

经典陷阱std::string s2(); 不是初始化,而是声明了一个无参数、返回 std::string 的函数 s2

非局部变量的初始化:具有静态存储期或线程存储期的非局部变量,在程序启动(或线程启动)时初始化,分为两个主要阶段:

  • 静态初始化
    • 常量初始化 :优先在编译期进行,将预计算好的对象表示存储在程序镜像中。
    • 零初始化 :若无法常量初始化,则放入 .bss 段,由操作系统在加载时清零。
  • 动态初始化: 在静态初始化完成后进行,分为:
    1. 无序动态初始化:针对未显式特化的类模板静态数据成员和变量模板。
    2. 部分有序动态初始化(自 C++17 起) :针对所有非实例化特化的 inline 变量。
    3. 有序动态初始化:针对其他非局部变量。同一编译单元内的定义严格按源码顺序初始化;不同编译单元之间初始化顺序不确定。
    • 异常处理:若非局部变量的动态初始化以抛出异常退出,将直接调用 std::terminate
    • 延迟动态初始化 :动态初始化可能被推迟到 main 函数第一条语句执行之后(在首次 ODR 使用该翻译单元中的变量之前发生)。

6.1 默认初始化

  • 基本定义:当对象在没有初始化器的情况下被构建时执行的初始化。
  • 主要语法形式
    1. T object;(无初始化器变量声明)
    2. new T(无初始化器动态分配)
  • 发生的三种主要情况
    1. 具有自动、静态或线程存储期的变量在声明时没有初始化器
    2. 具有动态存储期的对象由无初始化器的 new 表达式创建。
    3. 构造函数的成员初始化列表中未提及某个基类或非静态数据成员。

初始化核心行为: 根据类型 T 的不同,默认初始化的实际效果表现为:

  • 类类型 :调用其默认构造函数(通过重载决议匹配空参数列表)。
  • 数组类型 :递归地对数组的每一个元素进行默认初始化。
  • std::meta::info(自 C++26 起) :初始化为空反射值。
  • 其他类型(如内置标量类型、POD 等)不进行初始化(保留不确定值或错误值)。

常量对象的默认初始化限制

  • 如果对 const 限定类型的对象进行默认初始化,该类型必须是常量默认可构造的
  • 规则变化
    • C++11 之前:要求类型拥有用户提供的默认构造函数,或其所有非静态数据成员/基类均满足条件。
    • C++11 及以后:要求所有非变体非静态数据成员拥有默认成员 initializer 或其类型本身是 const-default-constructible,且联合体或基类满足相应条件。
  • 注意:引用和 const 标量对象不能被默认初始化。

未初始化对象的值与安全机制(自 C++26 演进)

  • 不确定值与错误值
    • 未初始化的自动/动态存储期对象会保留不确定值错误值(自 C++26 起)
    • 产生不确定值的计算会导致未定义行为
    • C++26 起,自动存储期未初始化对象的字节被赋予独立的错误值,引发的行为被重分类为错误行为
  • 无初始化友好类型
    • 包括 unsigned charstd::byte(自 C++17 起)以及底层类型为 unsigned charchar
    • 对这些类型进行特定的安全传递操作(如条件表达式、简单赋值右操作数等),即使源自未初始化状态,也能避免未定义行为。

6.2 值初始化

  • 基本定义:值初始化是指当对象使用空初始化器(如空圆括号或空大括号)进行构造时所执行的初始化。
  • 主要语法形式
    1. T() / T{}:无名临时对象。
    2. new T() / new T{}:动态存储期对象分配。
    3. member() / member{}:构造函数初始化列表中的成员或基类初始化。
    4. T object {};(自 C++11 起):具名字面量对象的列表初始化形式。
  • 特殊转折规则
    • 若使用空大括号 {}T聚合体 ,则会执行聚合初始化(而不是值初始化)。
    • T 是没有默认 constructor 但拥有接受 std::initializer_list 的构造函数的类类型,则会执行列表初始化(自 C++11 起)。

值初始化的核心行为:当对象确定执行值初始化时,其效果按类型分为以下三种情况:

  1. 类类型
    • 如果该类型的默认初始化会选择一个非用户提供的默认构造函数,则对象会首先被零初始化
    • 无论如何,最终都会调用相应的构造函数进行默认初始化
  2. 数组类型
    • 对数组的每一个元素进行值初始化。
  3. 其他类型(如标量类型、指针等)
    • 对象被直接零初始化(将其数值或指针清零)。

重要注意事项与陷阱

  • 函数声明陷阱:形如 T object(); 的语法不会初始化对象,而是被编译器解析为一个不接受参数、返回类型为 T 的函数声明(即最烦人的解析 )。在 C++11 之前若要对具名变量进行值初始化,通常需使用 T object = T();
  • 引用限制:引用不能被值初始化。
  • 数组与大括号:作为函数风格转型的一部分,T() 语法(1)禁止命名数组类型,而 T{} 语法(5)则允许。
  • 标准库容器行为:所有标准容器(如 std::vectorstd::list 等)在通过单个 size_type 大小参数构造或调用 resize() 扩容时,都会对其元素进行值初始化。

6.3 拷贝/复制初始化

  • 基本定义:复制初始化是指使用另一个对象来初始化当前对象的过程。
  • 主要触发场景
    1. 具名变量声明:T object = other;(使用等号)。
    2. 标量类型的旧版大括号初始化:T object = {other};(在 C++11 之后被归类为列表初始化,且不允许窄化转换)。
    3. 传参:以值传递方式将参数传递给函数 (f(other))。
    4. 返回值:从按值返回的函数中返回 (return other;)。
    5. 异常处理:按值抛出或捕获异常 (throw object;catch (T object))。
    6. 聚合初始化:作为聚合初始化的一部分,用于初始化提供有初始化器的每个元素。

核心行为: 根据类型和表达式的不同,复制初始化遵循以下处理步骤:

  1. 纯右值同类优化(自 C++17 起)
    • 如果 T 是类类型,且初始化器是一个纯右值表达式且无 cv-qualifier 的类型与 T 相同,则直接使用该表达式初始化目标对象,而不会物化临时对象(即复制消除)。
  2. 类类型转换与构造函数匹配
    • 如果 other 的类型是 TT 的派生类,则考察 T非显式构造函数 ,通过重载决议选择最佳匹配项来初始化对象。
    • 如果 otherT 类型不同(且涉及类类型转换),则考察用户定义的转换序列。转换得到的临时对象随后用于直接初始化目标对象(该步骤通常会被编译器优化,直接在目标内存中构造)。
  3. 内置/标准类型转换
    • 如果 Tother 均不是类类型,则通过必要的标准转换other 的值转换为 T 的无 cv-qualifier 版本。

重要特性与关键陷阱

  • 排斥 explicit 构造函数:复制初始化比直接初始化更为严格explicit 构造函数不是转换构造函数,因而在复制初始化过程中绝对不会被考虑。
    struct Exp { explicit Exp(const char*) {} };
    Exp e1("abc");  // 正确:直接初始化允许 explicit
    Exp e2 = "abc"; // 错误:复制 initialization 不考虑 explicit 构造函数
    
  • 多步隐式转换限制:复制初始化要求隐式转换必须直接从初始化器产生 T。例如,若构造函数接受 std::string,用 const char* 进行复制初始化(S s = "abc";)会因为需要“const char* \to std::string \to S”两步隐式转换而报错。
  • 移动语义的融入:如果 other 是一个右值表达式,重载决议会优先选择移动构造函数 。尽管如此,这在语义上仍然被称为复制初始化(C++ 中没有单独的“移动初始化”术语)。
  • 与赋值运算符无关:具名变量声明中的等号(=)属于复制初始化语法,与赋值运算符(operator=)没有任何关系,重载赋值运算符不会影响复制初始化。

6.4 直接初始化

  • 基本定义:直接初始化是指使用显式的一组构造函数参数来初始化对象的过程。
  • 主要触发场景
    1. 使用非空圆括号大括号列表的变量初始化(如 T object(arg);T object{arg};)。
    2. 非类类型的单个大括号初始化器初始化。
    3. 通过函数风格转型或带圆括号的表达式列表初始化纯右值临时对象 / 结果对象。
    4. 通过 static_cast 表达式初始化。
    5. 使用带初始化器的 new 表达式初始化动态存储期对象。
    6. 在构造函数初始化列表中初始化基类或非静态数据成员。
    7. 在 Lambda 表达式中,由按值捕获的变量初始化闭包对象的成员。

核心行为: 根据目标类型 T 的不同,直接初始化遵循以下处理规则:

  1. 数组类型
    • C++20 之前:程序格式错误。
    • 自 C++20 起:按照聚合初始化处理,但允许窄化转换 ,且任何没有初始化器的元素都会被值初始化
  2. 类类型
    • 若初始化器是与 T 相同的纯右值表达式(忽略 cv 限定),则直接使用该表达式初始化目标对象(无需临时对象物化)。
    • 通过重载决议检查 T所有构造函数(包括显式构造函数),选择最佳匹配项进行调用。
    • 若目标类型是聚合类,则作为聚合初始化处理(允许窄化转换,但不允许指定初始化器,引用绑定的临时对象不会延长生命周期,不支持大括号省略,未初始化的元素被值初始化)。
  3. 非类类型与转换
    • T 是非类类型而源类型是类类型,则检查源类型的转换函数并通过重载决议选出最佳匹配。
    • Tbool 且源类型是 std::nullptr_t,则初始化后的值为 false
    • 必要时使用标准转换将源值转换为 T 的无 cv 限定版本。

重要特性与经典陷阱

  • 比复制初始化更宽容
    • 复制初始化只考虑非显式的构造函数和转换函数;
    • 而直接初始化会考虑所有构造函数(包括 explicit 构造函数)和所有用户定义转换函数。
  • 最烦人的解析
    • 使用圆括号形式的直接初始化语法声明变量时,如果与函数声明产生歧义,编译器总是优先选择函数声明(例如经典的 T object(); 被解析为无参函数声明)。

6.5 聚合初始化

  • 基本定义:聚合初始化是指使用初始化列表来初始化聚合体(Aggregate)的形式(自 C++11 起被归类为列表初始化的一种)。
  • 主要语法形式
    1. T object = { arg1, arg2, ... };
    2. T object { arg1, arg2, ... };(自 C++11 起)
    3. T object = { .des1 = arg1, ... };(自 C++20 起,指定初始化器)
    4. T object { .des1 = arg1, ... };(自 C++20 起)

定义:聚合体必须满足以下条件

  • 数组类型,或者满足以下条件的类类型
    • 无用户声明或继承的构造函数(自 C++20 起;C++11/14/17 的规则略有演进)。
    • 无私有或保护的直接非静态数据成员(自 C++17 起放宽,此前要求所有成员均为 public)。
    • 无虚基类(自 C++17 起允许非虚基类)。
    • 无私有或保护的直接基类(自 C++17 起)。
    • 无虚成员函数
    • 无默认成员 initializer(自 C++14 起允许)。

初始化过程与元素归属

  • 归属分析:编译器通过递归规则,将初始化列表中的各个子句按顺序严格匹配到聚合体的各个元素(包括基类、非静态数据成员或数组成员)上。如果有多余的无法匹配的子句,程序格式错误。
  • 显式初始化元素
    • 若使用了指定初始化列表,则显式指定的成员被初始化。
    • 若为普通大括号列表,则具有对应初始化子句的元素被显式初始化。
    • 联合体限制:联合体在聚合初始化时,最多只能有一个显式初始化的成员,否则程序格式错误。
  • 隐式初始化元素
    • 对于非联合体聚合体,未被显式初始化的元素:
      1. 若存在默认成员 initializer(自 C++14 起),则使用该 initializer 初始化。
      2. 若不是引用,则使用空初始化列表进行初始化(对于标量类型即为零初始化)。
      3. 若是引用且无默认值,则程序格式错误。

C++20 重要扩展:指定初始化器

  • 语法规则:使用 .member = value 的形式明确指定要初始化的成员。
  • 严格限制(相比 C)
    • 指定项的顺序必须与类中数据成员的声明顺序完全一致
    • 不支持乱序初始化、嵌套指定初始化、混用指定与常规初始化,以及数组的指定初始化(这些在 C 语言中合法,但在 C++ 中均会导致编译错误)。

字符数组与字符串字面量初始化

  • 普通字符数组(charwchar_tchar16_tchar32_tchar8_t 等)可以直接由对应的字符串字面量进行聚合初始化(可选择加花括号)。
  • 字符串的隐式终止符 \0 会自动填充数组元素;若指定的数组大小大于字符串长度,剩余元素将被零初始化

6.6 列表初始化

  • 基本定义:使用由大括号包围的初始化器列表来初始化对象的形式(引入于 C++11)。
  • 两大核心分类
    1. 直接列表初始化 :如 T object { arg1, arg2, ... };。同时考虑显式和非显式构造函数。
    2. 复制列表初始化 :如 T object = { arg1, arg2, ... };。虽然参与重载决议的构造函数包含显式和非显式,但只有非显式构造函数可以被成功调用(若最佳匹配为显式构造函数则编译失败)。

核心初始化行为与匹配顺序: 编译器按以下顺序和规则进行处理:

  1. 聚合类与指定初始化器:若列表包含指定初始化器(自 C++20 起)且 T 不是引用,则 T 必须是聚合类,执行聚合初始化
  2. 同类型单元素直接复制/直接初始化:若 T 是聚合类且列表只有一个相同或派生类型的初始化子句,则直接由该子句初始化。
  3. 字符数组与字符串字面量:若 T 是字符数组且列表包含单个匹配的字符串字面量,按常规方式初始化。
  4. 常规聚合体:若 T 是其他聚合类型,执行聚合初始化
  5. 空列表与默认构造:若列表为空且 T 是拥有默认构造函数的类类型,执行值初始化
  6. std::initializer_list 优先匹配:若 Tstd::initializer_list 的特化,则由其进行初始化。
  7. 类类型构造函数匹配(两阶段)
    • 第一阶段:优先考察所有接受 std::initializer_list 作为唯一参数(或首个参数且其余参数有默认值)的构造函数。
    • 第二阶段:若未匹配成功,则考察所有普通构造函数(严格禁止窄化转换)。
  8. 引用绑定:若 T 是引用类型,通常会通过列表初始化生成一个 prvalue 临时对象并绑定到该引用。

核心安全机制:禁止窄化转换:列表初始化比传统初始化更为严格,完全禁止以下隐式窄化转换:

  • 浮点类型 \to 整数类型。
  • 浮点类型 \to 精度更低的浮点类型(常量表达式且无溢出或非有限值的情况除外)。
  • 整数类型 \to 浮点类型(源非可精确存储的常量表达式时)。
  • 整数或无作用域枚举 \to 无法表示原类型所有值的目标整数类型。
  • 指针或成员指针类型 \to bool

关键特性与注意事项

  • 无类型特性:大括号初始化列表本身不是表达式,因此没有类型(例如 decltype({1, 2}) 非法),无法直接用于普通模板类型推导。但 auto 在配合复制列表初始化时会被特殊推导为 std::initializer_list
  • 幕后数组std::initializer_list<E> 在底层由编译器生成并物化一个临时的 const E 数组(其生命周期与临时对象一致,若绑定到引用则会延长)。

6.7 引用初始化

  • 基本定义:引用初始化是将一个引用绑定到一个对象、函数或可隐式转换为该类型的表达式上。
  • 核心约束:引用一旦初始化,就无法被重新绑定指向另一个对象。
  • 主要触发场景
    1. 声明具名的左值或右值引用变量时。
    2. 函数调用时,参数为引用类型。
    3. 函数返回引用类型时。
    4. 类的非静态引用成员在构造函数初始化列表中进行初始化。

初始化机制与两大绑定路径:对于非列表引用的初始化,编译器按顺序尝试以下两种绑定方式(若均无法满足则程序格式错误):

  1. 直接绑定
    • 左值引用:若目标是兼容类型的非位域左值,左值引用直接绑定到该目标(或其基类子对象)。若目标是右值且引用不是 const 左值引用,则直接绑定失败。
    • 右值引用 / const 左值引用:若目标是兼容类型的 rvalue / xvalue / 纯右值,引用可直接绑定。自 C++17 起,若目标是纯右值,会应用临时物化转换 ,引用直接绑定到结果对象。
  2. 间接绑定
    • 当直接绑定不可用时,若涉及类类型则考虑用户定义转换(通过非引用复制初始化机制),否则会创建一个临时对象,将目标值复制初始化(或隐式转换为纯右值并物化)到该临时对象中,最后将引用绑定到该临时对象。

临时对象生命周期延长

  • 基本规则:当引用绑定到临时对象(或其子对象)时,临时对象的生命周期会延长至与该引用变量的生命周期相同
  • 关键生命周期异常(不会延长的场景)
    1. return 语句中绑定到函数返回值的临时对象(立即销毁,返回悬空引用)。
    2. 函数调用中绑定到引用参数的临时对象(仅存活到该函数调用完整表达式结束)。
    3. new 表达式初始化器中绑定到引用的临时对象。
    4. 自 C++20 起,使用圆括号直接初始化语法初始化聚合体中的引用成员时,临时对象生命周期不会延长(而大括号列表初始化会延长),会导致悬空引用:
      struct A { int&& r; };
      A a1{7}; // 正常:列表初始化延长临时对象生命周期
      A a2(7); // 良好但产生悬空引用:圆括号直接初始化
      

6.8 零初始化

  • 基本定义:将对象的初始值设置为零。
  • 语法特点零初始化本身没有专门的专属语法,它通常作为其他初始化形式(如静态初始化、值初始化、显式空列表初始化等)的一部分被触发。
  • 常见触发场景示例
    1. static T object;(静态或线程局部存储期变量在常量初始化前的第一阶段)。
    2. T(), T t = {}, 或 T {}(值初始化序列的一部分)。
    3. 字符数组用过短的字符串字面量初始化时,剩余部分被零初始化。

在不同类型上的具体效果:当对类型 T 的对象执行零初始化时,其表现如下:

  • 标量类型 :对象被初始化为将整数字面量 0 显式转换为类型 T 的值。
    • 指针注意事项:零初始化得到的指针是其类型的空指针值 ,即使该架构下的空指针底层整型表示并非全 0 也能正确保证。
  • 非联合体类类型
    • 所有填充位均被初始化为零位。
    • 每个非静态数据成员均被零初始化。
    • 每个非虚基类子对象以及(若该对象不是基类子对象)每个虚基类子对象均被零初始化。
  • 联合体类型
    • 所有填充位初始化为零位。
    • 对象的首个非静态命名数据成员被零初始化。
  • 数组类型 :每个元素均被零初始化。
  • 引用类型 :不执行任何操作。

核心机制与实际应用

  • 与默认初始化的协同:对于具有静态或线程局部存储期的非类非局部变量,若其定义没有初始化器,默认初始化不会改变前面已经完成的零初始化结果(即维持零初始化状态)。

6.9 常量初始化

  • 基本定义:将具有静态存储期(或自 C++11 起的线程存储期)的变量的初始值设置为编译期常量
  • 核心保障:常量初始化保证在任何其他静态或线程局部对象的动态初始化开始之前完全完成。

触发条件与演进:根据 C++ 标准的演进(特别是 C++11 及 C++17),满足以下条件的静态/线程存储期变量或临时对象会进行常量初始化:

  1. 引用初始化:使用常量表达式初始化静态存储期的引用(绑定到具有静态存储期的对象、函数、临时对象或其子对象)。
  2. 对象初始化(C++11/C++17 规则)
    • 通过常量表达式constexpr 构造函数(甚至可用于非字面量类类型)进行初始化,且对应的初始化完整表达式是常量表达式。
    • 或者该对象被值初始化 ,或者其初始化器中的每个完整表达式都是常量表达式。

核心效果与执行时机

  • 初始化效果:其效果与对应的初始化形式完全相同,但享有最高优先级的执行保证(在所有动态初始化之前完成)。
  • 底层实现与优化:通常在程序加载到内存中时(作为运行时环境初始化的一部分)完成。编译器甚至被允许将其他静态/线程局部对象提升为常量初始化,只要能保证其值与遵循标准初始化顺序的结果完全一致。

6.10 复制消除

  • 基本定义:当满足特定条件时,即使选定的构造函数和/或析构函数具有可观察的副作用,C++ 编译器也可以省略从同类型源对象创建类对象的拷贝或移动操作(即直接在目标存储空间中构造对象)。
  • 可改变副作用的优化:复制消除是 C++ 中唯一允许改变可观察副作用的优化手段(例如:构造/析构函数的调用次数)。因此,依赖拷贝/移动构造函数副作用的代码具有不可移植性。

传统复制消除的主要场景

  1. NRVO(具名返回值优化)
    • 在具有类返回类型的函数中,return 语句的操作数如果是同类型(忽略 cv-qualifier)的非 volatile 自动存储期局部对象(非函数参数或 catch 捕获参数),编译器可以直接将该局部对象构造到函数调用的结果对象中。
  2. URVO(匿名返回值优化)
    • 用未绑定到引用的临时类对象去复制初始化目标类对象时,可以省略拷贝。注:自 C++17 起,这已演变为强制性的 prvalue 语义,不再属于狭义的“复制消除”。
  3. 异常处理与协程
    • throw 表达式中抛出局部对象、catch 处理器中将参数视为异常对象的别名,或协程中省略形参的拷贝。

C++17 重要变革:Prvalue 语义与“保证的复制消除”

  • 核心机制:自 C++17 起,纯右值不再无条件物化为临时对象,而是直接在最终目标的存储空间中进行构造(称为“未物化值传递”或“延迟临时物化”)。
  • 无需拷贝/移动构造函数:因为压根没有临时对象需要被拷贝或移动,所以目标类型的拷贝/移动构造函数甚至可以不存在或被删除(但析构函数必须可访问且未被删除)。
  • 典型场景
    • 返回 prvalue 的函数返回值的初始化(如 T g() { return T(); } 直接构造,无移动)。
    • 用 prvalue 初始化同类型变量(如 T x = T(f()); 直接由 f() 的结果初始化)。
  • 不适用场景:当被初始化的对象可能是潜在重叠子对象(如基类子对象)时,无法应用此规则。

关键注意事项与限制

  • 常量表达式中的限制:在 constexpr 上下文和常量初始化中绝不执行复制消除。若代码逻辑依赖于地址自指(如 p(this)),在旧标准或涉及临时对象时可能会报错(如悬空指针),而 C++17 的纯右值保证能确保 this 指向最终的目标对象。
  • 后备移动机制:在 returnthrow 语句中,如果满足复制消除条件但编译器未能执行消除(或源对象是函数参数),编译器会尝试将源对象当作右值处理(自动优先匹配移动构造函数)。

7. 函数

  • 基本定义:C++ 中的函数是将一系列语句(函数体)与一个名称以及零个或多个参数列表关联起来的实体。
  • 主要特性
    • 非对象实体:函数不是对象,因此不存在函数数组,也不能通过值来传递或从函数中返回。不过,允许使用函数指针和函数引用(主函数 main 及多数标准库函数除外)。
    • 终止方式:函数可以通过 return 语句返回值,或者通过抛出异常来终止。
    • 协程支持:自 C++17/C++20 起,函数可以声明为协程,支持挂起与恢复。

函数声明与定义的作用域规则

  • 声明位置:函数声明可以出现在任何作用域中。
  • 定义位置:函数定义仅能出现在命名空间作用域中,或者对于成员函数和友元函数,可以出现在类作用域中。

函数类型:每个函数都有一个确定的类型,其组成部分包括:

  • 返回类型。
  • 所有参数的类型(经过数组到指针、函数到指针的常规转换后)。
  • 是否带有 noexcept 修饰(自 C++17 起)。
  • 对于非静态成员函数,还包含 cv-qualifierref-qualifier(自 C++11 起)。
  • 重要限制:不存在 cv-qualified 的函数类型本身(例如不能声明 const int f() 这样的函数类型,尽管可以声明具有 cv-qualifier 的成员函数如 int f() const)。

函数重载

  • 重载规则:在同一作用域中,多个函数可以拥有相同的名称,只要它们的参数列表以及(对于非静态成员函数)cv/ref-qualifications 不同。
  • 无效重载:仅在返回类型noexcept 说明上不同的函数声明无法构成重载,会导致编译错误。
  • 名称查找:在函数调用表达式中,无资格(Unqualified)函数名除了常规查找外,还会通过参数依赖查找(ADL, Argument-Dependent Lookup)进行额外查找。

函数对象:除了解析常规的函数左值外,函数调用表达式还支持:

  • 函数指针。
  • 重载了函数调用运算符 operator() 的类类型值。
  • 可隐式转换为函数指针的类型(包括自 C++11 起引入的Lambda 表达式)。
  • 标准库中也广泛预定义了各种函数对象模板和组合工具(如 std::lessstd::functionstd::bindstd::bind_front 以及新标准中的 std::move_only_functionstd::function_ref 等)。

7.1 函数声明与定义

  • 声明 :引入函数名及其类型,可出现在任何作用域中。
  • 定义 :将函数名和类型与具体的函数体关联起来,仅可出现在命名空间作用域或类作用域中。

函数声明的关键语法与特性

  • 尾随返回类型(自 C++11)
    • 语法形如 auto f(T t) -> decltype(t);,当返回类型依赖于参数名称或结构复杂时非常有用。此时 decl-specifier-seq 必须包含关键字 auto
  • 显式对象形参(自 C++23)
    • 允许在非静态成员函数的参数列表首位使用 this 关键字声明显式对象形参(如 void f(this C& self);),此时该成员函数不能再使用传统的 cvref 修饰符。
  • 参数默认值与省略号
    • 最后一个参数可以是省略号 (...) 以声明变长函数。

返回类型推导(自 C++14)

  • 规则概述:若 decl-specifier-seq 包含 auto(或 decltype(auto)),编译器可根据 return 语句的操作数自动推导返回类型。
  • 多条返回路径:若存在多条 return 语句,它们必须推导为完全相同的类型
  • 限制:虚函数和协程(自 C++20)不能使用返回类型deduction。

函数定义与特殊定义形式

  • 函数体 :由大括号包围的语句序列。对于构造函数,还包括成员初始化列表及隐式子对象初始化。
  • 显式预置(自 C++11)
    • 形如 T() = default;,仅适用于特殊成员函数或(自 C++20 起)比较运算符。在首个声明上预置的函数会被隐式视为 inline
  • 显式删除(自 C++11 / C++26)
    • 形如 void f() = delete;(自 C++26 起支持带原因的字符串字面量 = delete("reason");)。任何对删除函数的调用或使用均会导致编译错误。

现代 C++ 新特性(C++20 / C++26)

  • 关联约束(requires 子句,自 C++20) :可附加在函数声明/定义后,作为重载决议的参与条件。
  • 函数契约断言(自 C++26)
    • 支持使用 pre(predicate)(前置条件)和 post(r : predicate)(后置条件,可绑定返回值 r)来规范函数的输入输出行为。

7.2 默认参数

  • 基本定义:允许在调用函数时省略一个或多个尾随参数,缺省时自动使用默认值。
  • 语法形式:在函数声明的参数列表中使用 = initializer,例如:
    void point(int x = 3, int y = 4);
    
  • 位置限制:一旦某个参数指定了默认参数,其所有后续参数必须也带有默认参数(除非该参数是参数包 parameter pack)。

作用域、声明与多文件合并

  • 可见性与累加:在同一作用域内对函数进行多次声明时,默认参数会被联合在一起。调用时可以使用之前任何可见声明中指定的默认参数。
  • 内联与非内联函数限制
    • 内联函数:若在不同编译单元中声明,各编译单元结束时的默认参数组合必须完全一致。
    • 非内联函数:自 C++20 起,若在不同编译单元的相同命名空间作用域中声明,若提供了默认参数,它们必须保持一致(但某些翻译单元中可以省略默认参数的声明)。
  • 使用声明using 声明会继承原命名空间中已知的默认参数,后续在原命名空间中补充的默认参数也会自动对 using 声明可见。

求值规则与绑定时机

  • 绑定时机 vs 执行时机:默认参数中的名字在声明点进行查找、访问性检查和绑定;但表达式的实际求值发生在函数调用点
  • 每次调用求值:如果对应参数在调用时被省略,默认参数表达式会在每次调用时被重新求值。
  • 作用域独立性:内层作用域中的重声明不会从外层作用域继承默认参数。

限制与禁止事项

  • 禁止出现的位置
    • 函数指针、函数引用、typedef 声明中不允许使用默认参数。
    • 显式对象形参(自 C++23)不允许有默认参数。
    • 除函数调用运算符 operator() 和(自 C++23 起)下标运算符 operator[] 外,其他重载运算符函数不允许有默认参数。
  • 作用域内的实体限制
    • 局部变量:不能作为默认参数(除非在未求值上下文中,如 sizeof 内部)。
    • this 指针:不能在默认参数中使用。
    • 非静态类成员:不能直接用作默认参数,除非通过成员访问表达式、指向成员的指针或反射操作符。
    • 前面的形参:前面的形参不能直接在后续形参的默认参数中使用(除非处于未求值语境中)。

面向对象特性:虚函数与类成员

  • 虚函数与默认参数
    • 派生类重写(override)虚函数时,不会继承基类的默认参数。
    • 虚函数的默认参数由调用时的静态类型决定。
  • 类外定义 :非模板类的成员函数允许在类外定义时补充默认参数,并与类内部声明的默认参数合并。但若该默认参数意外将普通成员函数变成了默认构造函数或拷贝/移动特殊成员函数,会导致程序编译错误。

7.3 变长参数

  • 核心作用:允许函数接受任意数量的额外参数。
  • 定义方式:当参数列表的最后一个参数是省略号 (...) 时,该函数即为变长函数。
  • 逗号规则:省略号前的逗号可以省略(注:此风格自 C++26 起被弃用)。
    int printx(const char* fmt, ...);
    int printx(const char* fmt...); // 合法,但自 C++26 起弃用
    
  • 位置限制:省略号必须作为参数列表的最后一个参数。仅有省略号的函数 int printz(...); 在 C++ 中是合法的(常用于 SFINAE 中的兜底重载),但其传入的参数无法被便捷访问。

默认参数提升:当调用变长函数时,变长部分(省略号代表的参数)在经过左值到右值、数组到指针、函数到指针等转换后,必须经历额外的默认参数提升

  • std::nullptr_t 被转换为 void*(自 C++11)。
  • float 参数会被提升为 double(浮点提升)。
  • boolcharshort 以及无作用域枚举会被提升为 int 或更宽的整型(整型提升)。
  • 类类型限制:非 POD 类类型(C++11 前)或拥有合格非平凡复制构造函数、非平凡移动构造函数、非平凡析构函数的有作用域枚举与类类型(自 C++11 起),在潜在求值调用中是条件支持的(在未求值上下文中总是支持)。

参数访问与 <cstdarg>:在变长函数体内,必须通过头文件 <cstdarg> 中的宏和类型来访问这些参数:

  • va_list:保存 va_startva_argva_endva_copy 所需信息的类型。
  • va_start:启用对变长参数的访问。若省略号前的最后一个参数具有引用类型,或其类型与默认参数提升后的类型不兼容,则行为未定义。
  • va_arg:访问下一个变长参数。
  • va_copy:复制变长参数列表(自 C++11)。
  • va_end:结束对变长参数的遍历。

现代化替代方案

  • 变长模板(自 C++11) :通常是更好的选择。它们没有类型限制,不执行默认参数提升,并且是类型安全的。
  • std::initializer_list(自 C++11) :如果所有变长参数共享公共类型,它提供了一种语法不同但更便捷的机制(但由于只提供 const 指针,元素无法被修改)。

7.4 Lambda 表达式

  • 基本定义:Lambda 表达式构造一个闭包 ——一个能够捕获作用域中变量的未命名函数对象
  • 闭包类型特性
    • 每个 Lambda 表达式都会产生一个唯一的、无名的、非联合体、非聚合的类类型(称为闭包类型)。
    • 默认情况下,闭包类型的 operator()const 成员函数(除非使用了 mutable 关键字或显式对象形参)。
    • 闭包类型默认不可默认构造(直到 C++20 起,无捕获的 Lambda 才拥有默认构造函数和默认赋值运算符)。

语法与修饰符

  • 通用语法结构
    [captures] (params) specs except trailing requires { body }
    
  • 常用修饰符 (specs)
    • mutable:允许在函数体内修改按值捕获的变量(通过拷贝),并调用其非 const 成员函数。
    • constexpr(自 C++17) :显式指定 operator()constexpr 函数。
    • consteval(自 C++20) :指定 operator() 为即时函数
    • static(自 C++23) :指定 operator() 为静态成员函数(不能与 mutable 或捕获列表同时使用)。
  • 泛型 Lambda
    • C++14 起:参数类型可以使用 auto
    • C++20 起:支持显式模板参数列表,例如 []<typename T>(T a, auto b) {}

变量捕获机制

  • 捕获默认值
    • [&]:隐式按引用捕获。
    • [=]:隐式按值捕获(注意:隐式捕获 *this 自 C++20 起已被弃用)。
  • 显式捕获与初始化捕获(自 C++14)
    • 允许在捕获列表中直接声明并初始化新变量,常用于捕获只移类型
      auto p = [ptr = std::make_unique<int>(10)] { return *ptr; };
      
  • 生命周期与悬空引用
    • C++ 闭包不会延长按引用捕获的变量或 *this 对象的生命周期。若在引用对象销毁后调用 Lambda 的 operator(),将导致未定义行为。

转换函数与特殊性质

  • 转为函数指针
    • 无捕获且无显式对象形参的 Lambda 表达式,可以隐式转换为具有 C++ 语言链接的函数指针
    • C++17 起,该转换运算符自动为 constexpr
  • SFINAE 兜底:无捕获的 Lambda 表达式由于能隐式转换为函数指针,常被用于模板元编程中作为重载解析的兜底方案。

7.5 重载解析

核心流程与三个阶段:编译器在处理函数调用、运算符重载、构造函数初始化等场景时,通过以下三个步骤确定最终调用的函数:

  1. 候选函数 :通过名称 lookup(以及模板参数推导)搜集所有可能相关的函数集合。
  2. 可行函数 :从候选集中筛选出参数数量匹配、约束满足(C++20)、参数类型可隐式转换且引用绑定合法的函数集合。
  3. 最佳可行函数 :通过对各个参数的隐式转换序列进行逐项排序和打分,挑选出“唯一最优”的函数;若存在多个平局且无绝对胜者,则触发编译错误。

候选函数集的常见来源

  • 具名函数调用
    • 若为成员函数,通过成员访问表达式(.->)查找类 T 及其基类中的成员函数。
    • 若为初等表达式,通过常规名称查找(包含 ADL —— 实参依赖查找)寻找非成员函数或所属类的成员函数。
  • 类对象调用
    • 对象 Eoperator() 成员函数,以及能够转换为函数指针/引用的非显式用户定义转换函数(生成“代理调用函数”)。
  • 运算符重载
    • 综合收集成员候选T1::operator@)、非成员候选(ADL 查找)、内置候选以及 C++20 重写候选

可行函数的筛选条件:候选函数必须同时满足以下条件方可成为可行函数:

  1. 参数数量匹配:实参数量 MM 必须等于形参数量,或形参数量少于 MM 但包含省略号(...),或多余的形参均有默认实参。
  2. 约束满足:函数关联的 requires 约束必须为真(C++20)。
  3. 隐式转换可行:每个实参都必须存在至少一条隐式转换序列能够转换为对应的形参类型。
  4. 引用绑定有效:若形参为引用类型,必须符合引用绑定规则(例如:右键参数不能直接绑定到左值,除非是常量左值引用)。

隐式转换序列的排序:在比较两个可行函数 F1F2 时,编译器会按参数逐个对比其隐式转换序列的优劣:

  • 标准转换序列的等级(Rank)
    1. 精确匹配 :无转换、左值到右值转换、限定修饰转换(cv)、函数指针转换(C++17)。
    2. 提升 :整型提升、浮点数提升。
    3. 转换 :常规整型转换、浮点型转换、指针转换、布尔转换、派生类到基类的用户定义转换等。
  • 比较原则
    • 标准转换序列优于用户定义转换序列,用户定义转换序列优于省略号转换。
    • 子序列规则:若转换序列 S1S2 的严格子序列(排除左值变换后),则 S1 更优。
    • Cv 限定词与引用:更少 cv 限定的转换、或更精确匹配左/右值引用的转换通常具有更高优先级。

7.6 运算符重载

  • 可重载性:绝大多数 C++ 运算符支持重载,但以下运算符严禁重载
    • ::(域解析)
    • .(成员访问)
    • .*(通过成员指针的成员访问)
    • ?:(条件三目)
  • 无法创建新运算符:不允许创造如 **<>&| 等未定义的符号。
  • 规则不可变:无法改变运算符的优先级结合性(结合方向)以及操作数个数
  • 参数要求:运算符重载函数必须至少包含一个类类型类类型的引用枚举类型枚举类型的引用作为参数。

成员函数 vs 非成员函数

  • 必须为成员函数的运算符
    • =(赋值)、()(函数调用)、[](下标访问)、->(类成员访问)。
  • 推荐为非成员函数的运算符
    • 二元算术/比较运算符(如 +-==< 等):实现为非成员函数(通常配合 friend)能够保证左右操作数的对称性(例如允许 int + MyClass 的隐式转换)。
    • 流插入与提取符<<>>):左操作数由于是 std::ostream&std::istream&(标准库类型,无法为其添加成员函数),因此必须实现为非成员函数。

特殊运算符的语义变化

  • 短路求值与求值顺序的丢失(针对旧标准或特定运算符)
    • 重载 &&||彻底失去短路求值特性,且在 C++17 之前不会对左右操作数进行顺序定序(Sequencing)。
    • 重载逗号运算符 , 同样会失去内置版本严格从左到右的定序性质(C++17 前)。
  • 自增/自减运算符(++ / --
    • 前置版本:直接返回自身引用 (T& operator++();)。
    • 后置版本:带有占位整型参数 (T operator++(int);),通常通过拷贝旧值、复用前置版本来实现,并返回值。

C++23 新特性扩展

  • 静态重载运算符
    • C++23 起,operator()operator[] 允许声明为 static
    • 注意:虽然允许静态声明且支持函数记法调用(如 Obj::operator()(a, b)),但当它们作为运算符出现在表达式中时(如 obj(a, b)),语法上依然要求有一个对应类类型的对象参与。
  • 多维下标运算符
    • C++23 起,operator[] 支持接收任意数量的参数(例如 T& operator[](std::size_t x, std::size_t y, std::size_t z);),完美支持逗号表达式构成的多维访问语法(如 v[3, 2, 1] = 42;),避免了过去必须返回代理对象或多级嵌套 operator[] 的繁琐设计。

7.7 重载函数地址获取

当一个重载函数的名称出现在不需要立即调用、而是需要获取其函数指针成员函数指针的语境中时,编译器无法通过实参列表直接进行常规的函数调用重载解析。此时,编译器会通过目标类型匹配来唯一确定所指代的函数。

允许获取重载函数地址的 7 种上下文:当且仅当重载函数的名称出现在以下 7 种明确指定了“目标类型”的语境中时,才可以获取其地址(允许前置 & 符号或被多余的括号包裹):

  1. 对象或引用的声明初始化器:初始化目标对象或引用。
  2. 赋值表达式右侧:赋值给左侧指定了类型的变量。
  3. 函数调用实参:作为形参类型已知的函数实参传递。
  4. 用户定义运算符参数:作为运算符重载函数的实参。
  5. Return 语句:作为函数的返回值返回。
  6. 显式转型或 static_cast 参数:在强制类型转换中匹配目标类型。
  7. 非类型模板参数(NTTP) :匹配对应的模板参数类型。

候选函数集的选择:当尝试获取重载函数的地址时,编译器会根据上下文的目标类型(记为 FTFT)构建候选集 SS

  • 无目标类型:若没有明确的目标类型(如直接作为未重载的独立表达式),则会选中该名称对应的所有非模板函数
  • 有目标类型
    • 非模板函数:若其类型 FF(必要时通过 C++17 的函数指针转换)与目标类型 FTFT 完全相同,则被收入候选集 SS
    • 函数模板:通过模板参数推导生成的特化版本也会被加入候选集 SS
  • 成员函数与指针类型的适配规则
    • 若目标类型是函数指针函数引用,候选集 SS 仅能包含:非成员函数显式对象成员函数(C++23)以及静态成员函数(此时会自动应用从成员函数到普通函数指针的转换)。
    • 若目标类型是指向成员函数的指针 ,候选集 SS 仅能包含:隐式对象成员函数(此时会忽略该成员函数所属的具体类)。

候选函数的淘汰机制: 在形成候选集 SS 后,编译器会依次通过以下规则进行裁决,直到 SS仅剩唯一一个函数,否则程序编译报错(Ill-formed):

  1. 约束检查(C++20) :剔除所有关联 requires 约束未被满足的函数。
  2. 非模板优先原则:若候选集 SS 中同时包含非模板函数函数模板特化版本,则直接剔除所有的函数模板特化版本,优先保留非模板函数。
  3. 非模板偏序约束(C++20) :若存在两个非模板函数,其中一个函数的约束比另一个更具偏序约束性 ,则淘汰较弱的函数。
  4. 模板更特化原则:若存在多个函数模板特化版本,则淘汰其对应的函数模板不够特化的版本(保留更特化的模板)。

7.8 C++20 协程

  • 三大关键字:函数体中包含 co_awaitco_yieldco_return 之一的函数即为协程。

  • 限制条件

    • 不能使用变长参数(...)、普通 return 语句或占位返回类型(auto / Concept)。
    • constevalconstexpr 函数、构造函数、析构函数以及 main 函数不能是协程。

核心组件与协程状态: 每个协程在运行时都关联以下三个核心实体:

  1. Promise 对象:在协程内部被操作,负责提交结果、异常处理以及定制协程行为。
  2. Coroutine Handle(协程句柄) :在协程外部操作的非拥有型句柄,用于恢复(resume())或销毁(destroy())协程帧。
  3. Coroutine State(协程状态) :动态分配的内部存储空间,包含:
    • Promise 对象。
    • 所有按值拷贝的函数参数。
    • 当前悬挂点的表示(记录恢复和销毁的位置)。
    • 跨越当前悬挂点的局部变量与临时对象。

协程的完整生命周期与执行步骤: 当一个协程被调用并开始执行时,会按顺序发生以下步骤:

  1. 分配内存:通过 operator new 分配协程状态对象(若生命周期严格嵌套于调用方且大小已知,该分配可被优化消除)。
  2. 参数拷贝:将所有函数参数复制到协程状态中(按值拷贝或移动,按引用参数保持不变,注意悬空引用风险)。
  3. 构造 Promise:调用 Promise 对象的构造函数(若存在接收协程参数的构造函数则优先调用,否则调用默认构造函数)。
  4. 获取返回对象:调用 promise.get_return_object(),其结果会在协程首次悬挂时返回给调用方。
  5. 初始悬挂 :执行 co_await promise.initial_suspend()
    • 若返回 std::suspend_always,则为惰性启动协程。
    • 若返回 std::suspend_never,则为急切启动协程。
  6. 执行函数体:当初始悬挂恢复后,开始执行协程的主体代码。
  7. 退出与销毁
    • 正常通过 co_return(或落出函数体末尾)时,会调用 promise.return_void()promise.return_value(expr),随后销毁局部变量,执行 promise.final_suspend()
    • 若抛出未捕获异常,会调用 promise.unhandled_exception(),随后同样执行 promise.final_suspend()
    • 最终在协程状态销毁时,依次调用 Promise 析构函数、参数拷贝析构函数、并通过 operator delete 释放内存。

co_await 表达式执行逻辑:当执行 co_await expr 时,编译期按以下步骤解析并执行:

  1. 获取 Awaitable:根据是否为初始/最终悬挂点或 Promise 是否定义了 await_transform,将 expr 转换为 awaitable。
  2. 获取 Awaiter:通过重载解析寻找 operator co_await(成员或非成员),若无则直接将 awaitable 作为 awaiter。
  3. 检查就绪状态:调用 awaiter.await_ready()。若返回 true(表示已就绪),则跳过悬挂,直接进入最后一步。
  4. 悬挂与调度:若返回 false,协程正式悬挂,随后调用 awaiter.await_suspend(handle)
    • 若返回 voidbooltrue),控制权返回给调用方/恢复方。
    • 若返回 boolfalse),则立即恢复当前协程。
    • 若返回另一个协程的句柄,则直接恢复该句柄。
  5. 恢复与返回值:当协程恢复执行时,最终调用 awaiter.await_resume(),该函数的返回值即为整个 co_await expr 表达式的结果。

7.9 替换函数

  • 基本定义:C++ 标准库和实现提供了一些具有默认定义的函数,称为可替换函数 。用户可以在全局范围内自定义具有相同签名的函数(称为替换函数)来覆盖默认版本。
  • 替换时机:这种替换发生在程序启动之前。一旦用户提供了自定义版本,整个程序(包括标准库内部)在调用该函数时都会执行用户定义的版本。

替换函数的严格约束条件: 用户编写的替换函数必须满足以下所有条件,否则程序将处于语法错误 ,且编译器不强制要求报错

  1. 不能是 inline
  2. 必须附加到全局模块 (即不能处于任何命名空间或具名模块中)。
  3. 必须拥有 C++ 语言链接 (即不能是 extern "C" 以外的错误链接,标准库 replaceable 函数通常要求 C++ 链接或标准指定的特殊链接)。
  4. 返回值类型必须与原可替换函数完全一致
  5. 合法的重声明:如果在标准库头文件中有声明,用户的声明必须能够作为该头文件中声明的合法重声明。

可替换的标准库函数清单:标准库中明确规定可替换的函数主要包括两大部分:

  • 动态内存分配与释放函数
    • operator new / operator new[]
    • operator delete / operator delete[]
    • 用户可以通过重载全局的分配/释放函数来自定义内存池、对齐分配或实现内存泄漏追踪。
  • 调试与契约处理函数
    • is_debugger_present (自 C++26 起) :用于检查程序是否正在调试器控制下运行。
    • ::handle_contract_violation (实现定义,自 C++26 起) :契约违例处理器是否可替换取决于具体实现。

8. 语句

  • 定义:语句是 C++ 程序中按顺序执行的代码片段,任何函数的函数体本质上都是一组语句的序列。
  • 控制流限定语句 :包括 try 块、handler 的复合语句、if constexpr 的子语句以及 consteval if 的子语句(C++23)。
    • 跳转限制:在这些受限语句内部声明的 goto 目标标签只能被该语句内部的语句引用。
    • 分支限制:内部出现的 casedefault 标签也只能与该语句内部的 switch 语句关联。

核心语句类型速览

语句类别常见形式与说明
标号语句带有 identifier:casedefault 的语句,用于 goto 跳转或 switch 分支。C++23 起允许标号独立出现在复合语句末尾。
表达式语句表达式后跟分号 ;。不含表达式的分号称为空语句 ,常用于循环空体。
复合语句用大括号 {} 括起来的语句序列,会引入独立的块作用域 ,局部变量在离开作用域时按构造的相反顺序析构。
选择语句包括 ifif constexprswitch 以及 C++23 的 consteval if
迭代语句包括 while 循环、do-while 循环、for 循环以及 C++11 起的范围基 for 循环
扩展语句C++26 新增的 template for,用于将部分代码进行多份拷贝展开。
跳转语句包括 breakcontinuereturngoto。跳转出块或跳过已初始化自动变量时会触发对象的逆序析构。
断言语句C++26 新增的契约断言语句 contract_assert (predicate);
声明语句在块中引入一个或多个标识符的声明。
Try 块用于捕获执行其他语句时抛出的异常。
原子与同步块基于事务内存(TMTS)的并发控制块(如 synchronizedatomic_noexcept 等)。

包容关系规则:为了准确界定变量作用域与控制流边界,C++ 定义了包容关系:

  • 语句 S1S_1 包容 S2S_2,如果:
    1. S2S_2S1S_1 的直接子语句
    2. S1S_1 是选择、扩展或迭代语句,且 S2S_2 是其 init-statement
    3. S1S_1try 块,且 S2S_2 是其主体或异常处理器的复合语句。
    4. 传递性:S1S_1 包容 S3S_3S3S_3 包容 S2S_2

8.1 if 语句

if 语句用于根据条件选择性地执行代码。其主要包含四种变体:

  1. 常规 if(带或不带 else 分支)。
  2. if constexpr(C++17 引入的常量折叠分支)
  3. if consteval / if !consteval(C++23 引入的编译期常量语境检查分支)

带有初始化的 if 语句:自 C++17 起,if 语句支持在条件判断前引入一个 init-statement(可以是表达式语句、变量声明或别名声明):

if (auto it = m.find(10); it != m.end()) {
    // it 在整个 if / else 作用域内可见
    return it->second.size();
}
  • 作用域共享:由 init-statement 声明的名字与 condition 中声明的名字处于同一个作用域,并且该作用域同时延伸至 statement-truestatement-false 的主体内部。

if constexpr(constexpr if)

  • 常量求值:其条件必须是一个能够上下文转换为 bool 的常量表达式(C++23 进一步放宽了对非窄化转换的要求)。
  • 分支丢弃 :条件为假时,对应分支的代码会被编译器丢弃
    • 丢弃的分支不参与函数返回类型推导。
    • 丢弃的分支可以包含未定义的 ODR-use 变量,甚至允许包含非法的模板代码(只要在特定特化下不违背语法)。

if consteval(Consteval if)—— C++23

  • 语境感知:用于判断当前代码是否正处于显式常量求值语境中。
  • 语法强制:其分支主体必须是复合语句(大括号 {} ,即使单行也不能省略。
  • 即时函数上下文:在 if consteval 的代码块内部,允许直接调用通常需要常量表达式支撑的“即时函数”。

8.2 switch

switch 语句根据条件表达式的值,将控制流转移到多个分支中的某一个标签语句上。其语法结构支持可选的初始化语句(init-statement,自 C++17 起):

switch ( init-statement(optional) condition ) statement
  • 作用域共享:若使用了 init-statement(为声明时),其声明的名字与 condition(若为声明)声明的名字处于同一个作用域,并且该作用域延伸至整个 statement 内部。

条件(Condition)与类型要求

  • 允许的类型condition 的求值结果仅限于:
    1. 整型
    2. 枚举型
    3. 类类型 :若为类类型,会被上下文隐式转换为整型或枚举型。
  • 类型调整:最终的类型会经历整型提升

标号(Labels)与约束

  • 标号类型:包含 case constant-expression:default:。其中 constant-expression 必须是调整后的 switch 条件类型的转换常量表达式
  • 唯一性限制
    • 同一个 switch 语句中,不能存在多个具有相同值case 标号。
    • 同一个 switch 语句中,不能存在多个 default 标号。

控制流行为与 [[fallthrough]]

  • 穿透(Fallthrough) :执行完某个 case 分支后,如果末尾没有 break,控制流会顺序掉入下一个分支。
  • 意图标注:自 C++17 起,若有意允许穿透,应在下一个 casedefault 标号前显式加上 [[fallthrough]] 属性,以避免编译器发出警告。

常见陷阱:作用域与变量初始化

  • 跳过初始化错误:由于控制流不允许直接跳进一个具有自动存储期变量的生命周期/作用域中,如果某个 case 内部需要声明变量,必须用大括号 {} 将其包裹在独立的复合语句(块作用域)中,否则跨越该声明跳转到其他 case(如 default)会导致编译错误。

8.3 while

while 循环根据条件重复执行某个语句:

while ( condition ) statement
  • 等价逻辑:其执行流程可以等价理解为以下伪代码结构:
    /* label */ :
    {
        if ( condition )
        {
            statement
            goto /* label */;
        }
    }
    
  • 核心步骤:每次循环开始前都会先对 condition 求值,若转换或判定为 true,则执行循环体 statement;若为 false,则跳出循环。

条件(Condition)的形态 condition 可以是表达式简单声明(同样遵循与其他控制流语句一致的规则,并在 C++26 起支持结构化绑定声明)。

  • 表达式求值:会被上下文隐式转换为 bool
  • 声明求值:若条件是一个变量声明(如 while (int x = get_val())),该变量会在每一次循环迭代中被重新创建并销毁

作用域与控制流中断

  • 独立作用域:无论 statement 是否为复合语句(大括号 {}),它都会引入一个独立的块作用域 。在循环体内声明的变量,其生命周期仅限于当前单次迭代。
  • 中断控制
    • break:可用于在循环体内部直接终止整个 while 循环。
    • continue:可用于跳过当前迭代的剩余代码,直接进入下一次循环的条件检查。

前进保证

  • 根据 C++ 的前进保证,如果一个非平凡无限循环在运行过程中没有任何可观察行为且永不终止,其行为是未定义的。编译器有权直接移除此类不产生任何可观察效果的死循环

8.4 do-while

do-while 循环提供了一种至少执行一次的重复执行机制:

do statement while ( expression );
  • 核心步骤
    1. 当控制流到达 do 语句时,其循环体 statement无条件地首先执行一次
    2. statement 执行完毕后,会对尾部的 expression 进行求值,并将其上下文转换为 bool
    3. 若结果为 true,则再次执行 statement;若为 false,则循环终止,控制流跳出到循环体外。

控制流中断与作用域

  • 中断控制
    • break:可用于在 statement 内部直接终止整个 do-while 循环。
    • continue:可用于跳过当前迭代的剩余代码,直接跳转到尾部 expression 的求值判断阶段。
  • 作用域:与 while 循环类似,即使 statement 不是复合语句(大括号 {}),它也会隐式引入一个块作用域

前进保证

  • 同样受 C++ 的前进保证约束:如果一个非平凡无限循环在运行过程中没有任何可观察行为且永不终止,其行为是未定义的,编译器有权将其移除。

8.5 for

for 循环用于重复执行语句,免去了在循环体内手动管理循环条件的繁琐操作:

for ( init-statement condition(optional) ; expression(optional) ) statement
  • 等价逻辑:其执行流程在语义上等价于以下代码块:
    {
        init-statement
        while ( condition ) // 若 condition 为空,则等同于 while (true)
        {
            statement
            expression ;
        }
    }
    

核心组成部分说明

  • init-statement(初始化语句)
    • 可以是表达式语句(如空语句 ;)、简单声明(如定义循环计数器变量、多个变量或结构化绑定)、或别名声明(C++23)。
    • 必须以分号 ; 结尾。
  • condition(条件表达式或声明)
    • 用于在每次迭代前进行求值判断(上下文转换为 bool)。若缺省,则默认等价于 true
  • expression(表达式)
    • 通常用于在每次循环体执行完毕后更新循环计数器(如 ++i)。

作用域层级与变量遮蔽规则

  • 作用域范围
    • init-statementcondition 共享同一个作用域
    • 循环体 statement 与每次迭代求值的 expression 处于相互独立且嵌套在初始化作用域内部的子作用域中。
  • continue 的行为:在 statement 内部执行 continue 时,会直接跳转去执行尾部的 expression,然后再进行下一次的 condition 检查。
  • C 与 C++ 的重要差异(禁止变量遮蔽)
    • 在 C 语言中,允许在循环体内部遮蔽在 init-statement 中声明的同名变量;
    • 在 C++ 中严禁这样做:若在 init-statement 中声明了某个变量,在循环体 statement 中再次声明同名字变量会导致编译错误。

8.6 范围for(Range-for)

C++11 起引入,用于更简洁、可读性更高地遍历序列(如容器、数组等)中的所有元素:

for ( init-statement(optional) item-declaration : range-initializer ) statement
  • 版本特性
    • C++20:引入了 init-statement(可在循环前声明初始化变量或控制变量)。
    • C++23:对临时 range-initializer 的生命周期扩展进行了精细化调整。

底层展开等价形式C++20/C++23 的完整形式为例,其内部逻辑在语义上等价于以下代码(/* ... */ 为内部阐述专用):

{
    init-statement
    auto&& /* range */ = range-initializer;
    auto /* begin */ = /* begin-expr */;
    auto /* end */ = /* end-expr */;
    for ( ; /* begin */ != /* end */; ++/* begin */)
    {
        item-declaration = * /* begin */;
        statement
    }
}

查找规则: 编译器按以下优先级确定如何获取迭代器边界:

  1. 数组类型:若 /* range */ 是引用自大小为 NN 的数组,则 begin 为数组首地址,end 为首地址加 NN
  2. 成员函数优先:若 /* range */ 的类型是类类型 CC,且在其作用域中能通过成员查找找到 beginend(无论其访问权限或成员类型),则使用 C::begin()C::end()
  3. ADL 查找 :否则,通过 ADL 查找命名空间级别的 begin(/* range */)end(/* range */)
  4. C++17 新特性beginend 的返回类型可以不相同end 甚至不需要是传统迭代器,只要能与 begin 进行不相等比较(!=)即可。

临时范围初始化器的生命周期陷阱与规避

  • 临时对象延长:如果 range-initializer 返回一个临时对象,其生命周期会被绑定到转发引用 /* range */ 上,延长至循环结束。
  • C++20/C++23 规避方案:若涉及复杂的链式调用(如返回临时对象的成员函数),在 C++20 之前可能导致悬空引用或未定义行为。使用 init-statement 可以安全地延长生命周期:
    for (T thing = foo(); auto& x : thing.items()) { /* ... */ } // C++20 起安全
    

注意事项与最佳实践

  • 万能引用推荐:在泛型代码中,使用 for (auto&& var : sequence) 能够完美保持元素的 cv-qualifier(常量性与引用属性),是最安全、推荐的写法。
  • Copy-on-Write(COW)陷阱:对于具有写时复制特性的非 const 对象,直接遍历(如 for (auto x : str))可能会隐式调用非 constbegin() 从而触发不必要的深拷贝。若仅用于只读,应显式使用 std::as_const(str)

8.7 扩展语句(template for

  • 基本定义template forC++26 引入的一项编译期语言构造,用于将一段代码针对一组编译期可确定的元素进行静态展开。
  • 主要用途:作为对传统 for 循环或重复模板代码的现代化替代方案,它能以更高可读性的语法,将相同的逻辑应用到多个(甚至异构的)表达式、容器元素或结构化绑定对象上。
  • 语法结构
    template for ( init-statement(optional) item-declaration : expansion-initializer ) compound-statement
    

三大核心展开类别: 根据 expansion-initializer 的语法特征和性质,template for 分为以下三种具体形态:

  • 枚举扩展语句
    • 触发条件:当 expansion-initializer 是一个花括号包围的表达式列表(通常可结合包展开 pack expansion)时。
    • 展开逻辑:编译器在编译期将复合语句针对列表中的每一个表达式进行逐个展开(类似于静态解包),每个元素独立拥有自己的 item-declaration 作用域。
  • 迭代扩展语句
    • 触发条件:当 expansion-initializer 是一个可迭代表达式(即能够通过成员或 ADL 找到 begin/end 的范围类型,且类型非数组)。
    • 展开逻辑:编译期通过恒定求值(consteval)计算出范围的大小 NN,然后将循环静态展开为 NN 个独立的块。其中的迭代器和指针在编译期确定。
  • 解构扩展语句
    • 触发条件:当 expansion-initializer 是一个不可迭代的表达式时。
    • 展开逻辑:利用类型的结构化绑定大小NN
      • N=0N = 0,仅执行初始化语句和临时对象生命周期绑定。
      • N>0N > 0,则通过结构化绑定将对象解构为 NN 个底层分量(u0,u1,,uN1u_0, u_1, \dots, u_{N-1}),并将复合语句静态展开 NN 次,每次将 item-declaration 绑定到对应的分量上。

临时对象的生命周期扩展: 与普通的范围基 for 循环类似,template forexpansion-initializer 中产生的临时对象提供了严密的生命周期保障:

  • 对于枚举扩展,表达式列表里产生的临时对象,其生命周期会延长至从该元素初始化的 item-declaration 结束。
  • 对于迭代和解构扩展expansion-initializer 产生的临时对象(例如临时实例 T())会被绑定到内部的转发引用或结构化绑定上,其生命周期会一直持续到整个展开语句块结束,从而彻底避免悬空引用问题。

8.8 continue

continue 语句用于终止当前迭代,跳过循环体中剩余的代码:

continue ;
  • 前置条件continue 语句必须被以下类型的语句所包容
    1. 迭代语句for、范围基 forwhiledo-while)。
    2. 扩展语句(C++26 的 template for)。

控制流行为

  • 针对传统迭代语句(while / do-while / for / Range-for)
    • while / do-while / 范围基 for:控制流直接跳过当前迭代剩余的语句,跳转到循环条件检查或下一次迭代的准备阶段。
    • 传统 for 循环:控制流会先去执行 for 语句头部的第三个表达式(即循环计数器的更新表达式,如 ++i),然后再进行下一次的 condition 条件判断。
  • 针对 C++26 扩展语句(template for
    • 控制流会直接跳到当前展开项的末尾,相当于提前结束该特定编译期展开项的当前逻辑,直接进入下一个元素的静态展开。

8.9 break

break 语句用于无条件终止并跳出最内层的特定封闭语句:

break ;
  • 前置条件break 语句必须被以下类型的语句所包容(Enclosed)
    1. 迭代语句for、范围基 forwhiledo-while)。
    2. 扩展语句(C++26 的 template for)。
    3. switch 语句

控制流行为

  • 当执行 break 时,它会立即终止最内层的封闭循环或 switch 语句。
  • 控制流随后会直接跳转到被终止语句之后的下一条语句继续执行。

对象析构与多重嵌套限制

  • 自动销毁:与任何块退出一样,在被终止的循环或 switch 的复合语句内部、或者在循环/switchcondition 中声明的所有自动存储期对象,都会在控制流离开前按构造的相反顺序被销毁。
  • 无法跳出多重循环break 只能跳出一层最内层的循环或 switch。若需要一次性跳出多重嵌套循环,通常需要借助 goto 语句。

8.10 goto

goto 语句用于无条件地将控制流转移到指定的标号位置:

goto label ;
  • 函数限制goto 语句及其跳转的目标 label 必须位于同一个函数内部,但标号可以出现在 goto 语句的前面或后面。

对象析构与生命周期管理

  • 跳出作用域:当控制流通过 goto 离开任何自动存储期变量的作用域时(例如向后跳到变量声明之前,或向前跳出包含该变量的复合语句),所有离开作用域的对象会按照与其构造相反的顺序被依次调用析构函数。

跳转规则与限制(核心安全机制)

  • 允许的操作
    • 跳出控制流受限语句goto 可以从 try 块、if constexpr 等控制流受限语句内部跳到外部(前提是仍处在同一函数内)。
  • 严格禁止的操作(编译错误)
    • 禁止跳入控制流受限语句goto 无法将控制流转移到任何控制流受限语句内部。
    • 禁止跨越自动变量初始化:严禁使用 goto 向前跳跃并进入任何具有非平凡生命周期/初始化要求的自动变量的作用域。
      • 若跨越的变量是平凡类型 、具有平凡默认构造函数与析构函数的类类型、或者它们的 cv-qualified 版本及数组,且均未显式初始化,则允许跳过(程序合法)。
      • 只要涉及常规对象的初始化或非平凡构造,此类向前跳转会导致编译报错(程序语法错误)

8.11 return

return 语句用于终止当前函数的执行,并将结果值(如有)返回给调用方:

return expression ;               // (1) 表达式返回值
return braced-init-list ;         // (2) 列表初始化返回值 (since C++11)
return co_return expression ;     // (3) 协程返回值 (since C++20)
return co_return braced-init-list ; // (4) 协程列表初始化返回值 (since C++20)
  • 返回值隐式转换:对 expression 求值后,会通过隐式转换适配函数的返回类型。
  • void 与特殊函数限制:返回类型为 void 的函数允许使用无表达式的 return;(或返回 void 类型的表达式);但在构造函数和析构函数中严禁使用带有表达式的 return

控制流与隐式返回行为

  • 自动补全返回
    • 若控制流到达以下函数末尾而未遇到 return
      • 返回类型为 void 的函数、构造函数、析构函数、或者返回 void 的函数 try 块,编译器会自动执行隐式的 return;
      • main 函数是个特例,若流落到末尾,会自动执行 return 0;
  • 未定义行为(UB) :除了 main 函数和特定协程外,常规有返回值的函数如果在末尾没有执行 return 就流落出去,会导致未定义行为

返回值的生命周期与求值顺序

  • 临时对象销毁顺序:在函数返回结果的复制初始化与表达式末尾临时对象销毁之间存在明确的顺序链:
    1. 函数返回结果的复制初始化。
    2. 表达式末尾所有临时对象的销毁。
    3. 包含该 return 语句的块中局部变量的销毁(按构造的相反顺序)。
  • C++26 悬空引用新规:若函数的返回类型是引用类型,且 return 语句将该引用绑定到了某个临时表达式的结果上,则程序编译报错 (since C++26)。

优化机制:隐式移动与强制拷贝消除

  • 自动移动
    • 局部变量优化:当返回一个满足特定条件的局部自动变量(如非 volatile 对象或 rvalue 引用)时,编译器会将其视为右值(xvalue / rvalue),优先尝试调用移动构造函数
    • 强制拷贝消除 :自 C++17 起,若 return 的表达式是 prvalue(纯右值),结果对象由该表达式直接初始化,完全不需要调用拷贝或移动构造函数。

8.12 contract_assert

contract_assertC++26 引入的全新契约断言语句,用于在函数或 Lambda 表达式的主体内部验证内部状态条件:

contract_assert attr(optional) ( predicate ) ;
  • 谓词要求predicate 必须是一个布尔表达式,正常情况下应该在运行时评估为 true
  • 执行行为
    • Debug 构建:确保条件成立。若条件评估为 false,或者在评估过程中通过抛出异常退出,则会触发违规处理机制(例如直接终止程序)。
    • Release 构建:可以被忽略,从而免除运行时开销以优化性能。

特性宏与标准支持

  • 特征测试宏__cpp_contracts 的值为 202502L (since C++26)。
  • 提案来源:基于 C++26 的 Contracts 提案(如 P2900 序列)。

##8.13 事务内存 事务内存(Transactional Memory, TM TS)是一种并发同步机制,它将一组语句组合成一个事务,具有以下核心保证:

  • 原子性 :事务内的所有语句要么全部发生,要么全部不发生。
  • 隔离性 :事务中的语句无法观察到其他并行事务所做的半写入修改(半状态)。
  • 底层实现:典型实现结合了硬件事务内存(HTM) (在硬件支持和容量范围内)与软件事务内存(STM) (基于乐观并发,当检测到冲突时会静默重试)。

同步块

synchronized {
    // 语句
}
  • 语义:以类似全局锁的方式执行复合语句——程序中所有最外层的同步块在一个单一的总顺序中执行。
  • 特性
    • 同步块不是事务,它们可以调用非事务安全的函数。
    • 编译器会运用乐观并发对其中安全的子代码进行优化,但调用未内联的非 transaction_safe 函数时可能会退化为加锁保护。
  • 退出方式:可以通过正常结束、gotobreakcontinuereturn 或抛出异常离开。禁止使用 std::longjmp 跳出,也禁止通过 gotoswitch 进入。

原子块

atomic_noexcept   compound-statement // 1) 抛出异常时调用 std::abort
atomic_cancel     compound-statement // 2) 抛出特定取消异常时回滚内存修改
atomic_commit     compound-statement // 3) 抛出异常时正常提交
  • 核心限制:原子块中的复合语句严禁执行任何非 transaction-safe 的表达式、语句或调用非 transaction-safe 的函数(这在编译期会导致错误)。
  • 回滚机制(atomic_cancel :当抛出标准取消异常(如 std::bad_allocstd::exception 及其派生类、std::tx_exception<T>)时,事务将被取消,所有被修改的内存位置将恢复到事务开始时的状态。

事务安全函数

  • 显式声明:可以使用关键字 transaction_safe 显式声明一个函数是事务安全的。
  • 隐式推导:普通函数如果不包含违规操作(如读写 volatile 变量、调用非安全析构/函数等),也可能隐式具备安全性。
  • 函数指针转换:指向事务安全函数(或成员函数)的指针可以隐式转换为普通的函数(或成员函数)指针。

优化属性[[optimize_for_synchronized]]

  • 可应用于函数声明的第一个声明上。
  • 指示编译器为来自 synchronized 语句的调用优化函数定义,避免对那些绝大多数情况下事务安全、仅极少数情况(如哈希表重哈希、分配新内存块)需要串行化的操作进行完全加锁,从而提升并发性能。

如果在此文章中您有所收获,请给作者一个鼓励,点个赞,谢谢支持

技术变化都很快,但基础技术、理论知识永远都是那些;作者希望在余后的生活中,对常用技术点进行基础知识分享;如果你觉得文章写的不错,请给予关注和点赞;如果文章存在错误,也请多多指教!