跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C++

C++ 构造函数初始化列表详解

C++ 构造函数初始化列表是在构造函数体执行前对成员变量进行初始化的机制。它区别于构造函数体内的赋值操作,初始化发生在对象内存分配后,而赋值在函数体内。初始化列表对于引用成员、const 成员及没有默认构造函数的类类型成员是必须的。C++11 支持类内成员初始化作为兜底值。成员变量初始化顺序取决于声明顺序而非列表顺序。无论是否显式声明,所有成员都会经历初始化过程,内置类型若未初始化则为垃圾值。

微码行者发布于 2026/2/9更新于 2026/6/528 浏览
C++ 构造函数初始化列表详解

一、构造函数初始化列表

在 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
    SuperCar myCar(currentSpeed, 300, 500);
    return 0;
}

五、类内成员初始化

C++11 允许在声明成员变量时直接指定默认值,这些默认值主要用于未被显式列入初始化列表的成员变量。

它的核心作用就是为成员变量提供一个'保底值'(备胎)。

使用条件:如果构造函数在冒号后面显式提到了这个变量,那么类内写的那个缺省值就会被直接忽略,只有当构造函数没提这个变量时,编译器才会去用那个缺省值。

代码实测:

#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 a=10,int b=20) {
        // 此时 _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;
}

引入该特性的优势:在 C++11 之前,若存在多个构造函数且某个成员变量(如 _init)需要在所有构造函数中初始化为 0,我们不得不在每个构造函数中重复编写 : _init(0)。

C++98 的痛苦写法:

class OldStyle {
    int x;
    int y;
public:
    // 必须重复写 : x(0), y(0)
    OldStyle() : x(0), y(0) {} //x=0 y=0
    OldStyle(int a) : x(a), y(0) {} //x=a,y=0
    OldStyle(int a, int b) : x(a), y(b) {} //x=a,y=b
};

C++11 的优雅写法:

class NewStyle {
    int x = 0; // 写一次,到处通用
    int y = 0;
public:
    NewStyle() {} // x=0 y=0
    NewStyle(int a) : x(a) {} // x=a y=0
    NewStyle(int a, int b) : x(a), y(b) {} //x a y=b
};

六、初始化顺序

类成员变量的初始化顺序仅取决于它们在类中的声明顺序,与初始化列表中的排列顺序无关,建议将初始化列表的顺序与成员变量的声明顺序保持一致。

**代码示例:**我们想把 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;
}

打印结果如下所示:

文章配图

目录

  1. 一、构造函数初始化列表
  2. 二、语法格式
  3. 三、核心区别:初始化与赋值
  4. 四、必须用初始化列表的成员
  5. 4.1 引用成员
  6. 4.2 const 成员变量
  7. 4.3 没有默认构造函数的类类型变量
  8. 五、类内成员初始化
  9. 六、初始化顺序
  10. 七、初始化列表总结
  • 💰 8折买阿里云服务器限时8折了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • 基于 Kronos AI 模型的股票预测系统实战与 Streamlit 应用
  • 贪心算法:674.最长连续递增序列
  • Visual Studio 中 GitHub Copilot 隐私设置与数据共享控制
  • SDXL Prompt Styler 工具使用指南:优化 AI 绘画提示词
  • Java 后端实习复盘:企业级项目实战与核心代码解析
  • 基于 Python Django Vue3 的网上鲜花商城系统设计与实现
  • C++物理引擎碰撞精度优化:核心算法与性能平衡策略
  • 飞算 JavaAI:Java 智能开发助手核心功能解析
  • JavaScript 原生实现图片轮播图
  • 2026 年测试工程师必备的 10 款免费开源 AI 工具
  • MediaPipe Web 端接入实战:从 CDN 到工程化落地
  • 边缘计算实战:基于 LLaMA-Factory 微调模型部署至 Jetson
  • PyCharm 创建 Python 虚拟环境
  • 本地部署 ESPHome 智能家居方案及外网访问配置
  • Stable Diffusion 扩散模型原理与 PyTorch 实现
  • 基于Vector工具的车载诊断协议测试实现
  • 学术论文降重与去除 AIGC 痕迹的技术方案分析
  • JavaScript 中 this 的绑定机制与用法详解
  • JSP 基础:深入理解前后端交互与核心对象
  • Java 详解:局部变量与成员变量的区别

相关免费在线工具

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online

  • JSON 压缩

    通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online

  • JSON美化和格式化

    将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online