现代C++ 6 声明
文章目录
- C++ 中的冲突声明规则
- 1. **对应声明(Corresponding Declarations)**
- 2. **对应函数重载(Corresponding Function Overloads)**
- 3. **对应函数模板重载(Corresponding Function Template Overloads)**
- 4. **同一实体的多个声明(Multiple Declarations of the Same Entity)**
- 5. **限制(Restrictions)**
- 6. **潜在冲突的声明(Potentially Conflicting Declarations)**
- 7. **示例代码**
- 8. **总结**
- C++ 中的存储类说明符 (Storage Class Specifiers)
- 1. **存储期 (Storage Duration)**
- 2. **存储类说明符 (Storage Class Specifiers)**
- 3. **链接 (Linkage)**
- 4. **具体规则**
- 5. **示例代码**
- 6. **输出示例**
- 7. **总结**
- 翻译单元本地实体 (TU-Local Entities) 自 C++20 起
- 1. **TU-Local 实体的定义**
- 2. **公开 (Exposure)**
- 3. **TU-Local 约束**
- 4. **示例代码**
- 示例 1:模块单元中的 TU-Local 实体
- 示例 2:TU-Local 实体的正确使用
- 示例 3:公开的限制
- 示例 4:模块分区中的 TU-Local 实体
- 5. **总结**
- 语言链接 (Language Linkage) in C++
- 1. **语言链接的基本概念**
- 2. **`extern "C"` 语法**
- 3. **语言链接的传播**
- 4. **内部链接与语言链接的关系**
- 5. **语言链接冲突**
- 6. **特殊的 "C" 链接规则**
- 7. **跨命名空间的重新声明**
- 8. **嵌套的语言链接说明符**
- 9. **条件编译与 `extern "C"`**
- 10. **现代编译器的行为**
- 11. **总结**
- 命名空间 (Namespaces) in C++
- 1. **命名空间的基本概念**
- 2. **命名空间的语法**
- 3. **命名空间的作用域和查找规则**
- 4. **命名空间的扩展**
- 5. **内联命名空间**
- 6. **未命名命名空间**
- 7. **命名空间别名**
- 8. **友元声明与命名空间**
- 9. **总结**
- Using 声明 (Using Declarations) in C++
- 1. **`using` 声明的基本语法**
- 2. **`using` 声明的用途**
- 3. **`using` 声明的行为**
- 4. **示例**
- 引入命名空间成员
- 引入基类成员
- 引入枚举器(自 C++20 起)
- 5. **`using` 声明的限制**
- 示例:引入同名函数
- 6. **`using` 指令 (Using Directives)**
- 语法
- 行为
- 示例
- 7. **注意事项**
- 示例:避免 `using namespace std`
- 8. **总结**
- 引用声明 (Reference Declarations) in C++
- 1. **引用的基本语法**
- 2. **引用的初始化**
- 3. **引用折叠**
- 4. **左值引用**
- 示例:作为对象别名
- 示例:按引用传递参数
- 示例:返回左值引用
- 5. **右值引用**
- 示例:延长临时对象的生存期
- 示例:重载解析
- 示例:移动语义
- 6. **转发引用 (Forwarding References)**
- 示例:模板中的转发引用
- 示例:`auto&&` 作为转发引用
- 7. **悬空引用 (Dangling References)**
- 示例:悬空引用
- 8. **类型不可访问的引用**
- 示例:类型不可访问的引用
- 9. **调用不兼容的引用**
- 示例:调用不兼容的引用
- 10. **总结**
- 指针声明 (Pointer Declarations) in C++
- 1. **基本语法**
- 2. **指针的值**
- 3. **对象指针**
- 示例:指向对象的指针
- 示例:解引用指针
- 4. **指向数组的指针**
- 示例:指向数组的指针
- 5. **指向基类的指针**
- 示例:指向基类的指针
- 6. **指向 void 的指针**
- 示例:指向 void 的指针
- 7. **指向函数的指针**
- 示例:指向函数的指针
- 示例:解引用函数指针
- 8. **指向成员的指针**
- 8.1 **指向数据成员的指针**
- 示例:指向数据成员的指针
- 8.2 **指向成员函数的指针**
- 示例:指向成员函数的指针
- 示例:成员函数指针的隐式转换
- 示例:成员函数指针的显式转换
- 9. **多级指针和混合组合**
- 示例:多级指针
- 10. **总结**
- 空指针 (Null Pointers) in C++
- 1. **空指针值 (Null Pointer Value)**
- 2. **空指针常量 (Null Pointer Constant)**
- 示例:空指针的使用
- 3. **零初始化和值初始化**
- 4. **无效指针 (Invalid Pointers)**
- 示例:无效指针
- 5. **常量性 (Constness)**
- 示例:常量指针
- 6. **复合指针类型 (Composite Pointer Type)**
- 示例:复合指针类型
- 7. **总结**
- 数组声明 (Array Declarations) in C++
- 1. **基本语法**
- 示例:简单数组声明
- 2. **多维数组**
- 示例:多维数组
- 3. **数组到指针的隐式转换**
- 示例:数组到指针的隐式转换
- 示例:多维数组的指针
- 4. **未知边界的数组**
- 示例:未知边界数组
- 示例:边界推断
- 5. **指向未知边界数组的指针和引用**
- 示例:指向未知边界数组的指针和引用
- 6. **数组右值**
- 示例:数组右值
- 7. **数组赋值**
- 示例:数组赋值
- 8. **零大小数组**
- 示例:零大小数组
- 9. **总结**
- 结构化绑定声明 (Structured Bindings) in C++ (自 C++17 起)
- 1. **基本语法**
- 2. **绑定过程**
- 情况 1:绑定数组
- 情况 2:绑定实现元组操作的类型
- 情况 3:绑定到数据成员
- 3. **初始化顺序**
- 4. **注意事项**
- 5. **示例**
- 示例 1:插入集合
- 示例 2:绑定位域
- 6. **总结**
- 枚举声明 (Enumerations) in C++
- 1. **基本语法**
- 2. **非限定作用域枚举 (Unscoped Enumerations)**
- 示例 1:未指定底层类型的非限定作用域枚举
- 示例 2:指定底层类型的非限定作用域枚举
- 示例 3:匿名枚举
- 3. **限定作用域枚举 (Scoped Enumerations)**
- 示例 1:限定作用域枚举
- 示例 2:限定作用域枚举的底层类型
- 4. **不透明枚举声明 (Opaque Enum Declarations)**
- 示例:
- 5. **枚举量的初始化**
- 示例:
- 6. **枚举与类成员**
- 示例:
- 7. **`using enum` 声明 (C++20)**
- 示例:
- 8. **枚举的转换**
- 示例:
- 9. **枚举的属性 (C++17)**
- 示例:
- 10. **总结**
- 内联说明符 (inline) in C++
- 1. **内联函数 (Inline Functions)**
- 1.1 **基本语法**
- 1.2 **内联函数的特点**
- 1.3 **历史背景**
- 1.4 **示例**
- 2. **内联变量 (Inline Variables) (自 C++17 起)**
- 2.1 **基本语法**
- 2.2 **内联变量的特点**
- 2.3 **示例**
- 3. **内联函数与内联变量的区别**
- 4. **注意事项**
- 5. **总结**
- `const` 和 `volatile` 类型限定符 (CV Qualifiers) in C++
- 1. **`const` 限定符**
- 1.1 **基本语法**
- 1.2 **`const` 的特点**
- 1.3 **示例**
- 2. **`volatile` 限定符**
- 2.1 **基本语法**
- 2.2 **`volatile` 的特点**
- 2.3 **示例**
- 3. **`const volatile` 限定符**
- 3.1 **基本语法**
- 3.2 **`const volatile` 的特点**
- 3.3 **示例**
- 4. **`mutable` 说明符**
- 4.1 **基本语法**
- 4.2 **`mutable` 的特点**
- 4.3 **示例**
- 5. **转换规则**
- 5.1 **示例**
- 6. **注意事项**
- 7. **总结**
- `constexpr` 说明符 (自 C++11 起)
- 1. **`constexpr` 变量**
- 1.1 **基本语法**
- 1.2 **要求**
- 1.3 **示例**
- 2. **`constexpr` 函数**
- 2.1 **基本语法**
- 2.2 **要求**
- 2.3 **示例**
- 3. **`constexpr` 构造函数**
- 3.1 **基本语法**
- 3.2 **要求**
- 3.3 **示例**
- 4. **`constexpr` 析构函数**
- 4.1 **基本语法**
- 4.2 **要求**
- 4.3 **示例**
- 5. **`constexpr` 模板和特化**
- 5.1 **示例**
- 6. **`constexpr` 和 `consteval`**
- 6.1 **示例**
- 7. **注意事项**
- 8. **总结**
- `consteval` 规范 (自 C++20 起)
- 1. **基本语法**
- 2. **`consteval` 的特点**
- 3. **示例**
- 3.1 **简单的 `consteval` 函数**
- 3.2 **嵌套调用**
- 3.3 **立即函数的标识符限制**
- 3.4 **结合 `static_assert`**
- 4. **注意事项**
- 5. **总结**
- `constinit` 说明符 (自 C++20 起)
- 1. **基本语法**
- 2. **`constinit` 的特点**
- 3. **示例**
- 3.1 **静态初始化**
- 3.2 **`constinit` 和 `constexpr` 的区别**
- 3.3 **线程局部变量**
- 4. **注意事项**
- 5. **总结**
- `decltype` 说明符 (自 C++11 起)
- 1. **基本语法**
- 2. **`decltype(实体)` 的行为**
- 示例
- 3. **`decltype(表达式)` 的行为**
- 示例
- 4. **特殊情况**
- 示例
- 5. **`decltype(auto)`**
- 示例
- 6. **`decltype` 在模板中的应用**
- 示例
- 7. **`decltype` 与 lambda 表达式**
- 示例
- 8. **总结**
- 占位符类型说明符 (自 C++11 起)
- 1. **基本语法**
- 语法示例
- 2. **`auto` 的行为**
- 示例
- 3. **`decltype(auto)` 的行为**
- 示例
- 4. **带类型约束的 `auto` 和 `decltype(auto)` (自 C++20 起)**
- 示例
- 5. **占位符类型说明符的应用场景**
- 示例
- 6. **注意事项**
- 7. **总结**
- `typedef` 说明符
- 1. **基本语法**
- 2. **`typedef` 的行为**
- 3. **`typedef` 的应用场景**
- 4. **注意事项**
- 5. **`typedef` 与类型别名模板(C++11 起)**
- 示例
- 6. **示例**
- 7. **总结**
- 类型别名与别名模板 (自 C++11 起)
- 1. **类型别名 (`using`)**
- 语法
- 示例
- 2. **别名模板**
- 语法
- 示例
- 3. **别名模板的特性**
- 4. **类型别名与 `typedef` 的比较**
- 5. **示例代码**
- 6. **总结**
- 详尽类型说明符 (Elaborated Type Specifier)
- 1. **语法**
- 2. **解释**
- 2.1 **引用已声明的类型**
- 2.2 **处理命名冲突**
- 2.3 **声明新的类名**
- 2.4 **不透明枚举声明**
- 2.5 **注入类名**
- 2.6 **引用 typedef 名称、类型别名、模板类型参数或别名模板特化**
- 2.7 **关键字一致性**
- 2.8 **作为模板参数**
- 3. **示例代码**
- 4. **总结**
- 属性说明符序列 (自 C++11 起)
- 1. **语法**
- 示例
- 2. **属性的作用范围**
- 3. **标准属性**
- 4. **非标准属性**
- 示例
- 5. **属性说明符序列的组合**
- 示例
- 6. **`alignas` 说明符 (自 C++11 起)**
- 语法
- 示例
- 输出示例
- 7. **注意事项**
- 8. **预处理器宏**
- 示例
- 9. **总结**
- `static_assert` 声明 (自 C++11 起)
- 1. **语法**
- 2. **解释**
- 3. **作用范围**
- 4. **行为**
- 5. **示例**
- 示例 1:基本用法
- 示例 2:模板中的 `static_assert`
- 示例 3:带有用户生成错误消息的 `static_assert`(自 C++26 起)
- 示例 4:模板中的延迟断言
- 6. **特性测试宏**
- 7. **注意事项**
- 8. **总结**
C++ 中的冲突声明规则
在C++中,**冲突声明(Conflicting Declarations)**是指两个或多个声明试图引入相同的实体(如变量、函数、类等),但这些声明之间存在不兼容之处。为了避免歧义和潜在的错误,C++标准对冲突声明进行了严格的限制。以下是关于冲突声明的详细总结:
1. 对应声明(Corresponding Declarations)
两个声明被认为是对应的,如果它们满足以下条件之一:
- 它们都声明了构造函数。
- 它们都声明了析构函数。
- 它们都是函数或函数模板,并且声明了对应的重载。
如果两个声明是对应的,但声明了不同的实体,则它们是潜在冲突的声明。
2. 对应函数重载(Corresponding Function Overloads)
两个函数声明被认为是对应的重载,如果它们满足以下所有条件:
- 它们的参数类型列表相同(自 C++23 起,省略显式对象参数的类型)。
- 它们具有等效的尾随
requires
子句(如果有,除了友元声明)。 - 如果它们都是非静态成员函数,则它们还需要满足以下要求之一:
- 其中恰好一个是隐式对象成员函数,没有引用限定符,并且它们的对象参数的类型在删除顶层引用后是相同的(自 C++23 起)。
- 它们的对象参数具有相同的类型。
3. 对应函数模板重载(Corresponding Function Template Overloads)
两个函数模板声明被认为是对应的重载,如果它们满足以下所有条件:
- 它们的模板参数列表长度相同。
- 它们的对应模板参数是等效的。
- 它们具有等效的参数类型列表(自 C++23 起,省略显式对象参数的类型)。
- 它们具有等效的返回类型。
- 它们的对应模板参数要么都声明为没有约束,要么都声明为具有等效约束。
- 它们具有等效的尾随
requires
子句(如果有)。 - 如果它们都是非静态成员函数模板,则它们还需要满足以下要求之一:
- 其中恰好一个是隐式对象成员函数模板,没有引用限定符,并且它们的对象参数的类型在删除所有引用后是等效的(自 C++23 起)。
- 它们的对象参数具有等效的类型。
4. 同一实体的多个声明(Multiple Declarations of the Same Entity)
如果两个声明满足以下条件,则它们声明同一个实体:
- 它们对应。
- 它们具有相同的目标作用域,该作用域不是函数参数作用域或模板参数作用域。
- 它们都不是名称无关的声明(自 C++26 起)。
- 它们出现在同一个翻译单元中,或者都声明具有模块链接的名称并附加到同一个模块(自 C++20 起),或者都声明具有外部链接的名称。
5. 限制(Restrictions)
如果两个声明违反了以下限制之一,则程序是格式错误的:
- 如果一个声明将实体 E 声明为变量,则另一个也必须将 E 声明为相同类型的变量。
- 如果一个声明将 E 声明为函数,则另一个也必须将 E 声明为相同类型的函数。
- 如果一个声明将 E 声明为枚举器,则另一个也必须将 E 声明为枚举器。
- 如果一个声明将 E 声明为命名空间,则另一个也必须将 E 声明为命名空间。
- 如果一个声明将 E 声明为类类型,则另一个也必须将 E 声明为类类型。
- 如果一个声明将 E 声明为枚举类型,则另一个也必须将 E 声明为枚举类型。
- 如果一个声明将 E 声明为类模板,则另一个也必须将 E 声明为具有等效模板参数列表的类模板。
- 如果一个声明将 E 声明为函数模板,则另一个也必须将 E 声明为具有等效模板参数列表和类型的函数模板。
- 如果一个声明将 E 声明为别名模板,则另一个也必须将 E 声明为具有等效模板参数列表和类型标识的别名模板(自 C++11 起)。
- 如果一个声明将 E 声明为变量模板的部分特化,则另一个也必须将 E 声明为具有等效模板参数列表和类型的变量模板的部分特化(自 C++14 起)。
- 如果一个声明将 E 声明为概念,则另一个也必须将 E 声明为概念(自 C++20 起)。
6. 潜在冲突的声明(Potentially Conflicting Declarations)
如果两个声明对应但声明了不同的实体,则它们是潜在冲突的声明。如果在任何范围内,一个名称绑定到两个潜在冲突的声明 A 和 B,B 不是名称独立的(自 C++26 起),并且 A 先于 B,则程序是格式错误的。
7. 示例代码
以下是一些示例代码,展示了冲突声明的不同情况:
// 示例 1: 冲突的函数声明
void g(); // #1
void g(int); // OK, different entity from #1 (they do not correspond)
int g(); // Error: same entity as #1 with different type// 示例 2: 冲突的命名空间声明
namespace h {} // Error: same entity as void h(), but not a function// 示例 3: 冲突的变量声明
void f()
{int x, y;void x(); // Error: different entity for xint y; // Error: redefinition
}// 示例 4: 冲突的枚举器声明
enum { f }; // Error: different entity for ::f// 示例 5: 冲突的命名空间别名声明
namespace A {}
namespace B = A;
namespace B = A; // OK, no effect
namespace B = B; // OK, no effect
namespace A = B; // OK, no effect
namespace B {} // Error: different entity for B// 示例 6: 名称无关的声明(自 C++26 起)
void g()
{int _;_ = 0; // OKint _; // OK since C++26, name-independent declaration_ = 0; // Error: two non-function declarations in the lookup set
}// 示例 7: 静态变量与普通变量冲突
void h ()
{int _; // #1_ ++; // OKstatic int _; // Error: conflicts with #1 because static variables are not name-independent
}
8. 总结
C++ 对冲突声明的规则非常严格,旨在确保程序的正确性和一致性。主要规则包括:
- 对应声明:两个声明必须声明相同的实体或对应的重载。
- 对应函数重载:函数声明必须具有相同的参数类型列表和尾随
requires
子句。 - 对应函数模板重载:函数模板声明必须具有等效的模板参数列表、参数类型列表、返回类型和约束。
- 同一实体的多个声明:两个声明必须在同一作用域中声明相同的实体,并且不能是名称无关的声明。
- 限制:不同类型的声明不能混合使用,例如不能将变量声明为函数,或将类声明为命名空间。
- 潜在冲突的声明:如果两个声明对应但声明了不同的实体,则它们是潜在冲突的,程序可能是格式错误的。
通过遵循这些规则,C++ 确保了声明的一致性和可预测性,避免了潜在的编译错误和运行时问题。
C++ 中的存储类说明符 (Storage Class Specifiers)
存储类说明符是 C++ 声明语法的一部分,用于控制变量和函数的存储期(storage duration)和链接(linkage)。它们与名称的作用域一起决定了对象的生命周期和可见性。C++ 提供了多种存储类说明符,每种说明符都有其特定的行为和用途。
1. 存储期 (Storage Duration)
存储期定义了对象在其生命周期内存在的最小时间段。C++ 支持以下几种存储期:
-
静态存储期 (Static Storage Duration)
- 对象在整个程序的运行期间存在。
- 适用于全局变量、静态局部变量、命名空间范围内的静态变量以及使用
static
或extern
声明的变量。 - 线程存储期 (Thread Storage Duration)(自 C++11 起)
- 对象在创建它的线程的整个生命周期内存在。
- 每个线程都有一个独立的对象实例。
- 使用
thread_local
关键字声明。
-
自动存储期 (Automatic Storage Duration)
- 对象在其作用域块(如函数或代码块)中存在,当块结束时被销毁。
- 适用于局部变量(未显式声明为
static
或thread_local
的变量)和函数参数。
-
动态存储期 (Dynamic Storage Duration)
- 对象通过
new
表达式动态分配,并通过delete
表达式释放。 - 对象的生命周期由程序员显式控制。
- 对象通过
2. 存储类说明符 (Storage Class Specifiers)
C++ 提供了以下存储类说明符:
说明符 | 描述 | 适用范围 |
---|---|---|
auto | 自动存储期(仅限块范围),用于类型推断(自 C++11 起不再作为存储类说明符)。 | 变量声明 |
register | 提示编译器将变量存储在寄存器中(已弃用,自 C++17 起不再是存储类说明符)。 | 变量声明 |
static | 静态存储期,适用于全局变量、静态局部变量、命名空间范围内的静态变量。 | 变量声明、函数声明、成员声明 |
thread_local | 线程存储期,每个线程有一个独立的对象实例。 | 变量声明、成员声明 |
extern | 外部链接,表示变量或函数在其他翻译单元中定义。 | 变量声明、函数声明 |
mutable | 允许在常量对象中修改成员变量。不影响存储期。 | 成员声明 |
3. 链接 (Linkage)
链接决定了一个名称在程序中的可见性和唯一性。C++ 支持以下几种链接:
-
无链接 (No Linkage)
- 名称只能在同一作用域内访问。
- 适用于块范围内的局部变量、局部类及其成员函数等。
-
内部链接 (Internal Linkage)
- 名称在同一翻译单元中的不同作用域中可以重新声明,但不会与其他翻译单元中的同名实体冲突。
- 适用于声明为
static
的变量、函数、模板等,以及在未命名命名空间中声明的实体。
-
外部链接 (External Linkage)
- 名称可以在多个翻译单元中访问,并且所有翻译单元中的同名实体都引用同一个对象或函数。
- 适用于未声明为
static
的全局变量、函数、类、枚举等。
-
模块链接 (Module Linkage)(自 C++20 起)
- 名称可以在同一模块的不同翻译单元中访问,但不会与其他模块中的同名实体冲突。
- 适用于声明附加到命名模块的实体。
4. 具体规则
-
static
:- 在命名空间范围内声明的变量具有静态存储期和内部链接。
- 在块范围内声明的变量具有静态存储期和无链接。
- 在类中声明的静态成员具有静态存储期和外部链接(除非类本身有内部链接)。
-
thread_local
:- 可以与
static
或extern
一起使用,表示线程局部存储期。 - 每个线程都有一个独立的对象实例。
- 可以与
-
extern
:- 表示变量或函数在其他翻译单元中定义,具有外部链接。
- 不能与
inline
一起使用(除非在 C++20 中用于模块接口单元)。
-
mutable
:- 用于允许在常量对象中修改成员变量。
- 不影响存储期或链接。
5. 示例代码
以下是一些示例代码,展示了不同存储类说明符的使用:
#include <iostream>
#include <mutex>
#include <string>
#include <thread>// 静态存储期和外部链接
int global_var = 0;// 线程局部存储期
thread_local unsigned int thread_var = 1;
std::mutex cout_mutex;void increase_global() {++global_var;
}void increase_thread_var(const std::string& thread_name) {++thread_var; // 修改线程局部变量是安全的std::lock_guard<std::mutex> lock(cout_mutex);std::cout << "Thread-local variable for " << thread_name << ": " << thread_var << '\n';
}void static_local_example() {// 静态局部变量,静态存储期static int static_local = 0;++static_local;std::cout << "Static local variable: " << static_local << '\n';
}int main() {// 自动存储期int auto_var = 42;std::cout << "Auto variable: " << auto_var << '\n';// 静态局部变量示例static_local_example();static_local_example();// 线程局部变量示例std::thread t1(increase_thread_var, "t1");std::thread t2(increase_thread_var, "t2");t1.join();t2.join();// 全局变量示例increase_global();std::cout << "Global variable: " << global_var << '\n';return 0;
}
6. 输出示例
Auto variable: 42
Static local variable: 1
Static local variable: 2
Thread-local variable for t1: 2
Thread-local variable for t2: 2
Global variable: 1
7. 总结
- 存储期决定了对象的生命周期,包括静态存储期、线程存储期、自动存储期和动态存储期。
- 存储类说明符如
static
、thread_local
、extern
和mutable
用于控制存储期和链接。 - 链接决定了名称的可见性和唯一性,包括无链接、内部链接、外部链接和模块链接(自 C++20 起)。
- 正确使用存储类说明符可以确保程序的正确性和效率,尤其是在多线程和跨文件编程中。
通过理解这些概念,开发者可以更好地控制程序中对象的生命周期和可见性,从而编写更高效、更安全的代码。
翻译单元本地实体 (TU-Local Entities) 自 C++20 起
C++20 引入了**翻译单元本地实体(Translation Unit Local, TU-Local)**的概念,旨在防止那些本来应该仅限于单个翻译单元内部使用的实体被意外地公开并使用在其他翻译单元中。这有助于提高模块化编程的安全性和封装性,避免不必要的依赖和冲突。
1. TU-Local 实体的定义
一个实体被认为是 TU-Local 的,如果它满足以下条件之一:
- 具有内部链接的名称:例如,在未命名命名空间中声明的变量、函数或类型。
- 没有链接的名称,并且是在 TU-Local 实体的定义内声明的:例如,在 TU-Local 类、函数或模板中声明的实体。
- 无名类型:在类说明符、函数体或初始化器之外定义的无名类型。
- TU-Local 模板的特化:特化了一个 TU-Local 模板的实体。
- 具有任何 TU-Local 模板参数的模板的特化:如果模板的某个参数是 TU-Local 的,则其特化也是 TU-Local 的。
- 由用于声明仅 TU-Local 实体的定义类型说明符引入的实体:例如,
static
或thread_local
修饰的模板。
此外,如果一个值或对象是指向 TU-Local 函数的指针,或者是指向与 TU-Local 变量关联的对象的指针,或者是类或数组类型的对象,且其子对象或非静态数据成员引用了 TU-Local 实体,那么该值或对象也是 TU-Local 的。
2. 公开 (Exposure)
一个声明 公开 了一个实体 E,如果该声明包含以下内容之一:
- lambda 表达式:如果声明包含一个 lambda 表达式,而该表达式的闭包类型是 E。
- 标识符表达式、类型说明符、嵌套名称说明符、模板名称或概念名称:如果 E 不是函数或函数模板,而声明中包含表示 E 的这些元素。
- 命名 E 的表达式或引用包含 E 的重载集的标识符表达式:如果 E 是函数或函数模板,而声明中包含命名 E 的表达式或引用包含 E 的重载集的标识符表达式。
然而,某些情况下声明不会被视为公开 TU-Local 实体:
- 非内联函数或函数模板的函数体:函数体内的代码不会公开 TU-Local 实体。
- 变量或变量模板的初始化器:初始化器中的代码不会公开 TU-Local 实体。
- 类定义中的友元声明:友元声明不会公开 TU-Local 实体。
- 对非易失性
const
对象或具有内部或无链接的引用:如果这些引用使用非 ODR-use 的常量表达式初始化,则不会公开 TU-Local 实体。 - 定义初始化为 TU-Local 值的
constexpr
变量:只要这些变量本身不是在常量表达式中使用的 TU-Local 实体,它们也不会公开 TU-Local 实体。
3. TU-Local 约束
C++20 对 TU-Local 实体的使用施加了严格的约束,以确保模块化编程的安全性:
-
模块接口单元或模块分区中的非 TU-Local 实体不能公开 TU-Local 实体:如果在模块接口单元(在私有模块片段中,如果有的话)或模块分区中,声明或推断指南对非 TU-Local 实体进行了公开,则程序是格式错误的。在其他上下文中进行这样的声明是不推荐的。
-
一个翻译单元中的声明不能命名另一个翻译单元中声明的 TU-Local 实体:如果一个翻译单元中的声明命名了另一个翻译单元中声明的 TU-Local 实体,而该实体不是头文件单元,则程序是格式错误的。为模板特化实例化的声明出现在特化的实例化点。
4. 示例代码
以下是一些示例代码,展示了 TU-Local 实体的使用和约束:
示例 1:模块单元中的 TU-Local 实体
// Module unit with TU-local constraints
export module Foo;import <iostream>;namespace {class LolWatchThis { // internal linkage, cannot be exportedpublic:static void say_hello() {std::cout << "Hello, everyone!\n";}};
}// Error: LolWatchThis is exposed as return type
export LolWatchThis lolwut() {return LolWatchThis();
}
在这个例子中,LolWatchThis
是一个具有内部链接的类,因为它在未命名命名空间中声明。然而,lolwut
函数试图将其作为返回类型导出,这是不允许的,因为这会公开一个 TU-Local 实体。
示例 2:TU-Local 实体的正确使用
// TU-local entities with internal linkage
namespace {int tul_var = 1; // TU-local variableint tul_func() { return 1; } // TU-local functionstruct tul_type { int mem; }; // TU-local (class) type
}template<typename T>
static int tul_func_temp() { return 1; } // TU-local template// TU-local template specialization
template<>
static int tul_func_temp<int>() { return 3; } // TU-local specialization// template specialization with TU-local template argument
template <> struct std::hash<tul_type> { // TU-local specializationstd::size_t operator()(const tul_type& t) const { return 4u; }
};
在这个例子中,所有声明的实体都是 TU-Local 的,因为它们具有内部链接或是在 TU-Local 实体的定义内声明的。这些实体不会被公开,因此可以安全地使用。
示例 3:公开的限制
// Module unit with exposure rules
export module A;static void f() {}inline void it() { f(); } // error: is an exposure of f
static inline void its() { f(); } // OK
template<int> void g() { its(); } // OK
template void g<0>();decltype(f) *fp; // error: f (though not its type) is TU-local
auto &fr = f; // OK
constexpr auto &fr2 = fr; // error: is an exposure of f
constexpr static auto fp2 = fr; // OK
struct S { void (&ref)(); } s{f}; // OK: value is TU-local
constexpr extern struct W { S &s; } wrap{s}; // OK: value is not TU-localstatic auto x = []{ f(); }; // OK
auto x2 = x; // error: the closure type is TU-local
int y = ([]{ f(); }(), 0); // error: the closure type is not TU-local
int y2 = (x, 0); // OK
在这个例子中,it
函数公开了 f
,导致编译错误,因为 f
是一个 TU-Local 实体。相反,its
函数是 static inline
的,因此不会公开 f
。类似地,fp
和 fr2
的声明也会导致编译错误,因为它们公开了 f
,而 fr
和 fp2
的声明是合法的,因为它们不会公开 f
。
示例 4:模块分区中的 TU-Local 实体
// Translation unit #1
export module A;
static void f() {}
inline void it() { f(); } // error: is an exposure of f
static inline void its() { f(); } // OK
template<int> void g() { its(); } // OK
template void g<0>();// Translation unit #2
module A;
void other() {g<0>(); // OK: specialization is explicitly instantiatedg<1>(); // error: instantiation uses TU-local itsh(N::A{}); // error: overload set contains TU-local N::adl(int)h(0); // OK: calls adl(double)adl(N::A{}); // OK; N::adl(int) not found, calls N::adl(N::A)fr(); // OK: calls fconstexpr auto ptr = fr; // error: fr is not usable in constant expressions here
}
在这个例子中,other
函数尝试使用 g<1>
,但这是不允许的,因为 g<1>
的实例化会使用 TU-Local 的 its
。同样,h(N::A{})
也会导致编译错误,因为 N::adl(int)
是 TU-Local 的。
5. 总结
- TU-Local 实体 是为了防止那些本应仅限于单个翻译单元内部使用的实体被意外地公开并使用在其他翻译单元中。
- 公开 是指声明中包含某些特定的语法元素,这些元素可能会暴露 TU-Local 实体。
- TU-Local 约束 确保模块化编程的安全性,防止 TU-Local 实体被不恰当地使用或公开。
- 通过遵循这些规则,开发者可以更好地控制程序的封装性和安全性,避免不必要的依赖和冲突。
通过理解这些概念,开发者可以在 C++20 中更安全地编写模块化代码,确保 TU-Local 实体不会被意外地公开,从而提高代码的健壮性和可维护性。
语言链接 (Language Linkage) in C++
语言链接(Language Linkage)是 C++ 中的一个特性,它允许程序单元之间进行跨语言的链接。通过语言链接,C++ 程序可以与用其他编程语言(如 C)编写的代码进行交互。这在需要调用 C 库函数或使 C++ 函数能够被 C 代码调用时特别有用。
1. 语言链接的基本概念
每个具有外部链接的函数类型、函数名和变量名都有一个称为语言链接的属性。语言链接封装了链接到用不同编程语言编写的程序单元所需的规则,包括:
- 调用约定:如何传递参数和返回值。
- 名称修饰(Name Mangling):编译器如何处理函数名以支持重载等特性。
C++ 标准保证支持两种语言链接:
- “C++”:默认的语言链接,适用于 C++ 代码。
- “C”:用于与 C 代码进行链接,禁用了 C++ 的名称修饰和某些特性(如重载)。
2. extern "C"
语法
extern "C"
是最常用的语言链接说明符,用于指定 C 链接。它可以应用于单个声明或一组声明:
extern "C" {// 一组声明int open(const char *path_name, int flags); // C 函数声明
}// 单个声明
extern "C" void handler(int) {std::cout << "Callback invoked\n"; // 可以使用 C++
}
3. 语言链接的传播
语言链接是函数类型的一部分,因此指针到函数也会继承语言链接。例如:
extern "C" void f1(void(*pf)()); // f1 有 C 链接,pf 指向 C 函数extern "C" typedef void FUNC(); // FUNC 是 C 函数类型FUNC f2; // f2 有 C++ 链接,但类型是 C 函数
extern "C" FUNC f3; // f3 有 C 链接,类型是 C 函数void (*pf2)(FUNC*); // pf2 有 C++ 链接,指向 C++ 函数,该函数接受 C 函数指针
4. 内部链接与语言链接的关系
如果一个声明具有内部链接(如 static
),则语言链接对其没有影响。例如:
extern "C" {static void f4(); // f4 有内部链接,但其类型有 C 链接
}
5. 语言链接冲突
如果两个声明为同一个实体提供了不同的语言链接,则程序是不合法的,编译器可能会报错。例如:
extern "C" int f();
extern "C++" int f(); // 错误:不同的语言链接extern "C" int g();
int g(); // 正确:g 有 C 链接int h(); // 默认有 C++ 链接
extern "C" int h(); // 错误:不同的语言链接
6. 特殊的 “C” 链接规则
当类成员、带有尾置 requires
子句的友元函数(自 C++20 起),或非静态成员函数出现在 extern "C"
块中时,它们的类型仍然保持 C++ 链接,但参数类型保持 C 链接。例如:
extern "C" {class X {void mf(); // mf 和其类型有 C++ 链接void mf2(void(*)()); // mf2 有 C++ 链接,参数类型是 C 函数指针};
}
7. 跨命名空间的重新声明
当一个具有 “C” 链接的实体在全局作用域或其他命名空间中重新声明时,只要它们都是函数或变量,并且类型相同,则它们被视为同一个实体。例如:
extern "C" {int x;int f();int g() { return 1; }
}namespace A {int x; // 错误:重新定义 "x"int f(); // 正确:重新声明 "f"int g() { return 1; } // 错误:重新定义 "g"
}
8. 嵌套的语言链接说明符
当语言链接说明符嵌套时,最内层的说明符生效。例如:
extern "C" {extern "C++" void f(); // f 有 C++ 链接
}
9. 条件编译与 extern "C"
为了使头文件可以在 C 和 C++ 代码中共享,通常使用条件编译来隐藏 extern "C"
。例如:
#ifdef __cplusplus
extern "C" {
#endifint foo(int, int);#ifdef __cplusplus
}
#endif
10. 现代编译器的行为
大多数现代编译器(如 GCC 和 Clang)不会区分具有不同语言链接的函数类型,这意味着你不能仅通过语言链接来重载函数。例如:
extern "C" using c_predfun = int(const void*, const void*);
extern "C++" using cpp_predfun = int(const void*, const void*);static_assert(std::is_same<c_predfun, cpp_predfun>::value,"C 和 C++ 语言链接不应区分函数类型。");void qsort(void* base, std::size_t nmemb, std::size_t size, c_predfun* compar);
void qsort(void* base, std::size_t nmemb, std::size_t size, cpp_predfun* compar);
上述代码在大多数编译器中不会被视为重载,因为 c_predfun
和 cpp_predfun
被认为是相同的类型。
11. 总结
- 语言链接 是 C++ 中用于跨语言链接的关键特性,允许 C++ 代码与 C 代码进行互操作。
extern "C"
是最常用的语言链接说明符,禁用了 C++ 的名称修饰,使得 C++ 函数可以被 C 代码调用,反之亦然。- 语言链接冲突 会导致编译错误,因此必须确保同一实体的所有声明使用相同的语言链接。
- 跨命名空间的重新声明 是允许的,只要它们都是函数或变量,并且类型相同。
- 条件编译 是实现 C 和 C++ 代码共享头文件的常见方法。
- 现代编译器 通常不会区分具有不同语言链接的函数类型,因此不能仅通过语言链接来重载函数。
通过理解这些规则,开发者可以更有效地编写跨语言兼容的代码,确保 C++ 代码能够与 C 库和其他编程语言无缝集成。
命名空间 (Namespaces) in C++
命名空间是 C++ 中用于组织代码和防止名称冲突的重要机制。通过将相关的类、函数、变量等封装在命名空间中,可以避免不同部分的代码之间发生名称冲突,并提高代码的可读性和可维护性。
1. 命名空间的基本概念
命名空间提供了一种逻辑上的分隔,使得不同模块或库中的同名实体不会相互干扰。C++ 提供了多种方式来定义和使用命名空间:
- 命名命名空间:通过
namespace
关键字定义一个命名空间,并在其内部声明实体。 - 内联命名空间(自 C++11 起):通过
inline namespace
关键字定义一个内联命名空间,其成员被视为封闭命名空间的成员。 - 未命名命名空间:通过
namespace { ... }
定义一个未命名命名空间,其成员具有内部链接,只能在当前翻译单元中访问。 - 嵌套命名空间(自 C++17 起):可以通过
namespace A::B::C { ... }
的形式定义嵌套命名空间。 - 命名空间别名:通过
namespace 别名 = 限定命名空间;
定义一个命名空间的别名,方便引用复杂的命名空间路径。
2. 命名空间的语法
以下是 C++ 中命名空间的主要语法形式:
// 1. 命名命名空间定义
namespace 命名空间名称 {// 声明
}// 2. 内联命名空间定义(自 C++11 起)
inline namespace 命名空间名称 {// 声明
}// 3. 未命名命名空间定义
namespace {// 声明
}// 4. 使用作用域解析运算符访问命名空间成员
命名空间名称::成员名称// 5. using 指令:引入整个命名空间的成员
using namespace 命名空间名称;// 6. using 声明:引入特定的命名空间成员
using 命名空间名称::成员名称;// 7. 命名空间别名定义
namespace 名称 = 限定命名空间;// 8. 嵌套命名空间定义(自 C++17 起)
namespace 命名空间名称::成员名称 {// 声明
}// 9. 嵌套内联命名空间定义(自 C++20 起)
namespace 命名空间名称::inline 成员名称 {// 声明
}
3. 命名空间的作用域和查找规则
- 全局命名空间:所有不在任何命名空间内的声明都属于全局命名空间。可以通过
::
显式引用全局命名空间中的实体。 - 命名空间作用域:命名空间内的声明位于命名空间作用域中,只有通过显式的限定名称(如
命名空间名称::成员名称
)才能从外部访问。 - 名称查找:
- 非限定查找:在当前作用域及其嵌套的命名空间中查找名称。
- 限定查找:通过作用域解析运算符
::
明确指定查找的命名空间。 - 依赖于参数的查找(ADL):当调用函数时,编译器会根据函数参数的类型自动查找关联的命名空间。
4. 命名空间的扩展
可以在多个地方定义同一个命名空间,这些定义会被合并为同一个命名空间。例如:
namespace Q {void f() {// ...}
}namespace Q {void g() {// ...}
}
上述代码等效于:
namespace Q {void f() {// ...}void g() {// ...}
}
5. 内联命名空间
内联命名空间(inline namespace
)的成员被视为封闭命名空间的成员。这在库版本控制中非常有用,允许不同的库版本在不同的内联命名空间中实现,同时仍然可以通过封闭命名空间进行访问。例如:
namespace Lib {inline namespace Lib_1 {template<typename T> class A;}template<typename T> void g(T) { /* ... */ }
}struct MyClass { /* ... */ };namespace Lib {template<> class A<MyClass> { /* ... */ };
}int main() {Lib::A<MyClass> a;g(a); // ok, Lib is an associated namespace of A
}
6. 未命名命名空间
未命名命名空间的成员具有内部链接,只能在当前翻译单元中访问。每个未命名命名空间都有一个唯一的名称,但在同一翻译单元中,多个未命名命名空间定义会被视为同一个命名空间。例如:
namespace {int i; // 定义 ::(unique)::i
}void f() {i++; // 增加 ::(unique)::i
}namespace A {namespace {int i; // A::(unique)::iint j; // A::(unique)::j}void g() { i++; } // A::(unique)::i++
}using namespace A; // 引入 A 中的所有名称到全局命名空间void h() {i++; // 错误:::(unique)::i 和 ::A::(unique)::i 都在作用域中A::i++; // 正确,增加 ::A::(unique)::ij++; // 正确,增加 ::A::(unique)::j
}
7. 命名空间别名
命名空间别名可以简化对复杂命名空间路径的引用。例如:
namespace N1 {namespace N2 {int x = 42;}
}namespace N = N1::N2;int main() {std::cout << N::x << std::endl; // 输出 42
}
8. 友元声明与命名空间
在类内部的友元声明引入的名称成为最内层封闭命名空间的成员,但它们对普通名称查找(非限定或限定)不可见,除非在类定义之前或之后在命名空间作用域内提供匹配的声明。例如:
void h(int);
namespace A {class X {friend void f(X); // A::f 是友元class Y {friend void g(); // A::g 是友元friend void h(int); // A::h 是友元,不与 ::h 冲突};};X x;void g() // 定义 A::g{f(x); // A::X::f 通过 ADL 找到}void f(X) {} // 定义 A::fvoid h(int) {} // 定义 A::h
}
9. 总结
- 命名空间 是 C++ 中用于组织代码和防止名称冲突的重要工具。
- 命名命名空间、内联命名空间、未命名命名空间 和 嵌套命名空间 提供了灵活的方式来管理代码结构。
- using 指令 和 using 声明 可以简化对命名空间成员的访问。
- 命名空间别名 可以简化对复杂命名空间路径的引用。
- 友元声明 引入的名称成为最内层封闭命名空间的成员,但对普通名称查找不可见。
- 内联命名空间 在库版本控制中非常有用,允许不同的库版本在不同的内联命名空间中实现,同时仍然可以通过封闭命名空间进行访问。
通过合理使用命名空间,开发者可以更好地组织代码,避免名称冲突,并提高代码的可读性和可维护性。
Using 声明 (Using Declarations) in C++
using
声明是 C++ 中用于将其他命名空间、类或枚举中的名称引入当前作用域的机制。它们可以简化代码,避免频繁使用作用域解析运算符 ::
,并且在继承和模板编程中特别有用。
1. using
声明的基本语法
using typename(可选) 嵌套名称说明符 非限定标识符;
typename
(可选):当using
声明用于将基类的成员类型引入类模板时,可以使用typename
来解析依赖名称。嵌套名称说明符
:一系列名称和作用域解析运算符::
,以作用域解析运算符结尾。单个::
指的是全局命名空间。非限定标识符
:一个标识符表达式。
自 C++17 起,using
声明还可以包含多个声明符:
using 声明符列表;
2. using
声明的用途
using
声明可以用于以下几种情况:
-
将命名空间成员引入其他命名空间或块作用域:
- 将命名空间中的特定函数、变量或类引入当前命名空间或块作用域。
-
将基类成员引入派生类定义:
- 将基类的成员函数或成员变量引入派生类,使得这些成员可以直接在派生类中使用。
-
将枚举器引入命名空间、块或类作用域(自 C++20 起):
- 将枚举类型的枚举器引入当前作用域,使得可以直接使用这些枚举器。
3. using
声明的行为
- 引入的名称可以像任何其他名称一样使用,包括从其他作用域进行限定查找。
using
声明不会引入后续扩展的命名空间成员,除非这些成员是类模板的部分特化。using
声明不能命名模板 ID、命名空间或作用域枚举器(直到 C++20)。- 每个
using
声明只引入一个名称,例如枚举的using
声明不会引入其所有枚举器。 - 常规声明的所有限制、隐藏和重载规则都适用于
using
声明。
4. 示例
引入命名空间成员
void f();
namespace A {void g();
}namespace X {using ::f; // 全局 f 现在可见为 ::X::fusing A::g; // A::g 现在可见为 ::X::gusing A::g, A::g; // (C++17) OK: 可以重复声明
}void h() {X::f(); // 调用 ::fX::g(); // 调用 A::g
}
引入基类成员
struct Base {void f(int);
};struct Derived : Base {using Base::f; // 引入 Base::fvoid f(double); // 重载 f
};void func(Derived& d) {d.f(42); // 调用 Base::f(int)d.f(3.14); // 调用 Derived::f(double)
}
引入枚举器(自 C++20 起)
enum class Color { Red, Green, Blue };namespace N {using Color::Red;using Color::Green;
}void func() {N::Red; // OK: 使用 N::RedN::Blue; // Error: Blue 未引入
}
5. using
声明的限制
- 不能引入同名的多个函数,除非它们是同一函数的不同重载版本。
- 不能引入同名的多个模板,除非它们是同一模板的不同特化版本。
- 不能引入同名的多个实体,否则会导致编译错误。
示例:引入同名函数
namespace B {void f(int);void f(double);
}namespace C {void f(int);void f(double);void f(char);
}void h() {using B::f; // 引入 B::f(int), B::f(double)using C::f; // 引入 C::f(int), C::f(double), 和 C::f(char)f('h'); // 调用 C::f(char)f(1); // 错误:B::f(int) 或 C::f(int)?void f(int); // 错误:f(int) 与 C::f(int) 和 B::f(int) 冲突
}
6. using
指令 (Using Directives)
using
指令(using namespace
)与 using
声明不同,它会将整个命名空间中的所有名称引入当前作用域。using
指令不会阻止声明相同的名称,因此可能会导致名称冲突。
语法
attr(可选) using namespace 嵌套名称说明符(可选) 命名空间名称;
attr
(可选):适用于此using
指令的任意数量的属性(自 C++11 起)。嵌套名称说明符
(可选):以作用域解析运算符结尾的名称和作用域解析运算符::
序列。单个::
指的是全局命名空间。命名空间名称
:命名空间的名称。
行为
using
指令不会向其出现的作用域添加任何名称,因此不会阻止声明相同的名称。using
指令是可传递的:如果一个作用域包含一个指定命名空间的using
指令,而该命名空间本身包含针对某个命名空间的using
指令,则效果就好像第二个命名空间中的using
指令出现在第一个命名空间中一样。
示例
namespace A {int i;
}namespace B {int i;int j;namespace C {namespace D {using namespace A; // A 中的名称被引入到 Dint j;int k;int a = i; // i 是 B::i,因为 A::i 被 B::i 隐藏int b = ::i; // 错误:全局命名空间中没有 i}using namespace D; // D 和 A 中的名称被引入到 Cint k = 89; // OK: 可以声明与引入的名称相同的新名称int l = k; // 模糊:C::k 或 D::kint m = i; // OK: B::i 隐藏了 A::iint n = j; // OK: D::j 隐藏了 B::j}
}// 这些定义都是等价的:
int t0 = B::i;
int t1 = B::C::a;
int t2 = B::C::D::a;
7. 注意事项
- 避免在头文件中使用
using namespace
:在头文件中使用using namespace
可能会导致不希望的名称冲突,尤其是在全局命名空间中。因此,建议在头文件中避免使用using namespace
,而在源文件中使用using
声明来引入需要的名称。
示例:避免 using namespace std
#include <vector>namespace vec {template<typename T>class vector {// ...};
}int main() {std::vector<int> v1; // 标准库的 vectorvec::vector<int> v2; // 用户定义的 vector// v1 = v2; // 错误:v1 和 v2 是不同类型{using namespace std;vector<int> v3; // 等同于 std::vectorv1 = v3; // OK}{using vec::vector;vector<int> v4; // 等同于 vec::vectorv2 = v4; // OK}
}
8. 总结
using
声明 是一种强大的工具,用于将特定的名称引入当前作用域,简化代码并提高可读性。using
指令 则会引入整个命名空间中的所有名称,可能会导致名称冲突,因此应谨慎使用,尤其是在头文件中。- 合理使用
using
声明 可以避免频繁使用作用域解析运算符,同时保持代码的清晰性和模块化。 - 避免在头文件中使用
using namespace
,以防止不必要的名称冲突和潜在的维护问题。
通过理解和正确使用 using
声明和 using
指令,开发者可以更好地组织代码,减少冗余,并提高代码的可维护性。
引用声明 (Reference Declarations) in C++
引用是 C++ 中用于为已存在的对象或函数创建别名的机制。引用可以分为左值引用(&
)和右值引用(&&
),它们在不同的场景中有不同的用途。此外,C++ 还引入了转发引用(forwarding references)来支持完美转发。
1. 引用的基本语法
S& D; // 左值引用
S&& D; // 右值引用 (自 C++11 起)
S
:类型说明符。D
:声明符,不能是另一个引用声明符(即没有对引用的引用)。attr
(可选):属性列表(自 C++11 起)。
2. 引用的初始化
引用必须在声明时初始化为一个有效的对象或函数。引用不是对象,因此它们不一定占用存储空间,尽管编译器可能会为实现所需的语义分配存储空间(例如,引用类型的非静态数据成员通常会增加类的大小)。
- 类型“对(可能为 cv 限定的)void 的引用”无法形成。
- 引用类型不能在顶层进行 cv 限定,即
const int&
是合法的,但int const&
和int& const
是等价的,且顶层const
会被忽略。
3. 引用折叠
当通过模板或 typedef 中的类型操作形成对引用的引用时,引用折叠规则适用:
- 对右值引用的右值引用折叠为右值引用:
T&& &&
折叠为T&&
。 - 所有其他组合折叠为左值引用:
T& &
、T& &&
、T&& &
都折叠为T&
。
这使得 std::forward
成为可能,因为它可以根据传入的参数类型选择适当的引用类型。
typedef int& lref;
typedef int&& rref;
int n;lref& r1 = n; // type of r1 is int&
lref&& r2 = n; // type of r2 is int&
rref& r3 = n; // type of r3 is int&
rref&& r4 = 1; // type of r4 is int&&
4. 左值引用
左值引用用于为已存在的对象创建别名,并可以选择不同的 cv 限定符。它们可以用于按引用传递参数,以及返回左值表达式。
示例:作为对象别名
#include <iostream>
#include <string>int main() {std::string s = "Ex";std::string& r1 = s;const std::string& r2 = s;r1 += "ample"; // 修改了 s// r2 += "!"; // 错误:不能通过 const 引用修改对象std::cout << r2 << '\n'; // 输出 "Example"
}
示例:按引用传递参数
#include <iostream>
#include <string>void double_string(std::string& s) {s += s; // 's' 是 main() 中的 'str'
}int main() {std::string str = "Test";double_string(str);std::cout << str << '\n'; // 输出 "TestTest"
}
示例:返回左值引用
#include <iostream>
#include <string>char& char_number(std::string& s, std::size_t n) {return s.at(n); // string::at() 返回对 char 的引用
}int main() {std::string str = "Test";char_number(str, 1) = 'a'; // 函数调用是左值,可以赋值std::cout << str << '\n'; // 输出 "Tast"
}
5. 右值引用
右值引用主要用于延长临时对象的生存期,并支持移动语义。它们可以绑定到右值(包括纯右值和将要移动的值),而左值引用只能绑定到左值。
示例:延长临时对象的生存期
#include <iostream>
#include <string>int main() {std::string s1 = "Test";// std::string&& r1 = s1; // 错误:不能绑定到左值const std::string& r2 = s1 + s1; // 好的:左值引用到 const 延长了临时对象的生存期// r2 += "Test"; // 错误:不能通过 const 引用修改对象std::string&& r3 = s1 + s1; // 好的:右值引用延长了临时对象的生存期r3 += "Test"; // 好的:可以通过非 const 引用修改对象std::cout << r3 << '\n'; // 输出 "TestTestTest"
}
示例:重载解析
当一个函数同时具有右值引用和左值引用重载时,右值引用重载绑定到右值,而左值引用重载绑定到左值。
#include <iostream>
#include <utility>void f(int& x) {std::cout << "lvalue reference overload f(" << x << ")\n";
}void f(const int& x) {std::cout << "lvalue reference to const overload f(" << x << ")\n";
}void f(int&& x) {std::cout << "rvalue reference overload f(" << x << ")\n";
}int main() {int i = 1;const int ci = 2;f(i); // 调用 f(int&)f(ci); // 调用 f(const int&)f(3); // 调用 f(int&&)// 如果没有 f(int&&) 重载,将调用 f(const int&)f(std::move(i)); // 调用 f(int&&)// 右值引用变量在表达式中是左值int&& x = 1;f(x); // 调用 f(int&)f(std::move(x)); // 调用 f(int&&)
}
示例:移动语义
右值引用使得移动构造函数和移动赋值运算符成为可能,从而提高了性能,尤其是在处理大型对象时。
#include <iostream>
#include <vector>int main() {std::vector<int> v{1, 2, 3, 4, 5};std::vector<int> v2(std::move(v)); // 绑定右值引用到 vassert(v.empty()); // v 已被移动,现在为空
}
6. 转发引用 (Forwarding References)
转发引用是一种特殊的引用,它保留了函数参数的值类别,使得通过 std::forward
对其进行转发成为可能。转发引用可以是:
- 模板参数的右值引用:当函数模板的参数是对其类型模板参数的右值引用时,该参数是转发引用。
auto&&
:除非从大括号括起来的初始化列表推断出来,或者在进行类模板参数推导时代表类模板的模板参数(自 C++17 起)。
示例:模板中的转发引用
template<class T>
void f(T&& x) {g(std::forward<T>(x)); // 保留 x 的值类别
}int main() {int i;f(i); // 参数是左值,调用 f<int&>(int&),std::forward<int&>(x) 是左值f(0); // 参数是右值,调用 f<int>(int&&),std::forward<int>(x) 是右值
}
示例:auto&&
作为转发引用
auto&& vec = foo(); // foo() 可能是左值或右值,vec 是转发引用
auto i = std::begin(vec); // 无论 vec 是左值还是右值都可以工作
(*i)++; // 无论 vec 是左值还是右值都可以工作g(std::forward<decltype(vec)>(vec)); // 转发,保留值类别for (auto&& x : f()) {// x 是转发引用;这是泛型代码中使用范围 for 的常见方式
}auto&& z = {1, 2, 3}; // 不是转发引用(初始化列表的特殊情况)
7. 悬空引用 (Dangling References)
虽然引用一旦初始化就会始终引用有效的对象或函数,但如果所引用的对象的生存期结束,而引用仍然可以访问,则会导致未定义行为。常见的例子是函数返回对自动变量的引用。
示例:悬空引用
std::string& f() {std::string s = "Example";return s; // 离开 s 的作用域:它的析构函数被调用,存储空间被释放
}std::string& r = f(); // 悬空引用
std::cout << r; // 未定义行为:读取悬空引用
std::string s = f(); // 未定义行为:从悬空引用复制初始化
注意,右值引用和对 const
的左值引用会延长临时对象的生存期(有关规则和例外,请参阅 引用初始化)。
8. 类型不可访问的引用
尝试将引用绑定到一个对象,其中转换后的初始化器是左值(在 C++11 之前)或泛左值(自 C++11 起),并且通过该对象无法类型访问对象,会导致未定义行为。
示例:类型不可访问的引用
char x alignas(int);int& ir = *reinterpret_cast<int*>(&x); // 未定义行为:// 初始化器引用 char 对象
9. 调用不兼容的引用
尝试将引用绑定到一个函数,其中转换后的初始化器是左值(在 C++11 之前)或泛左值(自 C++11 起),并且其类型与函数定义的类型不调用兼容,会导致未定义行为。
示例:调用不兼容的引用
void f(int);using F = void(float);
F& ir = *reinterpret_cast<F*>(&f); // 未定义行为:// 初始化器引用 void(int) 函数
10. 总结
- 引用 是 C++ 中用于为已存在的对象或函数创建别名的机制。
- 左值引用 (
&
) 用于按引用传递参数和返回左值表达式。 - 右值引用 (
&&
) 用于延长临时对象的生存期和支持移动语义。 - 转发引用 是一种特殊的引用,保留了函数参数的值类别,使得通过
std::forward
对其进行转发成为可能。 - 悬空引用 会导致未定义行为,应避免返回对局部对象的引用。
- 类型不可访问的引用 和 调用不兼容的引用 也会导致未定义行为,应避免。
通过理解和正确使用引用,开发者可以编写更高效、更安全的代码,特别是在处理资源管理和泛型编程时。
指针声明 (Pointer Declarations) in C++
指针是 C++ 中用于存储内存地址的变量。它们可以指向对象、函数或类的成员,并且在处理动态内存分配、数组、函数回调等方面非常有用。C++ 支持多种类型的指针,包括普通指针、指向成员的指针(数据成员和成员函数)、以及指向函数的指针。
1. 基本语法
S* D; // 普通指针
S C::* D; // 指向类 C 的非静态成员的指针
nested-name-specifier * D; // 嵌套名称空间中的指针
S
:类型说明符。D
:声明符,可以是另一个指针声明符(允许指向指针的指针),但不能是指向引用的指针。attr
(可选):属性列表(自 C++11 起)。cv
(可选):const
或volatile
限定符,应用于指针本身,而不是指向的类型(指向的类型的限定符是声明说明符序列的一部分)。
2. 指针的值
每个指针类型的值可以是以下之一:
- 指向对象或函数的指针:表示对象或函数的地址。
- 指向对象末尾之后的指针:表示对象占用的存储空间末尾之后的第一个字节的地址。
- 空指针值:表示无效地址,通常用
nullptr
表示。 - 无效指针值:表示未定义或非法的地址。
3. 对象指针
对象指针可以通过取地址运算符 &
来初始化,指向任何对象类型的表达式(包括其他指针类型)。
示例:指向对象的指针
int n;
int* np = &n; // 指向 int 的指针
int* const* npp = &np; // 指向 const 指针的非 const 指针int a[2];
int (*ap)[2] = &a; // 指向数组的指针struct S { int n; };
S s = {1};
int* sp = &s.n; // 指向结构体成员的指针
指针可以作为内置间接运算符 operator*
的操作数,返回指向对象的左值表达式。
示例:解引用指针
int n;
int* p = &n; // 指向 n 的指针
int& r = *p; // 引用绑定到 n
r = 7; // 将 7 存储到 n 中
std::cout << *p; // 读取 n 的值
4. 指向数组的指针
由于数组到指针的隐式转换,可以使用数组类型的表达式来初始化指向数组第一个元素的指针。
示例:指向数组的指针
int a[2];
int* p1 = a; // 指向数组 a 的第一个元素 a[0]int b[6][3][8];
int (*p2)[3][8] = b; // 指向数组 b 的第一个元素 b[0]
5. 指向基类的指针
由于派生类到基类的隐式转换,可以使用派生类的地址来初始化指向基类的指针。
示例:指向基类的指针
struct Base {};
struct Derived : Base {};Derived d;
Base* p = &d; // 派生类到基类的隐式转换
如果 Derived
是多态的,则可以使用此类指针进行虚函数调用。
6. 指向 void 的指针
任何类型的对象指针都可以隐式转换为指向 void
的指针,指针值保持不变。反向转换需要 static_cast
或显式转换。
示例:指向 void 的指针
int n = 1;
int* p1 = &n;
void* pv = p1;
int* p2 = static_cast<int*>(pv);
std::cout << *p2 << '\n'; // 输出 1
指向 void
的指针与指向 char
的指针具有相同的大小、表示形式和对齐方式。它们常用于传递未知类型的对象,例如在 C 接口中。
7. 指向函数的指针
指向函数的指针可以用非成员函数或静态成员函数的地址初始化。取地址运算符是可选的,因为存在从函数到指针的隐式转换。
示例:指向函数的指针
void f(int);
void (*p1)(int) = &f;
void (*p2)(int) = f; // 同样有效// 使用函数指针调用函数
p1(7); // 调用 f(7)
(*p2)(7); // 也可以这样调用
指向函数的指针可以用作函数调用运算符的左操作数,这将调用指向的函数。
示例:解引用函数指针
int f();
int (*p)() = f; // 指向函数 f 的指针
int (&r)() = *p; // 绑定到函数 f 的引用
r(); // 通过引用调用 f
(*p)(); // 通过指针调用 f
p(); // 直接通过指针调用 f
8. 指向成员的指针
指向成员的指针可以分为两种类型:指向数据成员的指针和指向成员函数的指针。
8.1 指向数据成员的指针
指向类 C
的非静态成员对象 m
的指针可以用表达式 &C::m
精确初始化。
示例:指向数据成员的指针
struct C { int m; };int main() {int C::* p = &C::m; // 指向数据成员 m 的指针C c = {7};std::cout << c.*p << '\n'; // 输出 7C* cp = &c;cp->m = 10;std::cout << cp->*p << '\n'; // 输出 10
}
指向可访问的非歧义非虚基类的数据成员的指针可以隐式转换为指向派生类的相同数据成员的指针。
8.2 指向成员函数的指针
指向类 C
的非静态成员函数 f
的指针可以用表达式 &C::f
精确初始化。
示例:指向成员函数的指针
struct C {void f(int n) { std::cout << n << '\n'; }
};int main() {void (C::* p)(int) = &C::f; // 指向成员函数 f 的指针C c;(c.*p)(1); // 调用 c.f(1)C* cp = &c;(cp->*p)(2); // 调用 cp->f(2)
}
基类的成员函数指针可以隐式转换为派生类的相同成员函数的指针。
示例:成员函数指针的隐式转换
struct Base {void f(int n) { std::cout << n << '\n'; }
};
struct Derived : Base {};int main() {void (Base::* bp)(int) = &Base::f;void (Derived::* dp)(int) = bp;Derived d;(d.*dp)(1);(d.*bp)(2);
}
相反方向的转换,即从派生类的成员函数指针到基类的成员函数指针,也允许使用 static_cast
和显式转换。
示例:成员函数指针的显式转换
struct Base {};
struct Derived : Base {void f(int n) { std::cout << n << '\n'; }
};int main() {void (Derived::* dp)(int) = &Derived::f;void (Base::* bp)(int) = static_cast<void (Base::*)(int)>(dp);Derived d;(d.*bp)(1); // 正常工作:输出 1Base b;(b.*bp)(2); // 未定义行为
}
9. 多级指针和混合组合
指向成员的指针的指向类型本身可以是指向成员的指针,允许多级指针和指向成员的指针的混合组合。
示例:多级指针
struct A {int m;int A::* const p;
};int main() {// 非 const 指向 const 数据成员的指针int A::* const A::* p1 = &A::p;const A a = {1, &A::m};std::cout << a.*(a.*p1) << '\n'; // 输出 1// 普通非 const 指向 const 数据成员的指针int A::* const* p2 = &a.p;std::cout << a.**p2 << '\n'; // 输出 1
}
10. 总结
- 指针 是 C++ 中用于存储内存地址的变量,可以指向对象、函数或类的成员。
- 对象指针 可以通过取地址运算符
&
初始化,并可以解引用以访问对象。 - 指向数组的指针 可以通过数组到指针的隐式转换初始化。
- 指向基类的指针 可以通过派生类到基类的隐式转换初始化,并支持虚函数调用。
- 指向 void 的指针 可以隐式转换为任何类型的对象指针,反向转换需要
static_cast
。 - 指向函数的指针 可以用非成员函数或静态成员函数的地址初始化,并可以用作函数调用运算符的左操作数。
- 指向成员的指针 可以分为指向数据成员的指针和指向成员函数的指针,支持多级指针和混合组合。
通过理解和正确使用指针,开发者可以编写更灵活、高效的代码,尤其是在处理动态内存分配、数组、函数回调等场景中。
空指针 (Null Pointers) in C++
空指针是 C++ 中一种特殊的指针值,表示该指针不指向任何有效的对象或函数。解引用空指针会导致未定义行为(Undefined Behavior),因此在使用指针之前必须确保它不是空的。
1. 空指针值 (Null Pointer Value)
每种类型的指针都有一个特殊的值,称为该类型的空指针值。这个值用于表示指针当前不指向任何有效的对象或函数。所有相同类型的空指针值在比较时相等。
2. 空指针常量 (Null Pointer Constant)
空指针常量可以用于将指针初始化为空值或将空值赋给现有指针。C++ 提供了两种主要的方式表示空指针常量:
- 整数字面量 0:值为零的整数字面量可以隐式转换为任何类型的空指针。
nullptr
(自 C++11 起):nullptr
是std::nullptr_t
类型的纯右值,专门用于表示空指针。它比传统的0
更加安全和明确。
此外,C 风格的宏 NULL
也可以用来表示空指针,但它扩展为实现定义的空指针常量,通常是 0
或 (void*)0
。
示例:空指针的使用
int* p1 = nullptr; // 使用 nullptr 初始化
int* p2 = 0; // 使用整数 0 初始化
int* p3 = NULL; // 使用宏 NULL 初始化if (p1 == nullptr) {std::cout << "p1 is null\n";
}
3. 零初始化和值初始化
零初始化和值初始化也会将指针初始化为其空值。例如:
int* p = {}; // 值初始化,等同于 p = nullptr;
int* q = int*(); // 值初始化,等同于 q = nullptr;
4. 无效指针 (Invalid Pointers)
当指针不再指向有效的对象或函数时,它就变成了无效指针。使用无效指针进行间接寻址(如解引用)或传递给释放函数(如 delete
)会导致未定义行为。其他使用无效指针的行为则是实现定义的。
示例:无效指针
int* f() {int obj;int* local_ptr = new (&obj) int; // 指向局部对象的指针*local_ptr = 1; // OK: 在 obj 的存储持续时间内使用return local_ptr;
}int* ptr = f(); // 返回后,obj 的存储持续时间已结束,ptr 是无效指针int* copy = ptr; // 实现定义的行为
*ptr = 2; // 未定义行为:解引用无效指针
delete ptr; // 未定义行为:释放无效指针
5. 常量性 (Constness)
在指针声明中,const
和 volatile
限定符可以应用于指针本身或指针所指向的对象。它们的位置决定了它们的作用范围:
const T*
或T const*
:指向常量对象的指针。不能通过该指针修改所指向的对象,但指针本身可以改变。T* const
:指向对象的常量指针。指针本身不能改变,但可以通过该指针修改所指向的对象。const T* const
或T const* const
:指向常量对象的常量指针。既不能通过该指针修改所指向的对象,也不能改变指针本身。
示例:常量指针
const int ci = 10;
const int* pc = &ci; // 指向常量 int 的非常量指针
int* const cp = &i; // 指向非常量 int 的常量指针
const int* const cpc = pc; // 指向常量 int 的常量指针i = ci; // OK: 将常量 int 的值复制到非常量 int
*cp = ci; // OK: 通过常量指针修改非常量 int
pc++; // OK: 改变指向常量 int 的指针
pc = cpc; // OK: 改变指向常量 int 的指针
pc = p; // OK: 改变指向常量 int 的指针
ppc = &pc; // OK: 指向指向常量 int 的指针ci = 1; // 错误:不能修改常量 int
ci++; // 错误:不能修改常量 int
*pc = 2; // 错误:不能通过指向常量 int 的指针修改对象
cp = &ci; // 错误:不能改变常量指针
cpc++; // 错误:不能改变常量指针
p = pc; // 错误:不能将指向常量 int 的指针赋给指向非常量 int 的指针
ppc = &p; // 错误:不能将指向非常量 int 的指针赋给指向指向常量 int 的指针
6. 复合指针类型 (Composite Pointer Type)
当比较运算符的操作数或条件运算符的第二个和第三个操作数是指针或指向成员的指针时,C++ 会确定一个复合指针类型作为这些操作数的公共类型。复合指针类型的规则如下:
- 如果两个操作数都是指针,则它们的复合指针类型是它们的共同类型。
- 如果一个操作数是指针,另一个操作数是空指针常量,则复合指针类型是指针类型。
- 如果两个操作数都是空指针常量,则复合指针类型是
std::nullptr_t
。 - 如果一个操作数是指向
cv1 void
的指针,另一个操作数是指向cv2 T
的指针,则复合指针类型是指向cv12 void
的指针,其中cv12
是cv1
和cv2
的并集。 - 如果两个操作数是指向函数类型的指针,且函数类型除
noexcept
外相同,则复合指针类型是指向第一个函数类型的指针。 - 如果两个操作数是指向类成员的指针,且类类型之一与另一个引用相关,则复合指针类型是指向引用相关的类成员的指针。
- 如果两个操作数是指向相似类型的指针,则复合指针类型是它们的限定符组合类型。
示例:复合指针类型
using p = void*;
using q = const int*;// 复合指针类型是 “const void*”
if (p == q) {// ...
}using pi = int**;
using pci = const int**;// 复合指针类型是 “const void* const *”
if (pi == pci) {// ...
}
7. 总结
- 空指针 是表示指针不指向任何有效对象或函数的特殊值,使用
nullptr
是推荐的做法。 - 无效指针 是指不再指向有效对象或函数的指针,使用无效指针进行间接寻址或释放会导致未定义行为。
- 常量性 可以应用于指针本身或指针所指向的对象,
const
和volatile
限定符的位置决定了它们的作用范围。 - 复合指针类型 是在比较或条件运算中确定的公共指针类型,遵循特定的规则来确保类型兼容性。
通过正确理解和使用空指针、无效指针和常量性,开发者可以编写更安全、可靠的代码,避免常见的指针错误。
数组声明 (Array Declarations) in C++
数组是 C++ 中用于存储固定数量相同类型元素的复合数据类型。数组声明可以创建一个包含多个元素的对象,这些元素按照线性顺序存储在连续的内存位置中。每个元素可以通过下标运算符 []
访问,索引从 0 开始。
1. 基本语法
数组声明的基本形式如下:
T a[N];
T
:元素类型,可以是任何对象类型(除了void
),包括基本类型、指针、类、枚举等。a
:数组名称。N
:数组大小,必须是一个整型常量表达式或已转换的常量表达式(自 C++14 起),其值大于零。
示例:简单数组声明
int a[5]; // 包含 5 个 int 的数组
double b[3] = {1.1, 2.2, 3.3}; // 包含 3 个 double 的数组,并初始化
char c[4] = "abc"; // 包含 4 个 char 的数组,最后一个元素为 '\0'
2. 多维数组
当数组的元素类型是另一个数组时,称为多维数组。多维数组可以视为矩阵或多维表格。
示例:多维数组
int a[2][3] = {{1, 2, 3}, {4, 5, 6}}; // 2 行 3 列的二维数组
int b[2][3][4]; // 2 行 3 列 4 深度的三维数组
多维数组的元素访问方式如下:
a[0][1] = 10; // 访问二维数组 a 的第一个行的第二个元素
b[1][2][3] = 20; // 访问三维数组 b 的第二个行的第三个列的第四个深度的元素
3. 数组到指针的隐式转换
数组类型的左值和右值可以隐式转换为指向其第一个元素的指针。这种转换在数组出现在期望指针而不是数组的上下文中时自动发生。
示例:数组到指针的隐式转换
int a[3] = {1, 2, 3};
int* p = a; // a 隐式转换为指向第一个元素的指针std::cout << sizeof(a) << '\n'; // 输出数组的大小(字节数)
std::cout << sizeof(p) << '\n'; // 输出指针的大小(字节数)for (int n : a) // 使用范围 for 循环遍历数组std::cout << n << ' '; // 输出数组元素std::iota(std::begin(a), std::end(a), 7); // 使用标准库算法填充数组
需要注意的是,多维数组的隐式转换只应用一次。例如,int b[2][3]
会衰减为 int (*p)[3]
,而不会衰减为 int**
。
示例:多维数组的指针
int b[2][3];
int (*p2)[3] = b; // b 衰减为指向第一个 3 元素行的指针
// int** p2 = b; // 错误:b 不会衰减为 int**
4. 未知边界的数组
如果在数组声明中省略了大小表达式,则声明的类型为“未知边界数组”,这是一种不完整类型。未知边界数组只能在带有聚合初始化器的声明中使用。
示例:未知边界数组
extern int x[]; // 未知边界数组
int a[] = {1, 2, 3}; // 根据初始化器推断数组大小为 3extern int a[][2]; // 合法:未知边界数组,元素是 2 个 int 的数组
// extern int b[2][]; // 错误:元素类型不完整
在同一作用域中,如果存在该实体的先前声明并指定了边界,则省略的数组边界将被认为与该先前声明中的相同。
示例:边界推断
extern int x[10];
struct S {static int y[10];
};int x[]; // OK: 边界为 10
int S::y[]; // OK: 边界为 10void f() {extern int x[];int i = sizeof(x); // 错误:不完整的对象类型
}
5. 指向未知边界数组的指针和引用
可以形成指向未知边界数组的引用和指针,但不能对它们进行指针运算或在下标运算符的左侧使用。自 C++20 起,可以从已知边界数组和指向已知边界数组的指针进行初始化或赋值。
示例:指向未知边界数组的指针和引用
extern int a1[];int (&r1)[] = a1; // 合法
int (*p1)[] = &a1; // 合法
// int (*q)[2] = &a1; // 错误(但在 C 中合法)int a2[] = {1, 2, 3};
int (&r2)[] = a2; // 合法(自 C++20)
int (*p2)[] = &a2; // 合法(自 C++20)
6. 数组右值
虽然数组不能通过值从函数返回,也不能作为大多数强制转换表达式的目标,但可以通过使用类型别名来构建使用大括号初始化的函数强制转换的数组临时变量来形成数组纯右值。自 C++17 起,可以直接访问类右值的数组成员或使用 std::move
形成数组将右值。
示例:数组右值
#include <iostream>
#include <type_traits>
#include <utility>void f(int (&&x)[2][3]) {std::cout << sizeof x << '\n';
}struct X {int i[2][3];
} x;template<typename T>
using identity = T;int main() {std::cout << sizeof X().i << '\n'; // 输出数组的大小f(X().i); // 合法:绑定到 xvalue// f(x.i); // 错误:不能绑定到 lvalueint a[2][3];f(std::move(a)); // 合法:绑定到 xvalueusing arr_t = int[2][3];f(arr_t{}); // 合法:绑定到 prvaluef(identity<int[][3]>{{1, 2, 3}, {4, 5, 6}}); // 合法:绑定到 prvalue
}
7. 数组赋值
数组类型对象不能整体修改,即使它们是左值(可以获取数组的地址),它们也不能出现在赋值运算符的左侧。但是,结构体或类的数组成员可以通过隐式定义的复制赋值运算符进行赋值。
示例:数组赋值
int a[3] = {1, 2, 3}, b[3] = {4, 5, 6};
int (*p)[3] = &a; // 合法:可以获取数组的地址
// a = b; // 错误:a 是数组struct S {int c[3];
} s1, s2 = {3, 4, 5};
s1 = s2; // 合法:隐式定义的复制赋值运算符可以赋值数组成员
8. 零大小数组
当与 new[]
表达式一起使用时,数组的大小可以为零。这样的数组没有元素,但仍然需要调用 delete[]
进行清理。
示例:零大小数组
int* p = new int[0]; // 创建一个零大小的数组
// *p = 1; // 未定义行为:不能访问 p[0] 或 *p
delete[] p; // 仍然需要调用 delete[]
9. 总结
- 数组声明 是创建一个包含多个相同类型元素的对象的方式,元素可以通过下标运算符
[]
访问。 - 多维数组 是元素类型为另一个数组的数组,访问时需要多次下标操作。
- 数组到指针的隐式转换 使得数组可以衰减为指向其第一个元素的指针,但多维数组的衰减只应用一次。
- 未知边界数组 是一种不完整类型,只能在带有聚合初始化器的声明中使用,边界可以由先前声明推断。
- 指向未知边界数组的指针和引用 可以形成,但不能进行指针运算或在下标运算符的左侧使用。
- 数组右值 可以通过类型别名和
std::move
等方式形成,允许在某些情况下传递数组临时对象。 - 数组赋值 不支持直接赋值整个数组,但可以通过结构体或类的成员进行赋值。
- 零大小数组 是有效的,但不能访问其元素,仍然需要调用
delete[]
进行清理。
通过正确理解和使用数组声明及其特性,开发者可以编写更高效、灵活的代码,尤其是在处理大量同类型数据时。
结构化绑定声明 (Structured Bindings) in C++ (自 C++17 起)
结构化绑定是 C++17 引入的一项新特性,它允许将一个表达式的子对象或元素直接绑定到多个变量。这使得代码更加简洁和易读,尤其是在处理元组、数组和类的成员时。结构化绑定本质上是现有对象的别名,类似于引用,但不需要显式使用引用类型。
1. 基本语法
结构化绑定的基本形式如下:
attr (可选) cv-auto ref-qualifier (可选) [ identifier-list ] = expression;
attr
:任何数量的属性序列。cv-auto
:可能是带有const
和volatile
限定符的auto
类型说明符,也可以包括存储类说明符static
或thread_local
。从 C++20 开始,volatile
限定符已被弃用。ref-qualifier
:可以是&
或&&
,用于指定绑定的引用类型。identifier-list
:由逗号分隔的标识符列表,每个标识符后面可以跟属性说明符序列(自 C++26 起)。expression
:一个赋值表达式,其类型必须是数组或非联合类类型,并且不能在顶层包含逗号运算符。
2. 绑定过程
结构化绑定声明首先引入一个唯一命名的变量 e
来保存初始化器的值,然后根据 e
的类型 E
以三种可能的方式之一执行绑定:
- 绑定数组:如果
E
是数组类型,则名称将绑定到数组元素。 - 绑定实现元组操作的类型:如果
E
是非联合类类型,并且std::tuple_size<E>
是一个完整类型,则使用“类似元组”的绑定协议。 - 绑定到数据成员:如果
E
是非联合类类型,但std::tuple_size<E>
不是完整类型,则名称将绑定到E
的可访问数据成员。
情况 1:绑定数组
如果 E
是数组类型,则 identifier-list
中的每个标识符都成为引用数组对应元素的左值名称。标识符的数量必须等于数组元素的数量。
int a[2] = {1, 2};
auto [x, y] = a; // x 绑定到 e[0],y 绑定到 e[1]
auto& [xr, yr] = a; // xr 绑定到 a[0],yr 绑定到 a[1]
情况 2:绑定实现元组操作的类型
如果 E
是非联合类类型,并且 std::tuple_size<E>::value
是一个格式良好的整型常量表达式,则使用“类似元组”的绑定协议。identifier-list
中的每个标识符都成为引用 E
的第 i
个元素的左值或右值名称。
float x{};
char y{};
int z{};std::tuple<float&, char&&, int> tpl(x, std::move(y), z);
const auto& [a, b, c] = tpl;
// a 绑定到 x (左值引用)
// b 绑定到 y (右值引用)
// c 绑定到 tpl 的第三个元素 (const int)
情况 3:绑定到数据成员
如果 E
是非联合类类型,但 std::tuple_size<E>
不是完整类型,则 identifier-list
中的每个标识符都成为引用 E
的非静态数据成员的左值名称。
struct S {mutable int x1 : 2;volatile double y1;
};S f() { return S{1, 2.3}; }int main() {const auto [x, y] = f(); // x 绑定到 S::x1 (int 左值)// y 绑定到 S::y1 (const volatile double 左值)std::cout << x << ' ' << y << '\n'; // 1 2.3x = -2; // OK// y = -2.; // 错误:y 是 const-qualifiedstd::cout << x << ' ' << y << '\n'; // -2 2.3
}
3. 初始化顺序
- 隐藏变量
e
的初始化 在任何val_i
的初始化之前进行。 - 每个
val_i
的初始化 在任何val_j
的初始化之前进行,其中i < j
。
4. 注意事项
- 结构化绑定不能受约束:不能使用概念(concepts)来约束结构化绑定。
- 对成员
get
的查找 忽略可访问性和非类型模板参数的确切类型。 - 临时变量的生命周期扩展:如果存在引用限定符并且表达式是一个右值,则临时变量的引用绑定(包括生命周期扩展)的通常规则适用。
- 结构化绑定不能被 lambda 表达式捕获(直到 C++20)。
5. 示例
示例 1:插入集合
#include <iomanip>
#include <iostream>
#include <set>
#include <string>int main() {std::set<std::string> myset{"hello"};for (int i{2}; i; --i) {if (auto [iter, success] = myset.insert("Hello"); success) std::cout << "Insert is successful. The value is "<< std::quoted(*iter) << ".\n";elsestd::cout << "The value " << std::quoted(*iter)<< " already exists in the set.\n";}
}
输出:
Insert is successful. The value is "Hello".
The value "Hello" already exists in the set.
示例 2:绑定位域
struct BitFields {// C++20: default member initializer for bit-fieldsint b : 4 {1}, d : 4 {2}, p : 4 {3}, q : 4 {4};
};int main() {const auto [b, d, p, q] = BitFields{};std::cout << b << ' ' << d << ' ' << p << ' ' << q << '\n';const auto [b2, d2, p2, q2] = []{ return BitFields{4, 3, 2, 1}; }();std::cout << b2 << ' ' << d2 << ' ' << p2 << ' ' << q2 << '\n';BitFields s;auto& [b3, d3, p3, q3] = s;std::cout << b3 << ' ' << d3 << ' ' << p3 << ' ' << q3 << '\n';b3 = 4, d3 = 3, p3 = 2, q3 = 1;std::cout << s.b << ' ' << s.d << ' ' << s.p << ' ' << s.q << '\n';
}
输出:
1 2 3 4
4 3 2 1
1 2 3 4
4 3 2 1
6. 总结
- 结构化绑定 提供了一种简洁的方式来解构数组、元组和类的成员,使代码更易读和维护。
- 绑定过程 根据表达式的类型分为三种情况:绑定数组、绑定实现元组操作的类型、绑定到数据成员。
- 初始化顺序 确保隐藏变量
e
在所有结构化绑定之前初始化,且每个绑定按顺序初始化。 - 注意事项 包括结构化绑定不能受约束、不能被 lambda 表达式捕获等限制。
通过正确理解和使用结构化绑定,开发者可以编写更简洁、清晰的代码,尤其是在处理复杂数据结构时。
枚举声明 (Enumerations) in C++
枚举(enum
)是 C++ 中的一种独特类型,其值被限定在一定范围内,这些值被称为枚举量(enumerators)。每个枚举量都是一个命名常量,它们的值通常是整型类型的值。枚举与其底层类型具有相同的大小、值表示和对齐要求。C++11 引入了两种不同类型的枚举:非限定作用域枚举(enum
)和限定作用域枚举(enum class
或 enum struct
),并在后续版本中引入了更多特性。
1. 基本语法
枚举的声明语法如下:
enum [class|struct] [枚举名称] [ : 底层类型 ] { 枚举量列表 } [;]
enum
:关键字,用于声明枚举。class
或struct
:可选关键字,用于声明限定作用域枚举(enum class
或enum struct
),从 C++11 开始引入。枚举名称
:枚举的名称,可以省略。底层类型
:可选的底层类型,指定枚举量的存储类型,默认为int
。从 C++11 开始支持。枚举量列表
:以逗号分隔的枚举量定义列表,每个定义可以是一个唯一的标识符或带有常量表达式的标识符。;
:结束声明,对于不透明枚举声明是必需的。
2. 非限定作用域枚举 (Unscoped Enumerations)
非限定作用域枚举使用 enum
关键字声明,枚举量直接进入封闭作用域,可以像普通常量一样使用。
示例 1:未指定底层类型的非限定作用域枚举
enum Color { red, green, blue };
Color r = red;switch(r) {case red: std::cout << "red\n"; break;case green: std::cout << "green\n"; break;case blue: std::cout << "blue\n"; break;
}
- 默认情况下,第一个枚举量的值为
0
,后续枚举量的值依次递增。 - 枚举量可以直接在封闭作用域中使用,无需前缀。
示例 2:指定底层类型的非限定作用域枚举
enum Foo : unsigned char { a, b, c = 10, d, e = 1, f, g = f + c };
// a = 0, b = 1, c = 10, d = 11, e = 1, f = 2, g = 12
- 指定底层类型为
unsigned char
,枚举量的值必须适合该类型。
示例 3:匿名枚举
enum { a, b, c = 0, d = a + 2 }; // 定义 a = 0, b = 1, c = 0, d = 2
- 未命名的枚举将枚举量直接引入封闭作用域。
3. 限定作用域枚举 (Scoped Enumerations)
限定作用域枚举使用 enum class
或 enum struct
关键字声明,枚举量包含在枚举的范围内,必须使用作用域解析运算符访问。限定作用域枚举的枚举量不会自动提升为整数类型,需要显式转换。
示例 1:限定作用域枚举
enum class Color { red, green = 20, blue };
Color r = Color::blue;switch(r) {case Color::red: std::cout << "red\n"; break;case Color::green: std::cout << "green\n"; break;case Color::blue: std::cout << "blue\n"; break;
}int n = static_cast<int>(r); // OK, n = 21
std::cout << n << '\n'; // prints 21
- 枚举量必须通过作用域解析运算符访问。
- 从限定作用域枚举到整数类型的隐式转换是不允许的,必须使用
static_cast
显式转换。
示例 2:限定作用域枚举的底层类型
enum class altitude : char { high = 'h', low = 'l' };
altitude a = altitude::low;std::cout << static_cast<char>(a) << '\n'; // prints 'l'
- 指定底层类型为
char
,枚举量的值必须适合该类型。
4. 不透明枚举声明 (Opaque Enum Declarations)
不透明枚举声明只声明枚举类型,而不定义其枚举量。这使得可以在不完全定义枚举的情况下使用其类型。
示例:
enum Color : int; // 不透明枚举声明
- 这种声明后,
Color
是一个完整类型,但其枚举量尚未定义。
5. 枚举量的初始化
枚举量可以通过常量表达式初始化,允许自定义枚举量的值。
示例:
enum Foo { a, b, c = 10, d, e = 1, f, g = f + c };
// a = 0, b = 1, c = 10, d = 11, e = 1, f = 2, g = 12
- 如果没有提供初始值,枚举量的值将按顺序递增。
- 可以为任意枚举量提供初始值,后续枚举量将继续递增。
6. 枚举与类成员
当枚举是类的成员时,可以使用类成员访问运算符 .
, ->
或作用域解析运算符 ::
访问其枚举量。
示例:
struct X {enum direction { left = 'l', right = 'r' };
};X x;
X* p = &x;int a = X::direction::left; // C++11 及以后版本
int b = X::left;
int c = x.left;
int d = p->left;
X::direction::left
使用作用域解析运算符访问枚举量。X::left
直接访问枚举量。x.left
和p->left
使用类成员访问运算符访问枚举量。
7. using enum
声明 (C++20)
using enum
声明将枚举的枚举量引入当前作用域,使得可以直接使用枚举量而不需要作用域解析运算符。
示例:
enum class fruit { orange, apple };struct S {using enum fruit; // 将 orange 和 apple 引入 S 的作用域
};void f() {S s;s.orange; // OK: names fruit::orangeS::orange; // OK: names fruit::orange
}
using enum
将枚举量引入当前作用域,避免了重复使用作用域解析运算符。- 如果两个
using enum
声明引入了相同名称的枚举量,则会导致冲突。
8. 枚举的转换
- 非限定作用域枚举:可以隐式转换为整数类型。
- 限定作用域枚举:不能隐式转换为整数类型,必须使用
static_cast
显式转换。
示例:
enum access_t { read = 1, write = 2, exec = 4 };
access_t rwe = static_cast<access_t>(7); // OK, rwe = 7
static_cast
可以将整数转换为枚举类型,但转换后的值不一定等于任何命名的枚举量。
9. 枚举的属性 (C++17)
从 C++17 开始,枚举量可以带有属性说明符。
示例:
enum [[deprecated]] E {a,b [[deprecated("use c instead")]],c
};
[[deprecated]]
属性标记整个枚举为废弃。b
枚举量带有特定的废弃消息。
10. 总结
- 枚举 是一种独特类型,其值被限定在一定范围内,枚举量是命名常量。
- 非限定作用域枚举 使用
enum
关键字声明,枚举量直接进入封闭作用域,可以隐式转换为整数类型。 - 限定作用域枚举 使用
enum class
或enum struct
关键字声明,枚举量包含在枚举的范围内,必须使用作用域解析运算符访问,不能隐式转换为整数类型。 - 不透明枚举声明 只声明枚举类型,而不定义其枚举量。
using enum
声明 (C++20) 将枚举量引入当前作用域,避免重复使用作用域解析运算符。- 枚举量的属性 (C++17) 允许为枚举量添加属性说明符,如
[[deprecated]]
。
通过合理使用枚举,开发者可以编写更清晰、安全的代码,特别是在处理有限范围的值时。限定作用域枚举尤其有助于避免命名冲突,并提高代码的可读性和维护性。
内联说明符 (inline) in C++
inline
说明符是 C++ 中用于函数和变量(自 C++17 起)的一种特性,它允许在多个翻译单元中定义相同的函数或变量,而不违反一次定义规则(ODR, One Definition Rule)。尽管最初的设计目的是为了提示编译器进行内联替换以优化性能,但现代 C++ 中 inline
的主要用途是允许多个定义,而不是强制内联。
1. 内联函数 (Inline Functions)
1.1 基本语法
inline 返回类型 函数名(参数列表) {// 函数体
}
inline
:关键字,用于声明内联函数。- 返回类型:函数的返回类型。
- 函数名:函数的名称。
- 参数列表:函数的参数列表。
- 函数体:函数的具体实现。
1.2 内联函数的特点
- 允许多个定义:内联函数可以在多个翻译单元中定义,只要每个定义都相同。这对于头文件中的函数特别有用,因为头文件可能会被多个源文件包含。
- 共享静态变量:所有内联函数定义中的静态局部变量在所有翻译单元中共享。也就是说,它们引用的是同一个对象。
- 默认参数:如果内联函数在不同的翻译单元中有不同的默认参数声明,程序将格式不正确,且不需要诊断信息。
- 隐式内联:类内部定义的成员函数、
constexpr
函数、consteval
函数、已删除的函数以及隐式生成的成员函数(如默认构造函数、析构函数等)都是隐式内联的。
1.3 历史背景
- C++98 及之前:
inline
的主要目的是提示编译器进行内联替换,以避免函数调用的开销。然而,编译器并不一定会遵循这个提示,而是根据实际情况决定是否进行内联。 - C++98 及之后:
inline
的语义发生了变化,主要作用是允许多个定义,而不是强制内联。编译器仍然可以根据需要选择是否进行内联替换。
1.4 示例
// 头文件 "example.h"
#ifndef EXAMPLE_H
#define EXAMPLE_Hinline int sum(int a, int b) {return a + b;
}#endif
// 源文件 #1
#include "example.h"int a() {return sum(1, 2);
}
// 源文件 #2
#include "example.h"int b() {return sum(3, 4);
}
- 在这个例子中,
sum
函数可以在多个源文件中使用,而不会违反 ODR,因为它是内联函数。
2. 内联变量 (Inline Variables) (自 C++17 起)
2.1 基本语法
inline 类型 变量名 = 初始化值;
inline
:关键字,用于声明内联变量。- 类型:变量的类型。
- 变量名:变量的名称。
- 初始化值:可选的初始化值。
2.2 内联变量的特点
- 允许多个定义:内联变量可以在多个翻译单元中定义,只要每个定义都相同。这对于头文件中的全局变量特别有用,因为头文件可能会被多个源文件包含。
- 外部链接:内联变量默认具有外部链接(
extern
),这意味着它们可以在不同翻译单元中共享同一个实例。 - 共享同一地址:所有翻译单元中的内联变量都引用同一个内存地址。
- 静态数据成员:类的静态数据成员可以声明为内联变量,这消除了将 C++ 代码打包为仅包含头文件的库的主要障碍。
2.3 示例
// 头文件 "example.h"
#ifndef EXAMPLE_H
#define EXAMPLE_H#include <atomic>// 内联变量
inline std::atomic<int> counter(0);#endif
// 源文件 #1
#include "example.h"int a() {++counter;return counter.load();
}
// 源文件 #2
#include "example.h"int b() {++counter;return counter.load();
}
- 在这个例子中,
counter
是一个内联变量,可以在多个源文件中使用,而不会违反 ODR。所有翻译单元中的counter
都引用同一个std::atomic<int>
实例。
3. 内联函数与内联变量的区别
- 内联函数:主要用于减少函数调用的开销,允许多个定义,所有定义必须相同。内联函数中的静态局部变量在所有翻译单元中共享。
- 内联变量:主要用于允许多个定义,所有定义必须相同。内联变量在所有翻译单元中共享同一个实例,并且默认具有外部链接。
4. 注意事项
- 定义一致性:如果内联函数或内联变量在不同的翻译单元中被不同地定义,程序将格式不正确,且不需要诊断信息。因此,确保所有定义完全一致非常重要。
- 不能重新声明为非内联:不能使用
inline
说明符重新声明在翻译单元中已定义为非内联的函数或变量。 - 块作用域限制:不能将
inline
说明符与块作用域(在另一个函数内部)的函数或变量声明一起使用。 - C 语言中的差异:在 C 语言中,内联函数的行为有所不同。C 允许在不同的翻译单元中定义不同的内联函数版本,但这会导致行为不确定。此外,C 不要求所有翻译单元中的内联函数定义相同。
5. 总结
inline
说明符 主要用于允许多个定义,而不是强制内联。- 内联函数 可以在多个翻译单元中定义,所有定义必须相同,静态局部变量在所有翻译单元中共享。
- 内联变量(自 C++17 起)可以在多个翻译单元中定义,所有定义必须相同,所有翻译单元中的内联变量引用同一个实例,默认具有外部链接。
inline
的历史演变:从最初的内联替换提示演变为允许多个定义的工具,特别是在头文件中定义函数和变量时非常有用。
通过合理使用 inline
说明符,开发者可以编写更灵活、模块化的代码,尤其是在处理跨多个翻译单元的函数和变量时。
const
和 volatile
类型限定符 (CV Qualifiers) in C++
const
和 volatile
是 C++ 中的类型限定符,用于指定对象的常量性和易变性。这些限定符可以应用于几乎任何类型的对象(除了函数类型和引用类型),并且可以组合使用以创建四种不同的 cv-限定版本:
- 非限定 (cv-unqualified):没有任何
const
或volatile
限定。 const
限定:对象是常量,不能被修改。volatile
限定:对象是易变的,表示其值可能会在程序控制之外发生变化。const volatile
限定:对象既是常量又是易变的。
1. const
限定符
1.1 基本语法
const 类型 变量名 = 初始值;
const
:关键字,表示该对象是常量,不能被修改。- 类型:变量的类型。
- 变量名:变量的名称。
- 初始值:可选的初始化值。
1.2 const
的特点
- 不可修改:
const
对象在其生命周期内不能被修改。尝试直接或间接修改const
对象会导致编译错误或未定义行为。 - 常量表达式:
const
对象可以在编译时进行优化,尤其是在与constexpr
结合使用时。 - 成员函数:类的成员函数可以声明为
const
,表示该函数不会修改类的成员变量(除非这些成员变量是mutable
的)。 - 指针和引用:可以有指向
const
对象的指针或引用,但不能通过它们修改对象。
1.3 示例
const int n = 10; // 常量整数
n = 20; // 错误:不能修改 const 对象const int* p = &n; // 指向 const 整数的指针
*p = 30; // 错误:不能通过指针修改 const 对象int* const q = &n; // 指针本身是 const,但指向的整数不是 const
*q = 40; // OK:可以通过指针修改整数
q = nullptr; // 错误:指针本身是 const,不能重新赋值
2. volatile
限定符
2.1 基本语法
volatile 类型 变量名 = 初始值;
volatile
:关键字,表示该对象是易变的,其值可能会在程序控制之外发生变化。- 类型:变量的类型。
- 变量名:变量的名称。
- 初始值:可选的初始化值。
2.2 volatile
的特点
- 可见副作用:每次访问
volatile
对象(读取或写入操作、成员函数调用等)都被视为优化目的的可见副作用。编译器不会优化掉这些访问,也不会重新排序这些访问与其他可见副作用之间的顺序。 - 硬件交互:
volatile
通常用于与硬件寄存器、信号处理程序或其他可能在程序控制之外更改的对象进行交互。 - 线程通信:虽然
volatile
不适用于多线程编程中的同步,但它可以用于与信号处理程序通信。
2.3 示例
volatile int flag = 0; // 易变整数flag = 1; // 编译器不会优化掉这行代码,即使看起来没有其他地方使用 flagif (flag == 1) {// 处理信号
}
3. const volatile
限定符
3.1 基本语法
const volatile 类型 变量名 = 初始值;
const volatile
:关键字组合,表示该对象既是常量又是易变的。- 类型:变量的类型。
- 变量名:变量的名称。
- 初始值:可选的初始化值。
3.2 const volatile
的特点
- 不可修改:
const
部分确保对象不能被修改。 - 可见副作用:
volatile
部分确保每次访问都被视为可见副作用,不会被优化掉。
3.3 示例
const volatile int sensor_value = 0; // 既是常量又是易变的整数// 不能修改 sensor_value
// 但每次访问都会被视为可见副作用
4. mutable
说明符
4.1 基本语法
class 类名 {mutable 类型 成员变量;
};
mutable
:关键字,允许修改声明为mutable
的类成员,即使包含对象声明为const
。- 类型:成员变量的类型。
- 成员变量:类的成员变量。
4.2 mutable
的特点
- 可变成员:
mutable
成员可以在const
成员函数中被修改,而不会违反const
约束。 - 不影响外部可见状态:
mutable
成员通常用于互斥量、备忘录缓存、惰性求值和访问检测等场景,这些成员的变化不影响类的外部可见状态。
4.3 示例
class ThreadsafeCounter {mutable std::mutex m; // 互斥量是可变的int data = 0;public:int get() const {std::lock_guard<std::mutex> lk(m); // 即使在 const 成员函数中也可以锁定互斥量return data;}void inc() {std::lock_guard<std::mutex> lk(m);++data;}
};
5. 转换规则
- 隐式转换:对
cv-限定
类型的引用和指针可以隐式转换为对更多cv-限定
类型的引用和指针。例如,int*
可以隐式转换为const int*
或volatile int*
,但不能隐式转换为int* const
。 const_cast
:要将对cv-限定
类型的引用或指针转换为对更少cv-限定
类型的引用或指针,必须使用const_cast
。然而,通过const_cast
修改const
对象会导致未定义行为。
5.1 示例
const int n = 10;
const int& r1 = n; // OK: 引用到 const 绑定到 const 对象// r1 = 2; // 错误:尝试通过引用到 const 修改对象const_cast<int&>(r1) = 2; // OK: 修改非 const 对象 nconst int& r2 = n; // 引用到 const 绑定到 const 对象// r2 = 2; // 错误:尝试通过引用到 const 修改对象// const_cast<int&>(r2) = 2; // 未定义行为:尝试修改 const 对象 n
6. 注意事项
const
和volatile
的组合:每个cv-限定符
在任何cv-限定符
序列中最多只能出现一次。例如,const const
和volatile const volatile
是无效的。const
和内部链接:在非局部、非易失性、非模板(自 C++14 起)、非内联(自 C++17 起)变量的声明中使用const
限定符,并且该变量未声明为extern
,则会赋予其内部链接。这与 C 语言不同,在 C 语言中,const
文件作用域变量具有外部链接。volatile
的某些用法已弃用:从 C++20 开始,volatile
的某些用法已被弃用,例如作为内置递增/递减运算符的操作数、作为内置直接赋值运算符的左操作数(除非在未求值上下文中或为弃值表达式)、作为函数参数类型或返回类型、以及在结构化绑定声明中使用volatile
限定符。
7. 总结
const
:用于声明常量对象,确保对象在生命周期内不可被修改。const
对象可以用于优化编译器的行为,并且可以在const
成员函数中保护类的成员变量不被修改。volatile
:用于声明易变对象,确保每次访问都被视为可见副作用,不会被优化掉。volatile
通常用于与硬件寄存器、信号处理程序或其他可能在程序控制之外更改的对象进行交互。const volatile
:同时具有const
和volatile
属性,表示对象既是常量又是易变的。mutable
:允许修改声明为mutable
的类成员,即使包含对象声明为const
。mutable
成员通常用于互斥量、备忘录缓存、惰性求值和访问检测等场景。
通过合理使用 const
和 volatile
限定符,开发者可以编写更安全、高效的代码,特别是在处理常量数据和硬件交互时。
constexpr
说明符 (自 C++11 起)
constexpr
是 C++ 中用于声明编译时常量表达式的说明符,它允许在编译时评估函数和变量的值。constexpr
的引入使得 C++ 程序可以在编译时执行更多的计算,从而提高运行时性能并减少运行时开销。
1. constexpr
变量
1.1 基本语法
constexpr 类型 变量名 = 初始化表达式;
constexpr
:关键字,表示该变量是一个编译时常量。- 类型:变量的类型,必须是字面量类型(LiteralType)。
- 变量名:变量的名称。
- 初始化表达式:必须是一个常量表达式,即在编译时可以完全求值的表达式。
1.2 要求
- 字面量类型:
constexpr
变量的类型必须是字面量类型(LiteralType),例如int
、double
、std::string_view
、枚举类型等。 - 立即初始化:
constexpr
变量必须在声明时立即初始化,并且初始化表达式必须是常量表达式。 - 常量析构:如果
constexpr
变量是类类型的对象,该类必须有constexpr
析构函数,并且对象的销毁过程也必须是常量表达式的一部分。 - 引用限制:
constexpr
变量不能引用翻译单元局部实体(如局部静态变量),除非是在模块接口单元的私有模块片段之外或模块分区中。
1.3 示例
constexpr int max_value = 100; // 编译时常量struct Point {constexpr Point(int x, int y) : x(x), y(y) {}int x, y;
};constexpr Point origin{0, 0}; // 编译时常量对象
2. constexpr
函数
2.1 基本语法
constexpr 返回类型 函数名(参数列表) {// 函数体
}
constexpr
:关键字,表示该函数可以在编译时被调用。- 返回类型:函数的返回类型,必须是字面量类型。
- 函数名:函数的名称。
- 参数列表:函数的参数列表,所有参数类型也必须是字面量类型。
2.2 要求
- 非虚函数:
constexpr
函数不能是虚函数。 - 非协程:
constexpr
函数不能是协程(coroutine)。 - 非
function try
块:constexpr
函数不能是function try
块。 - 无基类为虚:对于构造函数和析构函数,类不能有虚基类。
- 返回值和参数:函数的返回值和所有参数必须是字面量类型。
- 核心常量表达式:至少存在一组参数值,使得函数的调用可以是核心常量表达式的一部分。
- 函数体限制:
- C++14 及之前:函数体只能包含一个
return
语句,且不能包含goto
、try-catch
、asm
等语句。 - C++17 及之后:放宽了对函数体的限制,允许使用局部变量、循环、条件语句等,但仍然不能抛出异常或执行汇编代码。
- C++23:进一步放宽了限制,允许使用非字面量类型的变量、标签和
goto
语句等。
- C++14 及之前:函数体只能包含一个
2.3 示例
// C++11: 使用递归计算阶乘
constexpr int factorial(int n) {return n <= 1 ? 1 : n * factorial(n - 1);
}// C++14: 使用循环计算阶乘
constexpr int factorial_cxx14(int n) {int result = 1;while (n > 1) {result *= n--;}return result;
}// C++23: 允许更复杂的表达式
constexpr void g(int& i) {f(i); // 即使 f 不是 constexpr,g 仍然是 constexpr
}
3. constexpr
构造函数
3.1 基本语法
struct 类名 {constexpr 类名(参数列表) {// 构造函数体}
};
constexpr
:关键字,表示该构造函数可以在编译时被调用。- 类名:类的名称。
- 参数列表:构造函数的参数列表。
- 构造函数体:构造函数的具体实现。
3.2 要求
- 成员初始化:对于类或结构体的构造函数,每个基类子对象和每个非变体非静态数据成员必须被初始化。对于联合体的构造函数,必须恰好初始化一个非静态数据成员。
constexpr
构造函数:用于初始化非静态数据成员和基类的构造函数也必须是constexpr
构造函数。- 平凡析构函数:类的析构函数必须是平凡的(即没有用户定义的析构函数或虚函数),或者是一个
constexpr
析构函数。
3.3 示例
struct Point {constexpr Point(int x, int y) : x(x), y(y) {}int x, y;
};constexpr Point origin{0, 0}; // 编译时常量对象
4. constexpr
析构函数
4.1 基本语法
struct 类名 {constexpr ~类名() {// 析构函数体}
};
constexpr
:关键字,表示该析构函数可以在编译时被调用。- 类名:类的名称。
- 析构函数体:析构函数的具体实现。
4.2 要求
constexpr
析构函数:用于销毁非静态数据成员和基类的析构函数也必须是constexpr
析构函数。- 平凡析构函数:类的析构函数可以隐式地调用在常量表达式中,即使它的析构函数不是平凡的。
4.3 示例
struct Point {constexpr Point(int x, int y) : x(x), y(y) {}constexpr ~Point() = default; // 默认析构函数是 constexprint x, y;
};constexpr Point p{1, 2}; // 编译时常量对象
5. constexpr
模板和特化
对于 constexpr
函数模板和类模板的 constexpr
成员函数,至少一个特化必须满足上述要求。其他特化仍然被认为是 constexpr
,即使对这样一个函数的调用不能出现在常量表达式中。
5.1 示例
template <typename T>
constexpr T add(T a, T b) {return a + b;
}constexpr int sum = add(1, 2); // 编译时常量
6. constexpr
和 consteval
consteval
:自 C++20 引入,表示该函数必须在编译时被调用,不能在运行时调用。与constexpr
不同,consteval
函数不能以非常量方式使用。constexpr
:表示该函数可以在编译时或运行时调用,具体取决于调用上下文。
6.1 示例
consteval int always_compile_time(int x) {return x * 2;
}constexpr int maybe_compile_time(int x) {return x * 2;
}constexpr int a = always_compile_time(5); // 必须在编译时计算
int b = maybe_compile_time(5); // 可以在编译时或运行时计算
7. 注意事项
constexpr
和const
:constexpr
变量隐式具有const
属性,但const
变量不一定能在编译时求值。constexpr
变量必须在编译时初始化,并且其初始化表达式必须是常量表达式。constexpr
和inline
:constexpr
函数隐式具有inline
属性,这意味着它们可以在多个翻译单元中定义而不会违反一次定义规则(ODR)。constexpr
和noexcept
:noexcept
运算符可以用于检查constexpr
函数的特定调用是否是常量表达式的一部分。如果函数的调用在编译时无法求值,则noexcept
将返回false
。
8. 总结
constexpr
:用于声明编译时常量表达式,允许在编译时评估函数和变量的值。constexpr
变量必须是字面量类型,并且必须在声明时立即初始化。constexpr
函数必须满足一定的限制,但随着 C++ 标准的演进,这些限制逐渐放宽。constexpr
构造函数:允许类对象在编译时构造,前提是所有成员和基类的构造函数也是constexpr
的。constexpr
析构函数:允许类对象在编译时销毁,前提是析构函数是平凡的或constexpr
的。consteval
:自 C++20 引入,表示函数必须在编译时调用,不能在运行时调用。
通过合理使用 constexpr
,开发者可以编写更高效的代码,减少运行时开销,并在编译时进行更多的计算。这不仅提高了程序的性能,还增强了代码的安全性和可维护性。
consteval
规范 (自 C++20 起)
consteval
是 C++20 引入的一种新的函数说明符,用于声明立即函数(immediate function)。与 constexpr
不同,consteval
函数必须在编译时求值,不能在运行时调用。这意味着每次对 consteval
函数的调用都必须生成一个编译时常量表达式。
1. 基本语法
consteval 返回类型 函数名(参数列表) {// 函数体
}
consteval
:关键字,表示该函数是一个立即函数,必须在编译时求值。- 返回类型:函数的返回类型,必须是字面量类型(LiteralType)。
- 函数名:函数的名称。
- 参数列表:函数的参数列表,所有参数类型也必须是字面量类型。
2. consteval
的特点
- 编译时求值:
consteval
函数必须在编译时求值,不能在运行时调用。如果尝试在运行时调用consteval
函数,会导致编译错误。 - 隐式
inline
:与constexpr
类似,consteval
函数隐式具有inline
属性,可以在多个翻译单元中定义而不会违反一次定义规则(ODR)。 - 不能应用于某些特殊函数:
consteval
不能应用于析构函数、分配函数或释放函数。 - 不能与
constexpr
同时使用:consteval
和constexpr
不能同时应用于同一个函数或函数模板。如果一个函数被声明为consteval
,那么它的所有重新声明也必须是consteval
。 - 立即调用:对
consteval
函数的潜在求值调用(即直接或间接调用)必须生成常量表达式。这种调用称为立即调用。如果调用上下文不是立即函数的函数参数作用域或consteval if
语句的真分支(自 C++23 起),则调用必须生成常量表达式。 - 标识符限制:表示立即函数的标识符表达式只能出现在立即调用的子表达式中,或出现在立即函数上下文中。可以获取指向立即函数的指针或引用,但它们不能逃离常量表达式求值。
3. 示例
3.1 简单的 consteval
函数
consteval int sqr(int n) {return n * n;
}constexpr int r = sqr(100); // OK: 编译时常量表达式int x = 100;
int r2 = sqr(x); // Error: 运行时调用不生成常量表达式
3.2 嵌套调用
consteval int sqrsqr(int n) {return sqr(sqr(n)); // OK: 虽然此时不是常量表达式,但在调用上下文中是
}constexpr int dblsqr(int n) {return 2 * sqr(n); // Error: 外部函数不是 consteval,且 sqr(n) 不是常量
}
3.3 立即函数的标识符限制
consteval int f() { return 42; }consteval auto g() { return &f; } // OK: 获取立即函数的指针consteval int h(int (*p)() = g()) { return p(); } // OK: 使用立即函数的指针constexpr int r = h(); // OK: 在常量表达式中调用constexpr auto e = g(); // Error: 指向立即函数的指针不能作为常量表达式的返回值
3.4 结合 static_assert
consteval unsigned factorial(unsigned n) {return n < 2 ? 1 : n * factorial(n - 1);
}consteval unsigned combination(unsigned m, unsigned n) {return factorial(n) / factorial(m) / factorial(n - m);
}static_assert(factorial(6) == 720); // OK: 编译时常量表达式
static_assert(combination(4, 8) == 70); // OK: 编译时常量表达式int main(int argc, const char*[]) {constexpr unsigned x = factorial(4); // OK: 编译时常量表达式std::cout << x << '\n';[[maybe_unused]]unsigned y = factorial(argc); // OK: 运行时调用// unsigned z = combination(argc, 7); // Error: 'argc' 不是常量表达式
}
4. 注意事项
-
consteval
和constexpr
的区别:consteval
函数必须在编译时求值,不能在运行时调用。constexpr
函数可以在编译时或运行时求值,具体取决于调用上下文。consteval
函数不能与constexpr
同时使用,因为它们的行为不同。
-
consteval
和inline
:consteval
函数隐式具有inline
属性,因此可以在多个翻译单元中定义而不会违反 ODR。
-
consteval
和析构函数:consteval
不能应用于析构函数、分配函数或释放函数。
-
立即调用的上下文:
- 对
consteval
函数的调用必须在立即函数上下文中进行,或者在常量表达式中进行。如果调用上下文不是立即函数的函数参数作用域或consteval if
语句的真分支(自 C++23 起),则调用必须生成常量表达式。
- 对
-
标识符表达式的限制:
- 表示立即函数的标识符表达式只能出现在立即调用的子表达式中,或出现在立即函数上下文中。可以获取指向立即函数的指针或引用,但它们不能逃离常量表达式求值。
5. 总结
consteval
:用于声明立即函数,确保函数在编译时求值,不能在运行时调用。它适用于需要在编译时计算的场景,例如静态断言、模板元编程等。consteval
和constexpr
的区别:consteval
函数必须在编译时求值,而constexpr
函数可以在编译时或运行时求值。consteval
的应用场景:consteval
适合用于那些必须在编译时完成计算的函数,例如生成编译时常量、执行复杂的编译时计算等。
通过合理使用 consteval
,开发者可以编写更高效的代码,减少运行时开销,并确保某些计算在编译时完成。这不仅提高了程序的性能,还增强了代码的安全性和可维护性。
constinit
说明符 (自 C++20 起)
constinit
是 C++20 引入的一个说明符,用于断言变量具有静态初始化,即零初始化和常量初始化。如果变量用 constinit
声明,则其初始化声明必须使用 constinit
应用。如果用 constinit
声明的变量具有动态初始化(即使它作为静态初始化执行),则程序格式错误。
1. 基本语法
constinit 类型 变量名 = 初始化表达式;
constinit
:关键字,表示该变量必须具有静态初始化。- 类型:变量的类型。
- 变量名:变量的名称。
- 初始化表达式:变量的初始化表达式,必须是常量表达式或零初始化。
2. constinit
的特点
- 静态初始化:
constinit
确保变量在编译时进行静态初始化,即零初始化和常量初始化。如果变量的初始化涉及到动态初始化(例如,调用构造函数或执行复杂的初始化逻辑),则程序格式错误。 - 不能与
constexpr
结合使用:constinit
不能与constexpr
同时使用。constexpr
变量不仅要求静态初始化,还要求常量析构和const
限定。而constinit
只要求静态初始化,不要求常量析构和const
限定。 - 适用于静态或线程存储持续时间的变量:
constinit
只能用于具有静态或线程存储持续时间的变量。对于局部变量或自动存储持续时间的变量,constinit
无效。 - 引用变量:当声明的变量为引用时,
constinit
等效于constexpr
。 - 减少线程局部变量的开销:
constinit
可以在非初始化声明中使用,告诉编译器thread_local
变量已初始化,从而减少隐藏保护变量产生的开销。
3. 示例
3.1 静态初始化
const char* g() { return "dynamic initialization"; }
constexpr const char* f(bool p) { return p ? "constant initializer" : g(); }constinit const char* c = f(true); // OK: 静态初始化
// constinit const char* d = f(false); // Error: 动态初始化
3.2 constinit
和 constexpr
的区别
std::shared_ptr<int> make_shared() {return std::make_shared<int>(42);
}constinit std::shared_ptr<int> p = make_shared(); // OK: 静态初始化,但没有常量析构
// constexpr std::shared_ptr<int> q = make_shared(); // Error: 需要常量析构
3.3 线程局部变量
extern thread_local constinit int x;int f() { return x; } // No check of a guard variable needed
4. 注意事项
-
constinit
和constexpr
的区别:constinit
只要求静态初始化,不要求常量析构和const
限定。constexpr
变量不仅要求静态初始化,还要求常量析构和const
限定。
-
constinit
和thread_local
:constinit
可以用于thread_local
变量,确保这些变量在编译时初始化,从而减少运行时的开销。
-
constinit
和动态初始化:- 如果用
constinit
声明的变量具有动态初始化(即使它作为静态初始化执行),则程序格式错误。编译器会报错,指出该变量不能进行动态初始化。
- 如果用
5. 总结
constinit
:用于确保变量在编译时进行静态初始化,适用于静态或线程存储持续时间的变量。它不要求常量析构和const
限定,因此可以用于更广泛的场景,例如std::shared_ptr
等类型的对象。constinit
和constexpr
的区别:constinit
只要求静态初始化,而constexpr
还要求常量析构和const
限定。constinit
的应用场景:constinit
适合用于那些需要在编译时初始化的变量,特别是那些不能使用constexpr
的场景,例如std::shared_ptr
或其他没有constexpr
析构函数的类型。
通过合理使用 constinit
,开发者可以确保变量在编译时初始化,减少运行时的开销,并提高程序的性能和安全性。
decltype
说明符 (自 C++11 起)
decltype
是 C++11 引入的一个关键字,用于检查实体的声明类型或表达式的类型和值类别。它在模板编程、泛型编程以及处理复杂类型时非常有用,尤其是在需要推导表达式类型的情况下。
1. 基本语法
decltype
有两种主要用法:
decltype(实体)
:用于获取未加括号的标识符表达式或类成员访问表达式的声明类型。decltype(表达式)
:用于获取任意表达式的类型,并根据表达式的值类别(左值、右值、纯右值)调整返回类型。
2. decltype(实体)
的行为
- 未加括号的标识符表达式:如果参数是一个未加括号的标识符表达式或类成员访问表达式,
decltype
生成该表达式命名的实体的类型。- 如果实体是结构化绑定(自 C++17 起),
decltype
生成引用类型。 - 如果实体是非类型模板参数(自 C++20 起),
decltype
生成模板参数的类型,即使实体是模板参数对象(常量对象),类型也是非常量。
- 如果实体是结构化绑定(自 C++17 起),
示例
struct A { double x; };
const A* a;decltype(a->x) y; // type of y is double (declared type)
decltype((a->x)) z = y; // type of z is const double& (lvalue expression)
decltype(a->x)
:a->x
是一个未加括号的类成员访问表达式,因此decltype(a->x)
返回double
,即成员x
的类型。decltype((a->x))
:由于a->x
被括号包围,它被视为一个普通的左值表达式,因此decltype((a->x))
返回const double&
,即左值引用类型。
3. decltype(表达式)
的行为
对于任意表达式,decltype
根据表达式的值类别(左值、右值、纯右值)调整返回类型:
- 左值:如果表达式的值类别为左值,则
decltype
生成T&
,其中T
是表达式的类型。 - 右值:如果表达式的值类别为右值,则
decltype
生成T&&
,其中T
是表达式的类型。 - 纯右值:如果表达式的值类别为纯右值,则
decltype
生成T
,其中T
是表达式的类型。
示例
int i = 33;
decltype(i) j = i * 2; // type of j is int (declared type)
decltype((i)) k = i; // type of k is int& (lvalue expression)static_assert(std::is_same_v<decltype(i), decltype(j)>);
static_assert(std::is_same_v<decltype((i)), int&>);
decltype(i)
:i
是一个未加括号的标识符表达式,因此decltype(i)
返回int
。decltype((i))
:由于i
被括号包围,它被视为一个普通的左值表达式,因此decltype((i))
返回int&
。
4. 特殊情况
- 函数调用和逗号表达式:如果表达式是一个返回类类型的纯右值的函数调用,或者是一个逗号表达式,其右操作数是此类函数调用,则不会为该纯右值引入临时对象。这意味着类型不需要完整或具有可用的析构函数,并且可以是抽象的。
示例
struct B {B() = default;B(const B&) = delete; // 禁用拷贝构造函数
};B f() { return B(); }decltype(f()) b; // OK: 不会创建临时对象
decltype(f())
:f()
是一个返回类类型B
的纯右值的函数调用,因此decltype(f())
返回B
,而不是B&&
。由于不会创建临时对象,B
的拷贝构造函数不需要是可用的。
5. decltype(auto)
自 C++14 起,C++ 引入了 decltype(auto)
,它结合了 auto
和 decltype
的功能。decltype(auto)
会根据初始化表达式的值类别推导出正确的类型,类似于 decltype
的行为。
示例
const int& getRef(const int* p) { return *p; }// 使用 auto 返回类型
auto getRefFwdBad(const int* p) { return getRef(p); }
static_assert(std::is_same_v<decltype(getRefFwdBad), int(const int*)>,"Just returning auto isn't perfect forwarding.");// 使用 decltype(auto) 返回类型
decltype(auto) getRefFwdGood(const int* p) { return getRef(p); }
static_assert(std::is_same_v<decltype(getRefFwdGood), const int&(const int*)>,"Returning decltype(auto) perfectly forwards the return type.");// 使用 decltype(return expression) 返回类型
auto getRefFwdGood1(const int* p) -> decltype(getRef(p)) { return getRef(p); }
static_assert(std::is_same_v<decltype(getRefFwdGood1), const int&(const int*)>,"Returning decltype(return expression) also perfectly forwards the return type.");
auto getRefFwdBad
:使用auto
返回类型时,返回类型会被推导为int
,而不是const int&
,导致完美转发失败。decltype(auto) getRefFwdGood
:使用decltype(auto)
返回类型时,返回类型会被正确推导为const int&
,实现了完美转发。auto getRefFwdGood1 -> decltype(getRef(p))
:使用decltype(return expression)
返回类型时,返回类型也会被正确推导为const int&
,同样实现了完美转发。
6. decltype
在模板中的应用
decltype
在模板中非常有用,特别是在需要根据模板参数推导返回类型时。它可以用于返回依赖于模板参数的类型,而无需显式指定类型。
示例
template<typename T, typename U>
auto add(T t, U u) -> decltype(t + u) {return t + u;
}
decltype(t + u)
:t + u
的类型取决于模板参数T
和U
,decltype
会根据t + u
的类型推导出返回类型。- C++14 及之后:从 C++14 开始,返回类型可以自动推导,因此可以简化为
auto add(T t, U u) { return t + u; }
,编译器会自动推导返回类型。
7. decltype
与 lambda 表达式
decltype
可以用于获取 lambda 表达式的类型。每个 lambda 表达式的类型都是唯一的且未命名的,因此 decltype
对于比较不同 lambda 表达式的类型非常有用。
示例
auto f = [i](int av, int bv) -> int { return av * bv + i; };
auto h = [i](int av, int bv) -> int { return av * bv + i; };static_assert(!std::is_same_v<decltype(f), decltype(h)>,"The type of a lambda function is unique and unnamed");decltype(f) g = f; // 复制 lambda 对象
std::cout << f(3, 3) << ' ' << g(3, 3) << '\n';
decltype(f)
:f
是一个 lambda 表达式,decltype(f)
返回该 lambda 表达式的类型。decltype(f) g = f
:将f
复制到g
,g
的类型与f
相同。
8. 总结
decltype
:用于检查实体的声明类型或表达式的类型和值类别。它可以根据表达式的值类别(左值、右值、纯右值)调整返回类型。decltype(实体)
:用于获取未加括号的标识符表达式或类成员访问表达式的声明类型。decltype(表达式)
:用于获取任意表达式的类型,并根据表达式的值类别调整返回类型。decltype(auto)
:结合了auto
和decltype
的功能,用于实现完美转发。decltype
在模板中的应用:decltype
在模板中非常有用,特别是在需要根据模板参数推导返回类型时。decltype
与 lambda 表达式:decltype
可以用于获取 lambda 表达式的类型,每个 lambda 表达式的类型都是唯一的且未命名的。
通过合理使用 decltype
,开发者可以编写更灵活、更通用的代码,特别是在处理复杂类型和模板编程时。decltype
使得类型推导更加方便,减少了手动指定类型的繁琐工作。
占位符类型说明符 (自 C++11 起)
占位符类型说明符(Placeholder Type Specifier)是 C++11 引入的一个特性,允许开发者在声明中使用 auto
或 decltype(auto)
作为类型的占位符。编译器会根据初始化表达式或函数返回值自动推导出实际的类型。这一特性大大简化了代码编写,特别是在处理复杂类型或模板编程时。
1. 基本语法
占位符类型说明符有两种主要形式:
auto
:用于从初始化表达式推导类型。decltype(auto)
:用于从初始化表达式推导类型,并保留表达式的引用和常量限定符。
此外,自 C++20 起,还可以使用带有类型约束的占位符类型说明符,结合概念(concepts)进行更严格的类型检查。
语法示例
type-constraint (可选) auto; // (1)
type-constraint (可选) decltype(auto); // (2) (自 C++14 起)
type-constraint; // (3) (自 C++20 起) 一个概念名称,可选限定,可选后跟用 <> 括起来的模板参数列表
type-constraint
:自 C++20 起引入的概念(concept),用于对推导出的类型进行约束。它可以在auto
或decltype(auto)
之前使用,确保推导出的类型满足某些条件。
2. auto
的行为
auto
是最常用的占位符类型说明符,编译器会根据初始化表达式推导出实际的类型。auto
可以伴随修饰符(如 const
、volatile
、&
、&&
等),这些修饰符会参与类型推导。
示例
auto x = 5; // type of x is int
const auto *v = &x; // type of v is const int*
static auto y = 0.0; // type of y is double
auto x = 5;
:x
的类型被推导为int
。const auto *v = &x;
:v
的类型被推导为const int*
,即指向const int
的指针。static auto y = 0.0;
:y
的类型被推导为double
,并且具有静态存储持续时间。
3. decltype(auto)
的行为
decltype(auto)
是自 C++14 引入的占位符类型说明符,它的行为类似于 decltype
,但更方便。decltype(auto)
会根据初始化表达式推导出类型,并保留表达式的引用和常量限定符。
示例
int a = 5;
decltype(auto) c1 = a; // type of c1 is int, holding a copy of a
decltype(auto) c2 = (a); // type of c2 is int&, an alias of a
decltype(auto) c1 = a;
:c1
的类型被推导为int
,因为a
是一个未加括号的标识符表达式。decltype(auto) c2 = (a);
:c2
的类型被推导为int&
,因为(a)
是一个左值表达式。
4. 带类型约束的 auto
和 decltype(auto)
(自 C++20 起)
自 C++20 起,可以使用带有类型约束的 auto
和 decltype(auto)
,结合概念(concepts)进行更严格的类型检查。类型约束可以是一个概念名称,后面可以跟随模板参数列表。
示例
template<typename T>
concept Integral = std::is_integral_v<T>;Integral auto x = 5; // OK: 5 是整数类型
// Integral auto y = 3.14; // Error: 3.14 不是整数类型template<auto n> // C++17 auto parameter declaration
auto f() -> std::pair<decltype(n), decltype(n)> {return {n, n};
}
Integral auto x = 5;
:x
的类型被推导为int
,并且满足Integral
概念。Integral auto y = 3.14;
:编译错误,因为3.14
不是整数类型,不满足Integral
概念。
5. 占位符类型说明符的应用场景
占位符类型说明符可以在多种上下文中使用,包括但不限于以下几种:
- 变量声明:用于从初始化表达式推导变量类型。
- 函数声明:用于从函数返回值推导返回类型。
- lambda 表达式:用于声明泛型 lambda。
- 非类型模板参数:用于从模板参数推导类型。
- 结构化绑定声明:用于从初始化表达式推导多个变量的类型。
- new 表达式:用于从初始化表达式推导动态分配的对象类型。
- 函数式转换:用于从初始化表达式推导转换类型。
示例
#include <iostream>
#include <utility>// 函数声明中的 auto
template<class T, class U>
auto add(T t, U u) { return t + u; } // 返回类型由 t + u 推导// perfect forwarding 中的 decltype(auto)
template<class F, class... Args>
decltype(auto) PerfectForward(F fun, Args&&... args) {return fun(std::forward<Args>(args)...);
}// 非类型模板参数中的 auto (C++17)
template<auto n>
auto f() -> std::pair<decltype(n), decltype(n)> {return {n, n};
}int main() {auto a = 1 + 2; // type of a is intauto b = add(1, 1.2); // type of b is doublestatic_assert(std::is_same_v<decltype(a), int>);static_assert(std::is_same_v<decltype(b), double>);auto c0 = a; // type of c0 is int, holding a copy of adecltype(auto) c1 = a; // type of c1 is int, holding a copy of adecltype(auto) c2 = (a); // type of c2 is int&, an alias of astd::cout << "before modification through c2, a = " << a << '\n';++c2;std::cout << " after modification through c2, a = " << a << '\n';auto [v, w] = f<0>(); // structured binding declarationauto d = {1, 2}; // type of d is std::initializer_list<int>auto n = {5}; // type of n is std::initializer_list<int>auto m{5}; // type of m is int (C++11 DR n3922)// auto is commonly used for unnamed types such as the types of lambda expressionsauto lambda = [](int x) { return x + 3; };[](...){}(c0, c1, v, w, d, n, m, lambda); // suppresses "unused variable" warnings
}
6. 注意事项
-
auto
和decltype(auto)
的区别:auto
会根据初始化表达式推导类型,但不会保留表达式的引用和常量限定符。decltype(auto)
会根据初始化表达式推导类型,并保留表达式的引用和常量限定符。
-
auto
和std::initializer_list
:- 使用
{}
初始化auto
时,如果没有显式指定类型,默认情况下auto
会被推导为std::initializer_list
。 - 例如,
auto d = {1, 2};
会将d
推导为std::initializer_list<int>
。 - 如果希望
auto
推导为普通类型而不是std::initializer_list
,可以使用auto m{5};
,这会将m
推导为int
(自 C++11 DR n3922 起)。
- 使用
-
auto
和decltype(auto)
的限制:auto
不能用于声明没有初始化器的变量。decltype(auto)
必须是声明类型的唯一组成部分,不能与其他类型说明符组合使用。
-
auto
和decltype(auto)
在多变量声明中的限制:- 如果在一个声明中声明多个变量,并且使用
auto
或decltype(auto)
,则所有变量的类型必须相同。 - 例如,
auto a = 5, b = {1, 2};
会导致编译错误,因为a
和b
的类型不同。
- 如果在一个声明中声明多个变量,并且使用
-
auto
和decltype(auto)
在函数声明中的应用:- 自 C++14 起,
auto
可以用于函数的返回类型推导。 - 自 C++20 起,
auto
可以用于函数参数的类型推导,声明简写函数模板。
- 自 C++14 起,
7. 总结
auto
:用于从初始化表达式推导类型,可以伴随修饰符(如const
、&
、&&
等)。auto
不能用于声明没有初始化器的变量。decltype(auto)
:用于从初始化表达式推导类型,并保留表达式的引用和常量限定符。decltype(auto)
必须是声明类型的唯一组成部分。- 带类型约束的
auto
和decltype(auto)
:自 C++20 起,可以使用带有类型约束的auto
和decltype(auto)
,结合概念(concepts)进行更严格的类型检查。 - 应用场景:占位符类型说明符广泛应用于变量声明、函数声明、lambda 表达式、非类型模板参数、结构化绑定声明、new 表达式和函数式转换中。
通过合理使用 auto
和 decltype(auto)
,开发者可以编写更简洁、更灵活的代码,特别是在处理复杂类型和模板编程时。占位符类型说明符使得类型推导更加方便,减少了手动指定类型的繁琐工作。
typedef
说明符
typedef
是 C++ 中用于创建类型别名的关键字。它允许开发者为现有类型定义一个更简洁或更具描述性的名称,从而提高代码的可读性和可维护性。typedef
本质上是为复杂类型提供一个别名,并不会创建新的类型。
1. 基本语法
typedef
说明符通常出现在声明的开头,但它也可以出现在类型说明符之间。typedef
不能与除类型说明符以外的任何其他说明符组合使用。
typedef 原始类型 别名;
原始类型
:现有的类型,可以是内置类型、用户定义类型(如类、结构体、枚举等),也可以是指针、引用、数组、函数类型等。别名
:为原始类型定义的新名称。
2. typedef
的行为
typedef
名称是现有类型的别名:typedef
不会创建新的类型,只是为现有类型提供一个别名。因此,typedef
名称和原始类型在语义上是完全等价的。typedef
名称只能重新声明为相同的类型:一旦定义了一个typedef
名称,它只能被重新声明为引用相同的类型。不能用typedef
改变现有类型名称的含义。typedef
名称的作用域:typedef
名称仅在其可见的作用域内有效。不同的函数或类可以定义同名但含义不同的typedef
名称。
3. typedef
的应用场景
typedef
可以用于简化复杂的类型声明,尤其是在指针、数组、函数类型、类类型等情况下。以下是一些常见的应用场景:
-
简化指针类型声明:
typedef int* IntPtr; // IntPtr 是 int* 的别名 IntPtr p1, p2; // 等价于 int* p1, *p2;
-
简化数组类型声明:
typedef int IntArray[10]; // IntArray 是 int[10] 的别名 IntArray arr1, arr2; // 等价于 int arr1[10], arr2[10];
-
简化函数类型声明:
typedef int (*FuncPtr)(int, double); // FuncPtr 是指向 int (int, double) 函数的指针 FuncPtr fp; // 等价于 int (*fp)(int, double);
-
简化结构体类型声明:
typedef struct {int a;int b; } S, *pS; // S 是 struct 的别名,pS 是指向 struct 的指针 S s; // 等价于 struct { int a; int b; } s; pS ps; // 等价于 struct { int a; int b; }* ps;
-
简化嵌套类型声明:
typedef struct Node {int data;struct Node* next; } Node; // Node 是 struct Node 的别名
-
结合模板使用:
template <typename T> struct add_const {typedef const T type; // type 是 const T 的别名 };
4. 注意事项
-
typedef
不能用于函数参数或返回类型声明:typedef
不能出现在函数参数的声明中,也不能出现在函数定义的decl-specifier-seq
中。- 例如,以下代码是非法的:
void f1(typedef int param); // 错误:typedef 不能用于函数参数 typedef int f2() {} // 错误:typedef 不能用于函数返回类型
-
typedef
不能用于无声明符的声明:typedef
不能出现在不包含声明符的声明中。例如,以下代码是非法的:typedef struct X {}; // 错误:typedef 不能用于无声明符的声明
-
typedef
和存储类说明符:typedef
不能与存储类说明符(如static
、extern
、register
等)组合使用。- 例如,以下代码是非法的:
typedef static unsigned int uint; // 错误:typedef 不能与存储类说明符组合
-
typedef
和匿名结构体/枚举:- 如果
typedef
声明定义了未命名的类或枚举,则该声明声明的类类型或枚举类型的第一个typedef
名称是该类型的“用于链接目的的typedef
名称”。 - 例如:
typedef struct { int a; int b; } S; // S 是用于链接目的的 typedef 名称
- 自 C++20 起,以这种方式定义的无名类应该只包含与 C 兼容的结构,不能声明成员函数、基类、默认成员初始化器或 lambda 表达式。
- 如果
-
typedef
和常量限定符:typedef
名称可以与常量限定符(如const
、volatile
)组合使用,但需要注意它们的作用范围。- 例如:
typedef int* IntPtr; // IntPtr 是 int* 的别名 const IntPtr p1 = nullptr; // 等价于 int* const p1 = nullptr; const int* p2 = nullptr; // 等价于 const int* p2 = nullptr;
5. typedef
与类型别名模板(C++11 起)
自 C++11 起,C++ 引入了类型别名模板(Type Alias Template),提供了与 typedef
类似的功能,但使用了不同的语法。类型别名模板可以用于模板名称,而 typedef
不能直接用于模板。
示例
// 使用 typedef 创建类型别名
template <typename T>
struct add_const {typedef const T type; // type 是 const T 的别名
};// 使用类型别名模板创建类型别名
template <typename T>
using AddConst = const T;int main() {add_const<int>::type x = 5; // x 是 const intAddConst<int> y = 10; // y 也是 const int
}
typedef
:使用typedef
创建类型别名时,必须通过结构体或类来定义。using
:使用using
创建类型别名时,语法更加简洁,可以直接用于模板。
6. 示例
#include <iostream>// 简单的 typedef
typedef unsigned long ulong;// 两个对象具有相同的类型
unsigned long l1;
ulong l2;// 更复杂的 typedef
typedef int int_t, *intp_t, (&fp)(int, ulong), arr_t[10];// 两个对象具有相同的类型
int a1[10];
arr_t a2;// 注意:两个对象的类型不同
const intp_t p1 = nullptr; // int *const p1 = nullptr
const int* p2 = nullptr; // const int* p2 = nullptr// 常见的 C 风格 idiom,避免写 "struct S"
typedef struct {int a;int b;
} S, *pS;// 两个对象具有相同的类型
pS ps1;
S* ps2;// 错误:存储类说明符不能出现在 typedef 声明中
// typedef static unsigned int uint;// typedef 可以出现在 decl-specifier-seq 的任何位置
long unsigned typedef int long ullong;
// 更常规的写法是 "typedef unsigned long long int ullong;"// std::add_const,像许多其他元函数一样,使用成员 typedef
template <typename T>
struct add_const {typedef const T type;
};// 错误:结构体的 typedef 名称与之前声明的结构体名称冲突
typedef struct {struct listNode* next; // 声明一个新的(不完整)结构体类型 listNode
} listNode; // 错误:与之前声明的 struct listNode 冲突// C++20 错误:带有 typedef 名称的结构体不能有成员函数
typedef struct {void f() {} // 错误:C++20 不允许带有成员函数的无名结构体
} C_Incompatible;int main() {std::cout << "l1: " << l1 << ", l2: " << l2 << std::endl;std::cout << "a1[0]: " << a1[0] << ", a2[0]: " << a2[0] << std::endl;std::cout << "p1: " << p1 << ", p2: " << p2 << std::endl;std::cout << "ps1: " << ps1 << ", ps2: " << ps2 << std::endl;
}
7. 总结
typedef
:用于为现有类型创建别名,简化复杂的类型声明。typedef
名称是现有类型的同义词,而不是新类型的声明。typedef
的限制:typedef
不能用于函数参数、返回类型声明或无声明符的声明。typedef
不能与存储类说明符组合使用。typedef
和匿名结构体/枚举:typedef
可以为匿名结构体或枚举创建类型别名,但在 C++20 中,带有typedef
名称的无名结构体不能包含成员函数、基类、默认成员初始化器或 lambda 表达式。typedef
与类型别名模板:自 C++11 起,using
提供了更简洁的类型别名声明方式,特别适用于模板。
通过合理使用 typedef
,开发者可以编写更简洁、更具可读性的代码,特别是在处理复杂类型时。typedef
是 C++ 中非常有用的一个特性,能够显著提高代码的可维护性和表达力。
类型别名与别名模板 (自 C++11 起)
C++11 引入了类型别名(Type Alias)和别名模板(Alias Template),它们提供了比传统的 typedef
更灵活、更简洁的语法来创建类型别名。类型别名和别名模板不仅简化了复杂的类型声明,还增强了代码的可读性和可维护性。
1. 类型别名 (using
)
类型别名使用 using
关键字来为现有类型创建一个同义词。它与 typedef
的功能相同,但语法更加直观和易读。类型别名可以出现在块作用域、类作用域或命名空间作用域中。
语法
using 标识符 = 类型标识;
标识符
:为现有类型定义的新名称。类型标识
:现有的类型,可以是内置类型、用户定义类型(如类、结构体、枚举等),也可以是指针、引用、数组、函数类型等。
示例
#include <iostream>
#include <string>// 简单的类型别名
using flags = std::ios_base::fmtflags; // flags 是 std::ios_base::fmtflags 的别名
flags fl = std::ios_base::dec;// 函数指针类型的别名
using func = void (*)(int, int); // func 是指向 void(int, int) 函数的指针
void example(int a, int b) {}
func f = example;// 指针类型的别名
template<class T>
using ptr = T*; // ptr<T> 是 T* 的别名
ptr<int> x;// 隐藏模板参数的类型别名
template<class CharT>
using mystring = std::basic_string<CharT, std::char_traits<CharT>>; // mystring<char> 是 std::string 的别名
mystring<char> str;int main() {std::cout << "Flags: " << fl << std::endl;f(1, 2);*x = 42;std::cout << "String: " << str << std::endl;
}
2. 别名模板
别名模板(Alias Template)是 C++11 引入的一个特性,允许开发者为一组相关类型创建别名。别名模板本质上是一个模板,当它被特化时,等效于将模板参数替换到类型标识中的结果。别名模板不能进行部分或显式特化。
语法
template <模板参数列表>
using 标识符 = 类型标识;// 自 C++20 起,可以添加约束表达式
template <模板参数列表> requires 约束
using 标识符 = 类型标识;
模板参数列表
:模板参数列表,类似于普通模板声明。标识符
:为模板定义的新名称。类型标识
:现有的类型,可以包含模板参数。约束
:自 C++20 起,可以使用约束表达式来限制模板参数的范围。
示例
#include <vector>
#include <memory>// 别名模板,简化容器的声明
template<class T>
using Vec = std::vector<T, std::allocator<T>>;Vec<int> v; // Vec<int> 等价于 std::vector<int, std::allocator<int>>// 别名模板,简化智能指针的声明
template<class T>
using UniquePtr = std::unique_ptr<T>;UniquePtr<int> up; // UniquePtr<int> 等价于 std::unique_ptr<int>// 别名模板,简化嵌套类型的声明
template<typename T>
struct Container {using value_type = T;
};template<typename ContainerT>
void info(const ContainerT& c) {typename ContainerT::value_type T;std::cout << "ContainerT is `" << typeid(ContainerT).name() << "`\n"<< "value_type is `" << typeid(T).name() << "`\n";
}// 别名模板结合 SFINAE(Substitution Failure Is Not An Error)
template<typename Condition>
using EnableIf = typename std::enable_if<Condition::value>::type;template<typename T, typename = EnableIf<std::is_polymorphic<T>>>
int fpoly_only(T) { return 1; }struct S { virtual ~S() {} };int main() {Container<int> c;info(c); // 输出 ContainerT 和 value_type 的信息// fpoly_only(c); // 错误:enable_if 禁止此调用S s;fpoly_only(s); // 正确:enable_if 允许此调用
}
3. 别名模板的特性
-
特化行为:
- 当特化别名模板时,编译器会将模板参数替换到类型标识中,生成相应的类型。
- 例如,
Vec<int>
会被特化为std::vector<int, std::allocator<int>>
。
-
依赖类型:
- 如果别名模板的结果是依赖的模板标识(即类型标识中包含模板参数),后续的替换将应用于该模板标识。
- 例如,
void_t<typename T::foo>
会在特化时检查T
是否有嵌套类型foo
。
-
递归限制:
- 特化别名模板时生成的类型不允许直接或间接使用其自身的类型。否则会导致编译错误。
- 例如,以下代码会导致错误:
template<class T> struct A;template<class T> using B = typename A<T>::U;template<class T> struct A { typedef B<T> U; };B<short> b; // 错误:B<short> 使用了其自身的类型 via A<short>::U
-
模板模板参数推断:
- 别名模板在推断模板模板参数时,永远不会通过模板参数推断推断出来。
- 例如,
template<class T> using A = decltype([] {});
中的 lambda 表达式的类型在不同实例化之间是不同的,即使 lambda 表达式不依赖于模板参数。
-
部分特化和显式特化:
- 别名模板不能进行部分特化或显式特化。如果需要特化,应该使用类模板而不是别名模板。
-
约束表达式(自 C++20 起):
- 自 C++20 起,可以在别名模板中使用
requires
子句来添加约束表达式,限制模板参数的范围。 - 例如:
template <typename T> requires std::is_integral_v<T> using IntOnly = T;
- 自 C++20 起,可以在别名模板中使用
4. 类型别名与 typedef
的比较
-
语法差异:
typedef
使用传统语法,而using
提供了更现代、更直观的语法。typedef
通常出现在类型说明符之后,而using
可以直接放在类型声明的开头。
-
功能相同:
- 两者都用于为现有类型创建别名,不会引入新的类型。
- 两者都不能改变现有类型名称的含义。
-
适用范围:
typedef
不能用于模板,而using
可以用于创建别名模板。using
更适合用于复杂的类型声明,尤其是涉及模板的情况下。
5. 示例代码
#include <iostream>
#include <string>
#include <type_traits>
#include <typeinfo>
#include <vector>
#include <memory>// 简单的类型别名
using flags = std::ios_base::fmtflags;
flags fl = std::ios_base::dec;// 函数指针类型的别名
using func = void (*)(int, int);
void example(int a, int b) {}
func f = example;// 指针类型的别名
template<class T>
using ptr = T*;
ptr<int> x;// 隐藏模板参数的类型别名
template<class CharT>
using mystring = std::basic_string<CharT, std::char_traits<CharT>>;
mystring<char> str;// 别名模板,简化容器的声明
template<class T>
using Vec = std::vector<T, std::allocator<T>>;Vec<int> v;// 别名模板,简化智能指针的声明
template<class T>
using UniquePtr = std::unique_ptr<T>;UniquePtr<int> up;// 别名模板,简化嵌套类型的声明
template<typename T>
struct Container {using value_type = T;
};template<typename ContainerT>
void info(const ContainerT& c) {typename ContainerT::value_type T;std::cout << "ContainerT is `" << typeid(ContainerT).name() << "`\n"<< "value_type is `" << typeid(T).name() << "`\n";
}// 别名模板结合 SFINAE(Substitution Failure Is Not An Error)
template<typename Condition>
using EnableIf = typename std::enable_if<Condition::value>::type;template<typename T, typename = EnableIf<std::is_polymorphic<T>>>
int fpoly_only(T) { return 1; }struct S { virtual ~S() {} };int main() {std::cout << "Flags: " << fl << std::endl;f(1, 2);*x = 42;std::cout << "String: " << str << std::endl;Container<int> c;info(c); // 输出 ContainerT 和 value_type 的信息// fpoly_only(c); // 错误:enable_if 禁止此调用S s;fpoly_only(s); // 正确:enable_if 允许此调用return 0;
}
6. 总结
- 类型别名 (
using
):使用using
关键字为现有类型创建别名,语法更直观,适用于块作用域、类作用域或命名空间作用域。类型别名与typedef
功能相同,但using
更适合用于复杂的类型声明。 - 别名模板:使用
using
关键字为一组相关类型创建别名模板。别名模板可以简化复杂的模板类型声明,并且可以在特化时生成相应的类型。别名模板不能进行部分或显式特化,但在 C++20 中可以添加约束表达式来限制模板参数的范围。 - 优点:类型别名和别名模板使得代码更加简洁、易读,特别是在处理复杂类型和模板编程时。它们减少了手动编写冗长类型声明的需求,提高了代码的可维护性。
通过合理使用类型别名和别名模板,开发者可以编写更简洁、更具表达力的代码,尤其是在处理复杂类型和模板编程时。
详尽类型说明符 (Elaborated Type Specifier)
在 C++ 中,详尽类型说明符(Elaborated Type Specifier)用于引用之前声明的类名(class
、struct
或 union
)或枚举名(enum
),即使这些名称被非类型声明隐藏。此外,详尽类型说明符也可以用于声明新的类名。它们提供了更明确的方式来进行类型引用,尤其是在命名冲突或作用域复杂的情况下。
1. 语法
详尽类型说明符有以下几种形式:
-
类类型的详尽类型说明符:
class-key class-name;
-
枚举类型的详尽类型说明符:
enum enum-name;
-
仅包含详尽类型说明符的声明(通常称为类的前向声明):
class-key attr (可选) 标识符;
class-key
:可以是class
、struct
或union
。class-name
:先前声明的类类型的名称,可以是限定的或未限定的标识符。enum-name
:先前声明的枚举类型的名称,可以是限定的或未限定的标识符。attr
:自 C++11 起,可以包含任意数量的属性。
2. 解释
2.1 引用已声明的类型
详尽类型说明符中的 class-name
或 enum-name
可以是简单标识符,也可以是限定标识符。名称查找的方式取决于它们的外观:
- 非限定名称查找:如果名称是未限定的(即没有作用域限定符),则使用非限定名称查找规则。
- 限定名称查找:如果名称是限定的(即带有作用域限定符),则使用限定名称查找规则。
无论哪种方式,名称查找都不会考虑非类型名称。这意味着,如果一个变量或函数与类或枚举同名,详尽类型说明符会优先匹配类或枚举,而忽略其他同名的非类型声明。
2.2 处理命名冲突
详尽类型说明符的一个重要用途是解决命名冲突。例如,当一个局部变量与类名同名时,使用详尽类型说明符可以明确指定要引用的是类而不是局部变量。
class T {
public:class U;
private:int U;
};int main() {int T; // 局部变量 TT t; // 错误:找到局部变量 Tclass T t; // 正确:找到全局类 T,局部变量 T 被忽略T::U* u; // 错误:找到私有数据成员 Uclass T::U* u; // 正确:找到类 T 中的嵌套类 U,私有数据成员 U 被忽略
}
2.3 声明新的类名
如果详尽类型说明符中的 class-name
是一个未声明的标识符,并且由 class
、struct
或 union
关键字引入,则该详尽类型说明符将声明一个新的类名。这种形式通常用于类的前向声明。
class Node; // 前向声明 Node 类
2.4 不透明枚举声明
类似于类的前向声明,C++11 引入了不透明枚举声明,它允许在不完全定义枚举的情况下声明枚举类型。不透明枚举声明后,枚举类型是完整类型。
enum Color : int; // 不透明枚举声明
2.5 注入类名
在类内部,类名会被注入到类的作用域中,因此可以直接使用类名来引用自身。然而,为了明确指定类名,仍然可以使用详尽类型说明符。
template<typename T>
struct Node {struct Node* Next; // OK: lookup of Node 找到注入类名struct Data* Data; // OK: 声明全局作用域中的 Data 类型,并声明数据成员 Datafriend class ::List; // 错误:不能引入限定名称enum Kind* kind; // 错误:不能引入枚举类型
};Data* p; // OK: struct Data 已经被声明
2.6 引用 typedef 名称、类型别名、模板类型参数或别名模板特化
如果详尽类型说明符中的名称引用了 typedef
名称、类型别名、模板类型参数或别名模板特化,程序将形成错误。这是因为详尽类型说明符只能用于引用类或枚举类型,而不能用于这些间接类型的别名。
template<typename T>
class Node {friend class T; // 错误:模板类型参数不能出现在详尽类型说明符中// 注意:类似的声明 `friend T;` 是正确的
};class A {};
enum b { f, t };int main() {class A a; // 正确:等价于 'A a;'enum b flag; // 正确:等价于 'b flag;'
}
2.7 关键字一致性
详尽类型说明符中的 class-key
或 enum-key
必须与所引用的类型一致:
enum
关键字:必须用于引用枚举类型(无论是作用域内的还是作用域外的)。union
关键字:必须用于引用联合体类型。class
或struct
关键字:必须用于引用非联合体类类型。class
和struct
在这里是可以互换的。
enum class E { a, b };
enum E x = E::a; // 正确
enum class E y = E::b; // 错误:'enum class' 不能出现在详尽类型说明符中struct A {};
class A a; // 正确:'class' 和 'struct' 在这里是可以互换的
2.8 作为模板参数
当详尽类型说明符用作模板参数时,class T
表示一个名为 T
的类型模板参数,而不是一个未命名的非类型参数,其类型为 T
。
template<class T>
class Container {T value;
};
3. 示例代码
#include <iostream>// 示例 1:解决命名冲突
class T {
public:class U;
private:int U;
};int main() {int T; // 局部变量 TT t; // 错误:找到局部变量 Tclass T t; // 正确:找到全局类 T,局部变量 T 被忽略T::U* u; // 错误:找到私有数据成员 Uclass T::U* u; // 正确:找到类 T 中的嵌套类 U,私有数据成员 U 被忽略
}// 示例 2:前向声明
class Node; // 前向声明 Node 类void process(Node* n); // 使用前向声明的 Node 类// 示例 3:不透明枚举声明
enum Color : int; // 不透明枚举声明void setColor(Color c); // 使用不透明枚举声明的 Color 类型// 示例 4:注入类名
template<typename T>
struct Node {struct Node* Next; // OK: lookup of Node 找到注入类名struct Data* Data; // OK: 声明全局作用域中的 Data 类型,并声明数据成员 Datafriend class ::List; // 错误:不能引入限定名称enum Kind* kind; // 错误:不能引入枚举类型
};Data* p; // OK: struct Data 已经被声明// 示例 5:引用 typedef 名称、类型别名、模板类型参数或别名模板特化
template<typename T>
class Node {friend class T; // 错误:模板类型参数不能出现在详尽类型说明符中// 注意:类似的声明 `friend T;` 是正确的
};class A {};
enum b { f, t };int main() {class A a; // 正确:等价于 'A a;'enum b flag; // 正确:等价于 'b flag;'
}// 示例 6:关键字一致性
enum class E { a, b };
enum E x = E::a; // 正确
enum class E y = E::b; // 错误:'enum class' 不能出现在详尽类型说明符中struct A {};
class A a; // 正确:'class' 和 'struct' 在这里是可以互换的
4. 总结
- 详尽类型说明符:用于引用之前声明的类名或枚举名,即使这些名称被非类型声明隐藏。它们还可以用于声明新的类名。
- 解决命名冲突:详尽类型说明符可以明确指定要引用的是类或枚举,而不是同名的非类型声明。
- 前向声明:详尽类型说明符可以用于类的前向声明,允许在类完全定义之前引用类。
- 不透明枚举声明:C++11 引入了不透明枚举声明,允许在不完全定义枚举的情况下声明枚举类型。
- 限制:详尽类型说明符不能用于引用
typedef
名称、类型别名、模板类型参数或别名模板特化。 - 关键字一致性:详尽类型说明符中的
class-key
或enum-key
必须与所引用的类型一致。
通过合理使用详尽类型说明符,开发者可以编写更清晰、更安全的代码,特别是在处理复杂的命名空间和作用域时。
属性说明符序列 (自 C++11 起)
属性说明符序列(Attribute Specifier Sequence)是 C++11 引入的一个特性,用于为类型、对象、函数、代码块等添加实现定义的属性。这些属性可以提供额外的信息给编译器,帮助优化代码、生成警告或控制行为。C++17 进一步扩展了属性说明符的功能,允许使用命名空间和包扩展。
1. 语法
属性说明符序列的基本语法如下:
[[ attribute-list ]] // 自 C++11 起
[[ using attribute-namespace : attribute-list ]] // 自 C++17 起
attribute-list
:零个或多个以逗号分隔的属性,可能以省略号...
结束,表示包扩展。attribute-namespace
:一个标识符,用于指定属性的命名空间。
每个属性可以有以下几种形式:
-
简单属性:
[[identifier]] // 例如 [[noreturn]]
-
具有命名空间的属性:
[[attribute-namespace::identifier]] // 例如 [[gnu::unused]]
-
具有参数的属性:
[[identifier (argument-list)]] // 例如 [[deprecated("because")]]
-
同时具有命名空间和参数列表的属性:
[[attribute-namespace::identifier (argument-list)]] // 例如 [[gnu::aligned(16)]]
示例
// 简单属性
[[noreturn]] void terminate() { exit(0); }// 具有命名空间的属性
[[gnu::unused]] int unused_variable = 42;// 具有参数的属性
[[deprecated("Use new_function instead")]] void old_function() {}// 同时具有命名空间和参数的属性
[[gnu::aligned(16)]] struct AlignedStruct {int data[4];
};
2. 属性的作用范围
属性可以在 C++ 程序中几乎所有地方使用,并且可以应用于几乎所有东西,包括但不限于:
- 类型声明
- 变量声明
- 函数声明和定义
- 名称
- 代码块
- 整个翻译单元
然而,每个特定属性仅在实现允许的地方有效。例如,[[expect_true]]
可能是一个仅能与 if
语句一起使用的属性,而 [[omp::parallel()]]
可能是一个应用于代码块或 for
循环的属性,但不能应用于 int
类型。
3. 标准属性
C++ 标准定义了一些标准属性,这些属性在所有符合标准的编译器中都应支持。以下是常见的标准属性:
-
[[noreturn]]
(自 C++11 起):- 表示该函数不会返回。编译器可以根据此信息进行优化。
[[noreturn]] void terminate() { exit(0); }
-
[[carries_dependency]]
(自 C++11 起):- 表示依赖链会传播到函数内部和外部,影响内存顺序。
[[carries_dependency]] void propagate_dependency();
-
[[deprecated]]
和[[deprecated("reason")]]
(自 C++14 起):- 表示不建议使用某个声明或实体,并可提供原因。
[[deprecated("Use new_function instead")]] void old_function() {}
-
[[fallthrough]]
(自 C++17 起):- 表示从上一个
case
标签的贯穿是故意的,避免编译器发出警告。
switch (value) {case 1:do_something();[[fallthrough]];case 2:do_more();break; }
- 表示从上一个
-
[[maybe_unused]]
(自 C++17 起):- 抑制编译器对未使用实体的警告。
[[maybe_unused]] int unused_variable = 42;
-
[[nodiscard]]
和[[nodiscard("reason")]]
(自 C++17 起):- 鼓励编译器在返回值被丢弃时发出警告。
[[nodiscard]] std::string get_message();
-
[[likely]]
和[[unlikely]]
(自 C++20 起):- 提示编译器针对特定执行路径进行优化。
if [[likely]] (condition) {// 优化为更可能执行的路径 } else {// 优化为不太可能执行的路径 }
-
[[no_unique_address]]
(自 C++20 起):- 表示非静态数据成员不必具有与其类中所有其他非静态数据成员不同的地址。
struct S {[[no_unique_address]] int x;int y; };
-
[[assume(expression)]]
(自 C++23 起):- 指定在给定点
expression
将始终计算为true
。
void check(int x) {[[assume(x > 0)]];// 编译器可以假设 x 总是大于 0 }
- 指定在给定点
-
[[indeterminate]]
(自 C++26 起):- 指定如果对象未初始化,则它具有不确定的值。
[[indeterminate]] int uninitialized;
4. 非标准属性
除了标准属性外,编译器还可以支持具有实现定义行为的非标准属性。这些属性通常带有命名空间,以避免冲突。常见的非标准属性包括:
- GNU 属性:如
[[gnu::always_inline]]
、[[gnu::hot]]
、[[gnu::const]]
等。 - Clang 属性:如
[[clang::trivial_abi]]
、[[clang::annotate("annotation")]]
等。 - MSVC 属性:如
[[msvc::noop_dtor]]
、[[msvc::forceinline]]
等。
示例
// 使用 GNU 属性
[[gnu::always_inline]] [[gnu::hot]] [[nodiscard]]
inline int fast_function() {return 42;
}// 使用 Clang 属性
[[clang::trivial_abi]] struct TrivialStruct {int x;
};// 使用 MSVC 属性
[[msvc::forceinline]] void inline_function() {}
5. 属性说明符序列的组合
属性说明符序列可以出现在声明的不同位置,并且可以组合使用。属性可以出现在整个声明之前,也可以直接出现在被声明实体的名称之后。在大多数情况下,属性应用于直接之前的实体。
示例
// 组合多个属性
[[gnu::always_inline, gnu::const, gnu::hot, nodiscard]]
int fast_function() {return 42;
}// 使用 C++17 的 using 语法
[[using gnu : const, always_inline, hot]] [[nodiscard]]
int fast_function() {return 42;
}
6. alignas
说明符 (自 C++11 起)
alignas
说明符用于指定类型或对象的对齐要求。它可以应用于类、非位域类数据成员和变量的声明,但不能应用于函数参数或 catch
子句的异常参数。
语法
alignas(表达式)
alignas(类型标识符)
alignas(包 ...)
表达式
:必须是整型常量表达式,其计算结果为零,或为对齐或扩展对齐的有效值。类型标识符
:等效于alignas(alignof(类型标识符))
。包 ...
:等效于对同一声明应用多个alignas
说明符,每个说明符对应于参数包的每个成员。
示例
#include <iostream>// 对齐到 float 的对齐方式
struct alignas(float) struct_float {// your definition here
};// 对齐到 32 字节边界
struct alignas(32) sse_t {float sse_data[4];
};int main() {struct default_aligned {float data[4];} a, b, c;sse_t x, y, z;std::cout<< "alignof(struct_float) = " << alignof(struct_float) << '\n'<< "sizeof(sse_t) = " << sizeof(sse_t) << '\n'<< "alignof(sse_t) = " << alignof(sse_t) << '\n'<< std::hex << std::showbase<< "&a: " << &a << "\n"<< "&b: " << &b << "\n"<< "&c: " << &c << "\n"<< "&x: " << &x << "\n"<< "&y: " << &y << "\n"<< "&z: " << &z << '\n';
}
输出示例
alignof(struct_float) = 4
sizeof(sse_t) = 32
alignof(sse_t) = 32
&a: 0x7fffcec89930
&b: 0x7fffcec89940
&c: 0x7fffcec89950
&x: 0x7fffcec89960
&y: 0x7fffcec89980
&z: 0x7fffcec899a0
7. 注意事项
- 无效的对齐方式:无效的非零对齐方式(如
alignas(3)
)是格式错误的。 - 比自然对齐方式更弱的对齐:如果声明中最严格的
alignas
比没有任何alignas
说明符时的对齐方式更弱,则程序是格式错误的。 - 忽略
alignas(0)
:alignas(0)
始终会被忽略。 - 包扩展:属性说明符序列中的包扩展(以
...
结尾)允许传递多个参数,适用于某些属性(如alignas
)。
8. 预处理器宏
可以使用 __has_cpp_attribute
预处理器宏检查给定平台上每个单独属性的存在。
示例
#if __has_cpp_attribute(noreturn)[[noreturn]] void terminate() { exit(0); }
#elsevoid terminate() { exit(0); }
#endif
9. 总结
- 属性说明符序列:提供了统一的标准语法来为类型、对象、函数等添加实现定义的属性。
- 标准属性:由 C++ 标准定义,确保跨平台一致性。
- 非标准属性:由编译器提供,带有命名空间以避免冲突。
alignas
说明符:用于指定对齐要求,确保对象按指定的对齐方式进行分配。- 组合使用:属性可以组合使用,并且可以出现在声明的不同位置。
通过合理使用属性说明符序列,开发者可以编写更优化、更安全的代码,并利用编译器提供的各种功能。
static_assert
声明 (自 C++11 起)
static_assert
是 C++11 引入的一个关键字,用于在编译时执行断言检查。它允许开发者在编译期间验证某些条件是否成立,如果条件不满足,则会生成编译错误,并提供可选的错误消息。这有助于捕获潜在的问题,而无需等到运行时才发现。
1. 语法
static_assert
的语法有三种形式:
-
带有固定错误消息的静态断言(自 C++11 起):
static_assert(bool-constexpr, unevaluated-string);
-
没有错误消息的静态断言(自 C++17 起):
static_assert(bool-constexpr);
-
带有用户生成的错误消息的静态断言(自 C++26 起):
static_assert(bool-constexpr, constant-expression);
bool-constexpr
:一个上下文转换为bool
的常量表达式。在 C++23 之前,不允许使用内置转换,除了非缩窄整数转换为bool
。自 C++23 起,bool-constexpr
是一个上下文转换为bool
的表达式,其中转换是常量表达式。unevaluated-string
:一个未求值的字符串字面量,用作错误消息。constant-expression
:一个满足特定条件的常量表达式,用于生成用户定义的错误消息(自 C++26 起)。
2. 解释
-
bool-constexpr
:必须是一个可以在编译时求值为true
或false
的布尔表达式。如果表达式求值为true
,则static_assert
没有任何效果;如果求值为false
,则会触发编译错误,并显示用户提供的错误消息(如果有)。 -
unevaluated-string
:这是一个字符串字面量,用作错误消息。它不会被求值,只是作为文本传递给编译器。这个字符串可以包含任何字符,但不能包含动态信息或模板参数。 -
constant-expression
(自 C++26 起):这是一个更灵活的错误消息生成方式。它必须满足以下条件:msg.size()
可以隐式转换为std::size_t
。msg.data()
可以隐式转换为const char*
。msg
必须是一个核心常量表达式。- 错误消息的文本由
msg.data()
指向的字符数组中的前msg.size()
个字符组成。
3. 作用范围
static_assert
可以出现在以下位置:
- 命名空间作用域:直接在命名空间中声明。
- 块作用域:在函数或代码块内部声明。
- 类作用域:在类的成员声明中使用。
4. 行为
- 如果
bool-constexpr
是良构的并且求值为true
,或者是在模板定义的上下文中求值并且模板未实例化,则static_assert
没有任何效果。 - 如果
bool-constexpr
求值为false
,则会触发编译错误,并且如果提供了用户定义的错误消息,则会将其包含在诊断消息中。 - 在模板中,
static_assert
仅在模板实例化时才会检查。如果模板从未被实例化,则static_assert
不会触发。
5. 示例
示例 1:基本用法
#include <type_traits>// 编译时断言:03301 == 1729
static_assert(03301 == 1729); // 自 C++17 起,消息字符串是可选的int main() {return 0;
}
示例 2:模板中的 static_assert
#include <type_traits>template<class T>
void swap(T& a, T& b) noexcept {// 确保 T 是可复制构造的static_assert(std::is_copy_constructible_v<T>, "Swap requires copying");// 确保 T 的复制构造和赋值操作不会抛出异常static_assert(std::is_nothrow_copy_constructible_v<T> && std::is_nothrow_copy_assignable_v<T>, "Swap requires nothrow copy/assign");auto c = b;b = a;a = c;
}struct no_copy {no_copy(const no_copy&) = delete;no_copy() = default;
};struct no_default {no_default() = delete;
};int main() {int a, b;swap(a, b); // 正常工作no_copy nc_a, nc_b;swap(nc_a, nc_b); // 错误:Swap requires copying[[maybe_unused]] data_structure<int> ds_ok; // 正常工作[[maybe_unused]] data_structure<no_default> ds_error; // 错误:Data structure requires default-constructible elements
}
示例 3:带有用户生成错误消息的 static_assert
(自 C++26 起)
#if __cpp_static_assert >= 202306L
#include <format>// 使用 std::format 生成动态错误消息
static_assert(sizeof(int) == 4, std::format("Expected 4, got {}", sizeof(int)));
#endif
示例 4:模板中的延迟断言
template<class T>
struct bad_type {// 使用依赖于模板参数的 false 来触发编译错误static_assert(dependent_false<T>, "error on instantiation, workaround");static_assert(false, "error on instantiation"); // OK because of CWG2518/P2593R1
};// 定义一个依赖于模板参数的 false 表达式
template<class>
constexpr bool dependent_false = false;int main() {// 这里不会触发编译错误,因为模板没有实例化// 如果尝试实例化 bad_type<int>,则会触发编译错误
}
6. 特性测试宏
可以使用以下特性测试宏来检查 static_assert
的支持情况:
__cpp_static_assert
:- 200410L:表示支持
static_assert
(语法 (1))。 - 201411L:表示支持单参数
static_assert
(语法 (2))。 - 202306L:表示支持带有用户生成错误消息的
static_assert
(语法 (3))。
- 200410L:表示支持
7. 注意事项
- 错误消息的限制:在 C++26 之前,
static_assert
的错误消息必须是字符串字面量,不能包含动态信息或模板参数。自 C++26 起,可以通过constant-expression
生成更复杂的错误消息。 - 编译器行为:标准不要求编译器逐字打印用户提供的错误消息,但大多数编译器会尽可能多地保留原始文本。
- 模板中的
static_assert
:在模板中,static_assert
仅在模板实例化时才会检查。如果模板从未被实例化,则static_assert
不会触发。
8. 总结
static_assert
:用于在编译时执行断言检查,确保某些条件在编译时成立。- 多版本支持:C++11 引入了带有固定错误消息的
static_assert
,C++17 允许省略错误消息,C++26 支持更灵活的用户生成错误消息。 - 作用范围:可以在命名空间、块和类作用域中使用。
- 模板中的应用:在模板中,
static_assert
仅在模板实例化时才会检查,允许编写更安全的泛型代码。
通过合理使用 static_assert
,开发者可以在编译时捕获潜在的错误,提高代码的健壮性和安全性。
相关文章:
现代C++ 6 声明
文章目录 C 中的冲突声明规则1. **对应声明(Corresponding Declarations)**2. **对应函数重载(Corresponding Function Overloads)**3. **对应函数模板重载(Corresponding Function Template Overloads)**4…...
Spark区分应用程序 Application、作业Job、阶段Stage、任务Task
目录 一、Spark核心概念 1、应用程序Application 2、作业Job 3、阶段Stage 4、任务Task 二、示例 一、Spark核心概念 在Apache Spark中,有几个核心概念用于描述应用程序的执行流程和组件,包括应用程序 Application、作业Job、阶段Stage、任务Task…...
【WebRTC】Android SDK使用教学
文章目录 前言PeerConnectionFactoryPeerConnection 前言 最近在学习WebRTC的时候,发现只有JavaScript的API文档,找了很久没有找到Android相关的API文档,所以通过此片文章记录下在Android应用层如何使用WebRTC 本篇文章结合:【W…...
算法-字符串-8.字符串转换整数
一、题目 二、思路解析 1.思路: 依次遍历,查看当前字符是否在规定范围内 2.常用方法: 1.trim(),去字符串的首尾空字符 ss.trim(); 2.substring(beginIndex),截断字符串,得到新的字符串是[1,s.length()-1] ss.substring(1); 3.st…...
普通算法——一维前缀和
一维前缀和 题目链接:https://www.acwing.com/problem/content/797/ 题目描述: 输入一个长度为 n 的整数序列。接下来再输入 m 个询问,每个询问输入一对 l,r。对于每个询问,输出原序列中从第 l 个数到第 r 个数的和。 **什么是…...
【Elasticsearch】ES+MySQL实现迷糊搜索
1. 技术选型 使用 Elasticsearch (ES) 结合 MySQL 进行数据存储和查询,而不是直接从 MySQL 中进行查询,主要是为了弥补传统关系型数据库(如 MySQL)在处理大规模、高并发和复杂搜索查询时的性能瓶颈。具体来说,ES 与 My…...
MacOS编译webRTC源码小tip
简单记录一下,本人在编译webRTC时,碰到了一下比较烦人的问题,在MacOS终端下,搭建科学上网之后,chromium的depot_tools仓库成功拉下来了,紧接着,使用fetch以及gclient sync始终都返回curl相关的网…...
Android显示系统(05)- OpenGL ES - Shader绘制三角形(使用glsl文件)
Android显示系统(02)- OpenGL ES - 概述 Android显示系统(03)- OpenGL ES - GLSurfaceView的使用 Android显示系统(04)- OpenGL ES - Shader绘制三角形 Android显示系统(05)- OpenGL…...
深度学习小麦头检测-基于Faster-RCNN的小麦头检测——附项目源码
比赛描述 为了获得有关全世界麦田的大量准确数据,植物科学家使用“小麦头”(包含谷物的植物上的穗)的图像检测。这些图像用于估计不同品种的小麦头的密度和大小。但是,在室外野外图像中进行准确的小麦头检测可能在视觉上具有挑战性。密集的小麦植株经常重叠,并且风会使照片…...
成像报告撰写格式
成像报告撰写格式 实验人员: 实验时间: 实验地点: 实验目的: 1实验仪器 1.1相机 包括制造商,型号,面阵还是线阵,彩色还是黑白,图像尺寸,光学接口等。 1.2镜头 包…...
【数学建模】线性规划问题及Matlab求解
问题一 题目: 求解下列线性规划问题 解答: 先将题目中求最大值转化为求最小值,则有 我们就可以得到系数列向量: 我们对问题中所给出的不等式约束进行标准化则得到了 就有不等式约束条件下的变系数矩阵和常系数矩阵分别为: 等式…...
C# 事件(Event)
文章目录 前言1、 声明委托2、 声明事件3、 触发事件4、订阅和取消订阅事件5、示例展示示例一:基础的事件使用流程示例二:简单数值变化触发事件示例三:锅炉系统相关事件应用 前言 在 C# 中,事件(Event)是一…...
企业数字化转型:从爆品起步,迈向生态平台
在当今数字化浪潮席卷全球的时代,企业数字化转型已成为必然趋势。然而,这条转型之路该如何走呢? 企业数字化转型的路径设计,绝不仅仅是技术的升级换代,它需要综合考量多方面因素。一方面,要为实现战略目标做…...
Windows 安装 MySQL
1.下载 MySQL 安装包 访问:MySQL :: Download MySQL Installer选择适合的版本。推荐下载 MySQL Installer for Windows,该安装包包含所有必要的组件选择 Windows (x86, 32-bit), MSI Installer 或 Windows (x86, 64-bit), MSI Installer 2.运行安装程序…...
游戏引擎学习第37天
仓库 : https://gitee.com/mrxiao_com/2d_game 回顾目前的进展 一个简单的调试工具——位图加载器,用于加载存储在硬盘上的位图文件。这个工具将文件加载到内存中,并查看文件头部信息,确保其正确性。接着使用位图头中的偏移量来获取像素数据…...
非常简单实用的前后端分离项目-仓库管理系统(Springboot+Vue)part 4
三十三、出入库管理 Header.vue导一下,RecordController加一个 //将入库数据和原有数据相加吧//新增PostMapping("/save")public Result save(RequestBody Record record) {return recordService.save(record) ? Result.success() : Result.fail();} GoodsManage.v…...
知乎Java后台开发面试题及参考答案
请简述 TCP 的三次握手和四次挥手过程。 TCP(传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议。 三次握手过程 首先,客户端想要建立连接,会发送一个带有 SYN(同步序列号)标志的 TCP 报文段,这个报文段中还包含一个初始序列号(ISN,Initial Sequenc…...
Java中的String类用法详解
1.字符串拆分 可以把一个完整的字符串按照规定的分隔符拆分为若干个子字符串 String[] split(String regex) 将字符串全部拆分 String[] split(String regex,int limit) 将字符串以指定的格式拆分,拆分成limit组 实例:字符串的拆分处理 public class Main4 {public stat…...
mac电脑安装hadoop、hive等大数据组件
背景:用本地的Hadoop测试Java调用cmd命令 2024-12-08 13:48:19,826 WARN util.NativeCodeLoader: Unable to load native-hadoop library for your platform... using builtin-java classes where applicable ls: .: No such file or directory解决方案:…...
DHCP和DNS
DHCP(动态主机配置协议)和DNS(域名系统)是计算机网络中两个重要的协议,它们在网络的管理和使用中发挥着关键作用。 DHCP(动态主机配置协议) 基本功能 自动分配IP地址:DHCP允许网…...
Postman安装使用教程
Postman(接口测试工具) ①、介绍 Postman是一款支持http协议的接口调试与测试工具,它不仅可以调试简单的css、html、脚本等简单的网页基本信息,还可以发送几乎所有类型的HTTP请求。 ②、安装 Ⅰ、运行安装包/官网直搜 Ⅱ、创建…...
剖析千益畅行,共享旅游-卡,合规运营与技术赋能双驱下的旅游新篇
在数字化浪潮席卷各行各业的当下,旅游产业与共享经济模式深度融合,催生出旅游卡这类新兴产品。然而,市场乱象丛生,诸多打着 “共享” 幌子的旅游卡弊病百出,让从业者与消费者都深陷困扰。今天,咱们聚焦技术…...
信创改造-达梦数据库配置项 dm.ini 优化
设置模式:兼容MySQL,COMPATIBLE_MODE 4 内存占比:90%,MAX_OS_MEMORY 90 目标内存:2G(不影响申请内存超过2G,但这部分内存不会回收),MEMORY_TARGET 2000 参考 https:…...
docker入门 自记录
1.先自己下载离线bao .tar 或者 自己pull docker pull xxx 如果遇到网络问题就换源 2.之后run一个docker 后面是映射本地路径 sudo docker run -it --name ultralytics_241124 --gpus all --shm-size 8G -v /home/oppenheim/detect/train241204/docker:/home/docker ultralyti…...
Axure设计之动态图表——排名图(中继器)
粉丝问我可不可以用中继器做条形图,而且是要做成自动增长的排名图表。所以现在教大家怎么用axure来制作制作排名图。 这个原型制作完成之后,后期有类似的功能,直接拿过去使用也比较简单,基本只需要修改中继器数据就可以了。喜欢、…...
在Java中几种常用数据压缩算法的实现及其优劣势
在Java中几种常用数据压缩算法的实现及其优劣势 背景:项目需要引入Redis作为缓存组件,需要考虑到Redis的内存占用(机器内存越大,成本越高),因此需要引入数据压缩。 1、介绍 数据压缩是计算机领域中一项重要…...
Mac通过Windows App远程访问windows电脑报错0x104的解决办法
1、远程windows电脑,确保打开 远程访问 2、Mac电脑上的配置: 2.1 新版的windows app远程桌面软件相比之前老的Microsoft Remote Desktop,对于mac来说,不会弹出“是否允许该app查找本地网络设备”,需要手动打开 操作步…...
Spring Boot接口返回统一格式
统一的标准数据格式好处 SpringBoot返回统一的标准数据格式主要有以下几点好处: 增强接口的可读性和可维护性,使得前端开发人员能够更加清晰地理解接口返回的数据结构,从而提高开发效率。 降低前后端耦合度,当后端需要修改返回数…...
小程序入门学习(八)之页面事件
一、下拉刷新新事件 1. 什么是下拉刷新 下拉刷新是移动端的专有名词,指的是通过手指在屏幕上的下拉滑动操作,从而重新加载页面数据的行为。 2. 启用下拉刷新 启用下拉刷新有两种方式: 全局开启下拉刷新:在 app.json 的 window…...
Docker基础【windows环境】
课程内容来自尚硅谷3小时速通Docker教程 1. Docker简介 Docker 通过 Docker Hub 实现一行命令安装应用(镜像)【Nginx,Mysql等】,避免繁琐的部署操作。同时通过轻量级(相对于虚拟机)的容器化的思想&#x…...
【docker】docker compose 和 docker swarm
Docker Compose 和 Docker Swarm 都是 Docker 生态中的工具,但它们有不同的用途和目标。 下面是这两者的主要区别,帮助你理解它们在不同场景中的使用。 1. 用途和目标 Docker Compose: 目标:主要用于在单个机器上定义和运行多个容器应用&a…...
第三部分:进阶概念 7.数组与对象 --[JavaScript 新手村:开启编程之旅的第一步]
第三部分:进阶概念 7.数组与对象 --[JavaScript 新手村:开启编程之旅的第一步] 在 JavaScript 中,数组和对象是两种非常重要的数据结构,它们用于存储和组织数据。尽管它们都属于引用类型(即它们存储的是对数据的引用而…...
LabVIEW密码保护与反编译的安全性分析
在LabVIEW中,密码保护是一种常见的源代码保护手段,但其安全性并不高,尤其是在面对专业反编译工具时。理论上,所有软件的反编译都是可能的,尽管反编译不一定恢复完全的源代码,但足以提取程序的核心功能和算法…...
Docker魔法:用docker run -p轻松开通容器服务大门
前言 “容器”与“虚拟化”作为现代软件开发和运维中的关键概念,已经广泛应用于各个技术领域。然而,在使用 Docker 部署应用时,常常会遇到这样的问题:容器正常运行,却无法让外界访问其内部服务?即使容器内的应用顺利启动,外部无法通过浏览器或 API 进行连接。此时,doc…...
ubuntu防火墙(三)——firewalld使用与讲解
本文是Linux下,用ufw实现端口关闭、流量控制(二) firewalld使用方式 firewalld 是一个动态管理防火墙的工具,主要用于 Linux 系统(包括 Ubuntu 和 CentOS 等)。它提供了一个基于区域(zones)和服务&#x…...
【大数据技术基础 | 实验十一】Hive实验:新建Hive表
文章目录 一、实验目的二、实验要求三、实验原理四、实验环境五、实验内容和步骤(一)启动Hive(二)创建表(三)显示表(四)显示表列(五)更改表(六&am…...
Python实现Excel中数据条显示
Python中要实现百分比数据条的显示,可以使用pandas库,pandas图表样式的设置与Excel中的条件格式设置比较类似,比如Excel里常用的数据条的用法,在pandas中使用代码进行高亮显示,用来突出重点数据,下面一起来…...
矩阵与向量的基本概念
**一、四个基本子空间的定义** 1. **行空间(Row Space)** 行空间是由矩阵的所有行向量所形成的空间。它包含所有可能的行向量的线性组合。行空间的维度称为矩阵的行秩。 2. **零空间(Null Space)** 零空间是与矩阵相乘后结果为零的…...
亚马逊云科技大语言模型加速OCR应用场景发展
目录 前言Amazon Bedrock关于OCR解决方案Amazon Bedrock进行OCR关键信息提取方案注册亚马逊账号API调用环境搭建 总结 前言 大语言模型是一种基于神经网络的自然语言处理技术,它能够学习和预测自然语言文本中的规律和模式,可以理解和生成自然语言的人工…...
十九(GIT2)、token、黑马就业数据平台(页面访问控制(token)、首页统计数据、登录状态失效)、axios请求及响应拦截器、Git远程仓库
1. JWT介绍 JSON Web Token 是目前最为流行的跨域认证解决方案,本质就是一个包含信息的字符串。 如何获取:在使用 JWT 身份验证中,当用户使用其凭据成功登录时,将返回 JSON Web Token(令牌)。 作用…...
深入探索现代 IT 技术:从云计算到人工智能的全面解析
目录 1. 云计算:重塑 IT 基础设施 2. 大数据:挖掘信息的价值 3. 物联网(IoT):连接物理世界 4. 区块链:重塑信任机制 5. 人工智能(AI):智能未来的驱动力 结语 在当今…...
Redis的持久化
目录 1. 文章前言2. RDB2.1 触发机制2.2 流程说明2.3 RDB文件的处理2.4 RDB的优缺点 3. AOF3.1 使用AOF3.2 命令写入3.3 文件同步3.4 重写机制3.5 启动时数据恢复 4. 持久化总结 1. 文章前言 (1)Redis支持RDB和AOF两种持久化机制,持久化功能…...
小型支付商城系统-MVC工程架构开发
第1-1节 DDD 架构概念 1.DDD 是什么 那 DDD 是什么呢?来自于维基百科的一段定义:"Domain-driven design (DDD) is a major software design approach. ",DDD 是一种软件设计方法。也就是说 DDD 是指导我们做软件工程设计的一种手…...
探索 ONLYOFFICE 8.2 版本:更高效、更安全的云端办公新体验
引言 在当今这个快节奏的时代,信息技术的发展已经深刻改变了我们的工作方式。从传统的纸质文件到电子文档,再到如今的云端协作,每一步技术进步都代表着效率的飞跃。尤其在后疫情时代,远程办公成为常态,如何保持团队之间…...
Spark 计算总销量
Spark 计算总销量 题目: 某电商平台存储了所有商品的销售数据,平台希望能够找到销量最好的前 N 个商品。通过分析销售记录,帮助平台决策哪些商品需要更多的推广资源。 假设你得到了一个商品销售记录的文本文件 product_id, product_name,…...
力扣每日一题 - 3001. 捕获黑皇后需要的最少移动次数
题目 还需要你前往力扣官网查看详细的题目要求 地址 1.现有一个下标从 1 开始的 8 x 8 棋盘,上面有 3 枚棋子。2.给你 6 个整数 a 、b 、c 、d 、e 和 f ,其中:(a, b) 表示白色车的位置。(c, d) 表示白色象的位置。(e, f) 表示黑皇后的位置。…...
【React】React常用开发工具
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 前言一、React DevTools二、Redux DevTools三、Create React App 前言 React 是一种用于构建用户界面的流行 JavaScript 库,由于其灵活性、性能和可重用…...
c++ 数据结构:图
图是一种重要的非线性数据结构,用于表示对象及其关系。它广泛应用于社交网络、交通网络、任务调度、导航等领域。 图的基本概念 图的定义: 图由 顶点(Vertex) 和 边(Edge) 组成,记为 G(V,E)&a…...
SpringBoot整合Mockito进行单元测试超全详细教程 JUnit断言 Mockito 单元测试
Mock概念 Mock叫做模拟对象,即用来模拟未被实现的对象可以预先定义这个对象在特定调用时的行为(例如返回值或抛出异常),从而模拟不同的系统状态。 导入Mock依赖 pom文件中引入springboot测试依赖,spring-boot-start…...
十六,Spring Boot 整合 Druid 以及使用 Druid 监控功能
十六,Spring Boot 整合 Druid 以及使用 Druid 监控功能 文章目录 十六,Spring Boot 整合 Druid 以及使用 Druid 监控功能1. Druid 的基本介绍2. 准备工作:3. Druid 监控功能 3.1 Druid 监控功能 —— Web 关联监控3.2 Druid 监控功能 —— …...