2026/4/18 9:50:38
网站建设
项目流程
电子商务成功的网站,用cms织梦做网站图文教程,好一点的网站建设公司,wordpress主题 v2ex前言#xff1a; 本文将继续深入探讨类与对象的进阶特性#xff0c;在前文介绍的构造函数、拷贝构造函数、析构函数和操作符重载基础上#xff0c;重点讲解初始化列表 一、构造函数初始化列表 在 C 中#xff0c;构造函数初始化列表是一种在构造函数体执行之前#xff0c;…前言本文将继续深入探讨类与对象的进阶特性在前文介绍的构造函数、拷贝构造函数、析构函数和操作符重载基础上重点讲解初始化列表一、构造函数初始化列表在 C 中构造函数初始化列表是一种在构造函数体执行之前对类成员变量进行初始化的机制。二、语法格式初始化列表位于构造函数的参数列表之后函数体的大括号之前以冒号:开头成员之间用逗号,分隔。语法形式构造函数 (函数参数1函数参数2) : 成员1(参数1), 成员2(参数2) { ... }代码示例class MyClass { public: // 语法构造函数(参数) : 成员1(值), 成员2(值) { ... } MyClass(int x, double y) : a(x), b(y) { // 此时 a 和 b 已经被初始化了 } private: int a; double b; };注意事项每个成员变量在初始化列表中只能出现⼀次语法理解上初始化列表可以认为是每个成员变量定义初始化的地⽅。三、核心区别初始化与赋值掌握初始化列表的核心在于明确初始化与赋值的区别。在使用初始化列表之前构造函数中对成员变量的操作实际上是赋值而初始化列表才是真正的初始化过程。场景 A构造函数体内赋值class MyClass { public: //你以为你没写初始化列表 MyClass(string s) { _name s; } // 编译器实际上看到的是 MyClass(string s): _name() { _name s; } private: string _name; };时间轴发生的事情初始化阶段隐式 编译器发现你没显示在列表里写_name(s)编译器也会生成隐式列表_name()然后它悄悄调用 string 的默认构造函数。此时 _name 已经诞生了它是一个空字符串 。进入函数体 { 开始执行用户代码。赋值阶段 执行 _name s; 调用 string 的赋值运算符。此时 把刚才那个空字符串的内容清掉换成 s 的内容。总结 先生出一个“空壳”然后再往里“填充”。场景 B初始化列表class MyClass { public: MyClass(string s) :_name(s) {} private: string _name; };时间轴发生的事情初始化阶段显式 编译器看到列表里有 _name(s)直接调用 string 的拷贝构造函数。此时 _name 在诞生的那一刻就直接拥有了 s 的值。进入函数体 { 执行用户代码 该段代码为空。总结 出生即完美一步到位。验证上述逻辑#include iostream using namespace std; // 1. 定义一个用于测试的类 class MyString { public: // A. 默认构造函数 MyString() { cout [底层] 默认构造 (创建空对象) endl; } // B. 拷贝构造函数 // 当你用一个已有的对象去创建一个新对象时调用这个 MyString(const MyString other) { cout [底层] 拷贝构造函数被调用 (拷贝了一个新对象) endl; } // C. 赋值运算符 // 当你把一个对象的值改写给另一个已存在的对象时调用这个 MyString operator(const MyString other) { cout [底层] 赋值运算符被调用 (覆盖旧值) endl; return *this; } }; // 2. 使用初始化列表的类 class InitListTester { MyString m_str; public: // 这里的 : m_str(s) 就是在让 m_str 出生 // 我们传入 const MyString s 避免传参时产生额外的拷贝干扰 InitListTester(const MyString s) : m_str(s) { cout --- 进入 InitListTester 构造函数体 --- endl; } }; // 3. 使用函数体内赋值的类 class AssignmentTester { MyString m_str; public: AssignmentTester(const MyString s) { cout --- 进入 AssignmentTester 构造函数体 --- endl; m_str s; // 这里是赋值 } }; int main() { // 先准备一个源对象 cout 准备工作创建一个源对象 source endl; MyString source; cout \n 验证 1初始化列表 endl; // 预测这里应该直接调用拷贝构造不会有默认构造也不会有赋值 InitListTester t1(source); cout \n 验证 2函数体内赋值 endl; // 预测先默认构造进函数体后再赋值 AssignmentTester t2(source); return 0; }打印结果如下第一部分初始化列表 验证 1初始化列表 (Init List) [底层] 拷贝构造函数被调用 (拷贝了一个新对象)--- 进入 InitListTester 构造函数体 ---注意它没有打印“默认构造”也没有打印“赋值运算符”。这证明了m_str(s) 这一行代码直接利用 s 为蓝本通过拷贝构造函数生出了 m_str。第二部分函数体内赋值 验证 2函数体内赋值 [底层] 默认构造 (创建空对象)--- 进入 AssignmentTester 构造函数体 ---[底层] 赋值运算符被调用 (覆盖旧值)注意先调用了“默认构造”因为成员必须先存在 - 然后进入函数体 - 最后才调用“赋值运算符”。这证明了编译器发现你没在列表里写 m_str(s)编译器也会生成隐式列表m_str()它会悄悄调用 MyString 的默认构造函数然后再执行赋值运算符操作。小结无论是否显式声明初始化列表每个构造函数都包含初始化过程。四、必须用初始化列表的成员首先建立一个核心概念C 对象的生命周期时间轴。时间轴A、内存分配B、初始化列表阶段这是成员变量“出生”的时刻C.、构造函数体阶段这是对成员变量进行“修改/赋值”的时刻在 C 中初始化 和 赋值 是两个完全不同的步骤1.成员变量通过初始化列表进行默认初始化这个过程初始化列表进行默认初始化发生在对象内存分配之后、构造函数体执行之前。2.当程序执行到构造函数体 { ... } 时实际上是在进行赋值操作如果成员变量进入了构造函数体说明它已经完成了默认初始化过程。4.1 引用成员因为引用不能为空所以对于引用成员而言必须在定义时绑定一个对象且一旦绑定不可更改即不能重新指向别的对象,如果不在初始化列表中绑定进入函数体时引用就是“未绑定”状态这是违法的。错误演示在构造函数体内赋值class Referencer { private: int m_ref; // 引用成员 public: Referencer(int target) { // 错误 // 此时 m_ref 已经“出生”了但没有绑定对象。 // 下面这行代码实际上是“赋值”而不是“初始化”。 m_ref target; } }; // 报错信息通常为error C2530: “Referencer::m_ref”: 必须初始化引用正确演示使用初始化列表class Referencer { public: // 正确在 m_ref “出生”的那一刻直接将其绑定到 target Referencer(int target) : m_ref(target) { // 函数体可以是空的 } private: int m_ref; };4.2 const 成员变量const 意味着“只读”它的值必须在创建时确定之后不能被修改。如果在构造函数体内赋值实际上是在试图修改一个已经初始化过的常量这是违法的。错误演示在构造函数体内赋值class ConstHolder { private: const int m_val; // 常量成员 public: ConstHolder(int x) { // 错误 // 此时 m_val 已经“出生”了通常会被初始化为随机垃圾值且属性为“不可修改”。 // 下面这行试图修改一个只读变量。 m_val x; } }; // 报错信息通常为error C2789: “ConstHolder::m_val”: 必须初始化常量限定类型的对象正确演示使用初始化列表class ConstHolder { private: const int m_val; public: // 正确在 m_val “出生”的同时赋予初值 ConstHolder(int x) : m_val(x) { //函数体为空 } };4.3 没有默认构造函数的类类型变量这一点最容易让人产生困惑,当类 B 包含类 A 的对象成员时在创建 B 的实例时编译器会优先自动创建 A 的成员对象。如果 B 的初始化列表中没有指定如何初始化 A编译器会默认调用 A 的无参构造函数此时若 A 未定义无参构造函数就会导致编译错误。错误演示在初始化列表中没有指定如何初始化 Engine且Engine未定义无参构造函数编译器无法调用导致编译错误。class Engine { public: // 只有带参构造没有 Engine() 默认构造 Engine(int power) :_power(power) { } private: int _power; }; class Car { private: Engine m_engine; // Car 包含 Engine public: Car(int p) :m_engine(p) {} };正确演示显式调用构造函数#include iostream using namespace std; class Engine { public: // 只有带参构造没有 Engine() 默认构造 Engine(int power) :_power(power) { } private: int _power; }; class Car { private: Engine m_engine; // Car 包含 Engine public: //在初始化列表显示指定初始化方式 Car(int p) :m_engine(p) {} };实战演示#include iostream using namespace std; class Engine { public: // 只有带参构造没有 Engine() 默认构造 Engine(int power) :_power(power) { } private: int _power; }; class SuperCar { private: int m_refSpeed; // 1. 引用 const int m_maxSpeed; // 2. const Engine m_engine; // 3. 无默认构造的类成员 public: // 初始化列表必须同时处理这三个刺头 SuperCar(int speedMetric, int maxS, int power) : m_refSpeed(speedMetric) // 绑定引用 ,m_maxSpeed(maxS) // 初始化 const ,m_engine(power) // 初始化类成员 {} }; int main() { int currentSpeed 0; // 实例化 SuperCar myCar(currentSpeed, 300, 500); return 0; }五、类内成员初始化C11 允许在声明成员变量时直接指定默认值这些默认值主要用于未被显式列入初始化列表的成员变量。它的核心作用就是为成员变量提供一个“保底值”备胎。使用条件如果构造函数在冒号后面显式提到了这个变量那么类内写的那个缺省值就会被直接忽略只有当构造函数没提这个变量时编译器才会去用那个缺省值。代码实测#include iostream using namespace std; class Settings { public: // 构造函数 1什么都不写 // 结果_v1 和 _v2 都会使用上面的缺省值 Settings() { // 此时 _v1 50, _v2 80 } // 构造函数 2只初始化 _v1 // 结果_v1 使用参数 a_v2 继续使用缺省值 80 Settings(int a) : _v1(a) { // 此时 _v1 a, _v2 80 } // 构造函数 3全部覆盖 // 结果两个缺省值都被忽略 Settings(int a, int b) : _v1(a), _v2(b) { // 此时 _v1 a, _v2 b } void print() { cout _v1: _v1 _v2: _v2 endl; } private: //成员变量进行声明 //在这里给缺省值 int _v1 50; int _v2 80; }; int main() { Settings s1; // 输出: _v1: 50 _v2: 80 Settings s2(10); // 输出: _v1: 10 _v2: 80 Settings s3(10, 20);// 输出: _v1: 10 _v2: 20 s1.print(); s2.print(); s3.print(); return 0; }温馨提示如果函数参数上带有缺省值效果与上述一致如果构造函数在冒号后面显式提到了这个变量那么类内写的那个缺省值就会被直接忽略用显示的初始化方式进行初始化只有当构造函数没提这个变量时编译器才会去用那个缺省值。代码示例尽管函数参数带有缺省值但是初始化列表没有显示初始化方式编译器只会去用成员变量声明处的缺省值。#include iostream using namespace std; class Settings { public: // 函数参数带有缺省值初始化列表没有显示初始化方式 // 结果_v1 和 _v2 都会使用成员变量的缺省值 Settings(int a10,int b20) { // 此时 _v1 50, _v2 80 } void print() { cout _v1: _v1 _v2: _v2 endl; } private: //成员变量进行声明 //在这里给缺省值 int _v1 50; int _v2 80; }; int main() { Settings s1; // 输出: _v1: 50 _v2: 80 s1.print(); return 0; }引入该特性的优势在 C11 之前若存在多个构造函数且某个成员变量如 _init需要在所有构造函数中初始化为 0我们不得不在每个构造函数中重复编写 : _init(0)。C98 的痛苦写法class OldStyle { int x; int y; public: // 必须重复写 : x(0), y(0) OldStyle() : x(0), y(0) {} //x0 y0 OldStyle(int a) : x(a), y(0) {} //xa,y0 OldStyle(int a, int b) : x(a), y(b) {} //xa,yb };C11 的优雅写法class NewStyle { int x 0; // 写一次到处通用 int y 0; public: NewStyle() {} // x0 y0 NewStyle(int a) : x(a) {} // xa y0 NewStyle(int a, int b) : x(a), y(b) {} //xa yb };六、初始化顺序类成员变量的初始化顺序仅取决于它们在类中的声明顺序与初始化列表中的排列顺序无关建议将初始化列表的顺序与成员变量的声明顺序保持一致。代码示例我们想把x存给m_b然后把m_b的值赋给m_a。#include iostream class Trap { public: // 初始化列表故意把 m_b 写在前面 // 你的意图先 m_b x然后 m_a m_b Trap(int x) : m_b(x), m_a(m_b) { cout m_a m_a endl; cout m_b m_b endl; } private: // 声明顺序m_a 先声明m_b 后声明 int m_a; int m_b; }; int main() { Trap t(10); return 0; }打印结果如下编译器实际生成的执行步骤如下①第一步初始化 m_a编译器看一眼声明发现 m_a 排第一。它去看初始化列表找到了 : m_a(m_b)。灾难发生 此时 m_b 还没有被初始化它里面是内存里的随机垃圾值。结果m_a 被初始化为垃圾值。②第二步初始化 m_b编译器发现 m_b 排第二。它去看初始化列表找到了 : m_b(x)。结果m_b 被正确初始化为 10。③第三步进入构造函数体打印 m_a (垃圾值) 和 m_b (10)。总结这就充分的证明了类成员变量的初始化顺序仅取决于它们在类中的声明顺序与初始化列表中的排列顺序无关。修改后的安全代码class Safe { public: // 安全写法两个都直接用参数 x 初始化互不依赖 Safe(int x) : m_a(x), m_b(x) {} private: int m_a; int m_b; };为什么要这样设计这是为了保证析构顺序的确定性在 C 中对象的析构顺序必须严格是构造顺序的逆序。如果不按声明顺序构造而是按列表顺序构造程序员 A 写了 Class(x) : a(x), b(x) {}程序员 B 写了 Class(x) : b(x), a(x) {}同一个类竟然会有两种不同的构造顺序那析构函数该按什么顺序销毁成员呢这会导致混乱。因此C 规定声明顺序是唯一的真理这样析构函数就可以无脑地按照“声明顺序的逆序”来清理资源。七、初始化列表总结关于初始化列表的要点总结①所有构造函数都包含初始化列表无论是否显式声明②每个成员变量都会通过初始化列表进行初始化不论是否在列表中显式指定。核心观念初始化列表不是“选修课”而是成员变量出生的“必经之路”。简单理解无论你写不写冒号:无论你写不写初始化列表每一个成员变量在进入构造函数体{}之前都必须在初始化列表这个阶段完成初始化。我们可以把这个过程想象成一个“三级过筛”的决策流程图。假设编译器正在初始化成员变量m_var流程如下第一关查看初始化列表问 构造函数的冒号后面有没有写: m_var(x)是 停止检查直接使用 x 初始化这是最高优先级。否 进入第二关。第二关查看类内声明缺省值问 在class定义里有没有写Type m_var y;是 停止检查使用缺省值 y 初始化。否 进入第三关。第三关兜底处理最危险的阶段问m_var是什么类型情况 A自定义类型类对象调用它的默认构造函数Type()。风险提示如果该类型没有默认构造函数编译报错。情况 B内置类型int, double, 指针等不处理。它里面的值是内存里残留的随机垃圾值。风险提示这是 C 中无数莫名其妙 Bug 的根源代码示例#include iostream using namespace std; class Inner { public: Inner() { cout Inner: 默认构造 endl; } Inner(int x) { cout Inner: 带参构造 x endl; } }; class MyClass { public: // ------ 构造函数 ------ MyClass(int val) : m_explicit(val) { // 此时所有成员都已经处理完毕 cout MyClass 构造函数体开始执行...endl; cout m_explicit: m_explicit (使用了列表值)endl; cout m_default: m_default (使用了缺省值)endl; cout m_garbage: m_garbage (未定义可能是乱码)endl; } private: // ------ 成员声明区域 ------ // 1. 有缺省值但在列表里被覆盖 int m_explicit 100; // 2. 有缺省值没在列表里将使用缺省值 int m_default 200; // 3. 自定义类型没缺省值没在列表 - 调默认构造 Inner m_obj; // 4. 内置类型没缺省值没在列表 - 【危险】随机值 int m_garbage; }; int main() { MyClass c(999); return 0; }打印结果如下所示既然看到这里了不妨关注点赞收藏感谢大家若有问题请指正。