# 纲要 > 主干纲要、Hint/线索/路标 - **类数据成员的初始化** - 成员初始化列表 - 类内初始化项 - **类的数据成员** - 非静态数据成员 - **静态数据成员** static - **可变数据成员** mutable %% # Q&A #### 已明确 #### 待明确 > 当下仍存有的疑惑 **❓<font color="#c0504d"> 有什么问题?</font>** %% <br><br> # 类的数据成员的初始化 ## 初始化方式 ⭐ > 参见说明 [^1] (P65) 区分两类: - 对于 "**非静态数据成员**",初始化方式包括两种: - ==**构造函数初始化列表**==,也称 "**==成员初始化列表==**" (*constructor initializer list* / *member initializer list*) - **==类内初始化项==**,也称 "**==默认成员初始化项==**" (since C++11) (*in-class initializer* / *default member initializer*) - 对于 "**静态数据成员**",初始化方式包括: - 对于 **`inline` 或 `constexpr` 静态数据成员**,只能采用 "**==类内初始化==**"。 - 对于 `constexpr`,其初始化将在 "**编译时完成**" - 对于 **`const` 修饰的 "整数类型 or 枚举类型" 的静态数据成员**,"**==类内初始化==**" or "**==类外初始化==**" 均可。 - 当采用 "**类内初始化**" 时,其初始化将在 "**编译时完成**"。 - 本质上是编译器应用了 "**==常量传播==**" 优化,**省略了为该静态成员变量申请内存**,而直接**在所有使用该静态成员变量值的地方**进行**常量值替换**。 - 对于其他静态数据成员, "**==必须==在==类外==进行定义和初始化**"。 > [!NOTE] "成员初始化列表"与 "类内初始化" 同时存在时,**==前者的优先级更高==**,**前者将覆盖后者的值** > > ```cpp > class MyClass { > public: > MyClass () = default; // 类内初始化 `data=10` 将被应用 > MyClass (int x) : data (x) {} // data 的类内初始化值被覆盖 > private: > int data = 10; > }; > ``` > > [!important] 必须进行 "显式初始化" 的情况(否则编译错误) [^1] (P259) > > 1. 非静态数据成员是 "**==引用类型==**" 或者 **"==`const`类型==**" 时; > 2. 非静态数据成员是 "**类类型**" 且不具有 "**默认构造函数**" 时。 > > ```cpp title:member_init_list.cpp > class MyClass { > public: > int value; > const int cstValue; > OtherClass &obj; // 引用数据成员, 只能用初始化列表进行初始化; > // 使用构造函数的成员初始化列表来初始化非静态数据成员 > MyClass(int v1, int v2, OtherClass &rh) > : value(v1), cstValue(v2), obj(rh) {} > }; > ``` > > [!caution] 未被 "显式初始化" 的的数据成员的默认值 > > - 对于**非静态数据成员**,未被显式初始化时,将执行 "**默认初始化**" 或 "**零初始化**"(发生在进入构造函数体之前) > - (取决于其所属对象本身是被 "默认初始化" 还是 "值初始化") 。 > > > > > - 对于**静态数据成员**,其初始化**分为两个阶段**: > - (1)**静态初始化**阶段,由于无显式初始化项,故执行 **==零初始化==**; > - (2)**动态初始化**阶段,由于无显式初始化项,故执行**默认初始化** > - 对于类类型,则是调用默认构造函数; > - 对于基本数据类型,则什么也没做,最终表现为 "**零初始化**"。 > > [!caution] 在构造函数体内给成员变量赋值,不属于 "初始化",而是 "赋值"。 > > 示例:下例中的成员变量 `x` 和 `y` 由于未被显式初始化,因此实际上是先**在构造函数体之前被进行了 "==默认初始化=="**(对这两个基本类型的非静态变量而言是随机值),**然后才在函数体内被 "重新赋值"** 。 > > ```cpp > class MyClass { > int x; > double y; > const char ch; > int &ri; > public: > MyClass(int i, double d, char c) : ch(c), ri(i) { // const 与引用的成员必须显式初始化 > x = i; // 在构造函数体内赋值. > y = d; // > // ch = c; // 错误, const 成员不能被赋值. > // ri = i; // 错误, 引用没有被初始化, 不能为其赋值. > } > }; > ``` > > <br><br> ### 成员初始化列表 成员初始化列表跟在**构造函数的参数列表**之后,以冒号 `:` 开始,列出一个或多个成员及其初始化值。 成员初始化列表只能用于: - (1)**初始化 =="非静态"数据成员==**; - (2)**调用 "其他构造函数"** 或 "**基类的构造函数**" > [!NOTE] 成员初始化列表必须放在 "类定义" 处,而在声明处不能使用 > > ```cpp > class MyClass { > public: > MyClass(int a, int b); // 声明 > private: > int x, int y; > }; > > MyClass::MyClass(int a, int b) : x(a), y(b) {} // 定义 > ``` > > > [!NOTE] 成员初始化列表**只声明 "用于初始化成员的值"**,**==不影响==初始化的具体执行顺序**。 > > **成员变量的初始化顺序只取决于==类定义中声明的出现顺序==,与该顺序一致**。 > > ```cpp > class X { > int i; > int j; > public: > // 由于类定义中j在i之后才出现, 因此下面语句是错误的! > // 初始化列表不影响初始化的执行顺序, 成员变量的初始化顺序只取决于类中的声明顺序. 所以下面将会先用"未被初始化的`j`"来初始化`i`, 然后采用val对`j`进行初始化. > X(int val) : j(val), i(j) { } > // 因此, 最好使用参数值来初始化成员变量, 而不是用一个成员变量初始化其它成员变量. > }; > ``` > > #### 在成员初始化列表内调用 "其他构造函数" 这样的构造函数称之为 "**委托构造函数**",即调用类内的另一个构造函数来初始化成员。 ```cpp class MyClass { int x; double y; public: MyClass(int i) : x(i), y(0) {} // 委托构造函数, 委托另一个构造函数来完成初始化 MyClass(int i, double d) : MyClass(i) { y = d; } }; ``` #### 在成员初始化列表内调用 "基类构造函数" 若一个类继承自另一个类,则可以使用基类的构造函数来**初始化继承来的成员**。 ```cpp class Base { int x; public: Base(int i) : x(i) {} }; class Derived : public Base { public: using Base::Base;; // 继承构造函数 }; ``` <br><br> ### 类内初始化项 "**类内初始化项**" 允许**在类定义中直接为 "非静态数据成员" 提供初始值**,供**创建实例对象时进行初始化**,适用于基本类型、类类型、数组等成员。( since C++11) 类内初始化只能使用以下两种形式: - 使用 **`=` 符号** 的 "**拷贝初始化**" 形式 - 使用 **花括号** 的 "**列表初始化**" 形式 (直接列表初始化 or 拷贝列表初始化) ```cpp struct MyClass { int x = 10; // 类内初始化 double y{5.5}; // 类内初始化: 使用花括号进行列表初始化 const char ch = {'G'}; // 类内初始化; // 对类类型进行类内初始化 std::string classTypeMember = "example"; // 对容器类型类型进行类内初始化 std::vector<int> vectorMember = {1, 2, 3}; }; ``` <br><br> ## 初始化时机与顺序 ###### 对 "**非静态数据成员**" - 初始化时机:**进入构造函数体之前**,"**成员列表初始化值**" 将覆盖 "**类内初始值**"。 - 初始化顺序:只取决于 **==类定义中声明的出现顺序==,与该顺序一致**。 ###### 对 "**静态数据成员**" 其初始化分为 "**静态初始化**" (**==编译时==**) 与 "**动态初始化**" (**运行时**)两个阶段, 且**所有==静态初始化==(可在编译期确定的值) 会在动态初始化之前完成**,参见 [[02-开发笔记/01-cpp/cpp 基本概念/cpp-初始化声明|cpp-初始化声明]] 。 初始化时机: - (1)满足下列条件的静态数据成员,其将在为 "**静态初始化阶段(==编译时==)**" 完成初始化(**==常量初始化==**): - (1)使用 "**==类内初始化值==**" 的方式(即声明为 **`inline`、 `constexpr`**;或者 **`const` 修饰且是整型或枚举类型**) - (2)为 **==字面值类型==**,如基本数据类型、指针类型、枚举类型; - (3)初始化表达式为 "**==常量表达式==**"; - (2)其他静态数据成员,其值在: - (1)在 "**静态初始化阶段**" 被 **==零初始化==**; - (2)随后在 "**动态初始化阶段** 根据初始化式 "Initializer" 的形式执行对应的初始化。 初始化顺序:**遵循下列规则** - (1)**在同一个翻译单元中,静态数据成员的初始化按照它们==在代码中定义的顺序==进行。** - 注意区分 "**==阶段==**",如果一个静态数据成员**依赖于另一个静态成员的值,且后者是==动态初始化==的**,可能导致未定义行为。 - (2)**不同翻译单元**中定义的静态数据成员的初始化顺序是**未定义的**。 <br><br> # 类的静态数据成员 类的静态数据成员是**属于类本身的变量**,仅**存在一份数据==实体==**,**被类的所有实例对象所共享**,具有 "**静态存储持续性**",在整个程序运行期间有效。 静态数据成员的访问: 1. 可通过**类作用域**访问 `MyClass::`(可在不创建类的任何对象的情况下访问) 2. 也可通过**类的实例对象**访问; > [!NOTE] 静态数据成员可以是"**==不完整类型==**",甚至**可以是其==本身所属的类类型==**。 > > [!NOTE] `static` 关键字只需出现在类内部声明中,类外部定义静态数据成员、静态成员函数时,都不需要再加该关键字 #### 静态数据成员的初始化 初始化机制参见上文总结。 ```cpp title:static_member_init.cpp class MyClass { public: static int stcMem; // `non-const`静态数据成员只能在类外初始化 static int stcMem2; static const int cstStcMem = 5; // `const`修饰且为整型或枚举类型的静态数据成员可以在类内初始化, 其将在编译时被初始化 static const int cstStcMem2; // 也可以在类外初始化(但将在运行时完成初始化) static constexpr int stcCstexprMem = 66; // `constexpr`静态成员只能在类内初始化 }; // `non-const`的静态数据成员, 必须在类外进行初始化 int MyClass::stcMem = 42; int MyClass::stcMem2; // 类外定义 (零初始化&默认初始化), 初值为0 // `const`的静态数据成员, 可以在类外初始化,也可以在类内初始化 const int MyClass::cstStcMem2 = 33; ``` `constexpr` 只能用于"常量类型" > [!caution] 当需要使用 "`const` 整型或枚举类型静态数据成员" 地址时,必须进行类外定义! > > 类的**静态成员变量若为 `const` 整型或枚举类型,可在类内直接初始化**,而**==省略== "类外定义"** 。 > > 在此情况下,编译器会应用 "常量传播" 优化,在所有**使用到该 "静态成员变量==值=="** 的地方直接替换为常量值,从而 **==直接省略为该静态成员分配内存==**。 > > 但是,如果程序中有需要使用 "**该静态成员变量==地址==**" 的场景,例如**定义指向该 `const` 整型静态成员的指针或引用**时,则必须为其在 "**==类外定义==**"(若类内指定了初始值,则类外定义省略初始化项),**保证编译器为其分配内存**。 > > ```cpp > struct MyClass { > static const int var = 5; > static constexpr int var2 = 15; > }; > > const int MyClass::var; > // const int MyClass::var2; > > int main() { > cout << MyClass::var << endl; > cout << MyClass::var2 << endl; > const int& ref = MyClass::var; // 如果没有类外的`const int MyClass::var`定义, 则该句报编译错误. > const int& ref2 = MyClass::var2; // `constexpr`则只影响编译期初始化, 即使没有类外定义, 也会为其分配内存. > return 0; > } > ``` > > ^mku4i3 <br> #### 静态数据成员的定义说明 > [!tip] 通常,**类的静态数据成员的 "类外定义" 应当放在类对应的 `.cpp` 文件中,**防止重定义错误** 声明示例: ```cpp title:MyClass.h class MyClass { private: static int myVar; // 声明静态数据成员 static int myArray[5]; // 声明静态数据成员 // 类内初始化的静态成员变量将在编译时就被完成初始化 static const int sCstInt = 10; // const且为整型, 可在类内直接初始化; static constexpr double sCstExprVar = 3.14; // constexpr, 可在类内直接初始化; }; ``` ```cpp title:MyClass.cpp #include "MyClass.h" int MyClass::myVar; // 类外定义, 默认初始化, 初始化为0; int MyClass::myArray[5]; // 类外定义, 默认初始化, 所有元素初始化为0; // 可改为在定义时显式初始化 int MyClass::myVar = 5; // 类外定义,显式初始化, 初始值为5. int MyClass::myArray[5] = {1, 2, 3, 4, 5}; // 类外定义,显式初始化 // 只有在特定情况下需要对sCstInt进行类外定义 const int MyClass::staticConstInt; // 对于sCstExprVar通常不需要类外定义 ``` 示例二:静态数据成员可以是"**==不完全类型==**",**==可以是其本身所属的类类型==**。 ```cpp class Bar { public: // ... private: static Bar mem1; // 正确: 静态数据成员可以是不完全类型, 甚至是其自身所属类类型 Bar *mem2; // 正确: 指针类型可以是不完全类型 // Bar mem3; // error: 非静态数据成员必须是完整类型 }; ``` %% ![[_attachment/02-开发笔记/01-cpp/类相关.assets/IMG-类相关-329AE9DB583B0EA7C91BB692EB8A283C.png|450]] %% <br><br> # 类的可变数据成员(mutable) `mutable` 说明符声明一个**可变数据成员**(mutable data member)。 其特性包括: - **在类的 ==const 成员函数== 中可被修改**。 - **==常量对象==中的 "可变数据成员",在任何可访问该成员的地方都允许被修改** [!example] 可变数据成员在类的 `const` 成员函数中被修改的典型场景 缓存数据、延迟计算(惰性求值)、记录类实例的使用情况或状态(如引用计数、日志记录等)。 ## 使用示例 `mutable` 的典型使用场景: 1. **缓存数据**(在**提供只读数据**的 `const` 成员函数中,若**数据不存在则先计算并缓存**,否则直接返回) 2. **状态追踪**(如引用计数、日志记录等) 3. **延迟计算**(惰性求值) 示例一:缓存数据[^1] (Item 16) ```cpp class Polynomial { public: using RootsType = std::vector<double>; RootsType roots() const { std::lock_guard<std::mutex> g(m); // 使用互斥量, 保证线程安全. if (!rootsAreValid) { // 缓存不可用, 则进行一次计算, 并用rootVals存储. ... rootsAreValid = true; } return rootVals; } private: mutable std::mutex m; mutable bool rootsAreValid { false }; mutable RootsType rootsVals {}; }; ``` 示例二:追踪每个 Screen 的成员函数被调用了多少次: ```cpp class Screen { public: void some_member() const; // 允许在const成员函数中修改可变数据成员 void reset() ; // 与其他非常量成员一样, 能被正常修改 private: mutable size_t access_ctr; // 可变数据成员 }; void Screen::some_member() const { // const成员函数中修改mutable数据成员 ++access_str; // 保存一个计数值, 用于记录成员函数被调用的次数 }; void Screen::reset() { // 非const成员函数中也可以修改mutable成员 access_ctr = 0; } ``` <br><br> # Buffer ## 闪念 > sudden idea ## 候选资料 > Read it later # ♾️参考资料 # Footnotes [^1]: 《Effective Modern C++》