c++构造函数初始化列表

初始化列表使用示例

话不多说,直接上一段最简单的代码

cpp 复制代码
#include <iostream>

class Point{ 
private: 
    int x; 
    int y; 
public: 
    Point(int i = 0, int j = 0): x(i), y(j) {} 
    int getX() const { return x; } 
    int getY() const { return y; } 
};

int main()
{
    Point p(3,4);
    std::cout << "Point x=: " << p.getX() << std::endl;
    std::cout << "Point y=: " << p.getY() << std::endl;
    return 0;
}

看一下私有变量x和y

Point(int i = 0, int j = 0): x(i), y(j) {}

给成员变量x和y传参?这就是构造函数初始化列表

运行结果:

为什么需要引入这个写法???

如果改成在函数体内赋值

cpp 复制代码
Point(int i = 0, int j = 0) {
    x = i;   // 这是赋值,不是初始化
    y = j;
}

看起来结果一样,但底层过程不同。引入初始化列表主要有以下几个原因:

原因一. 性能:避免"先默认构造,再赋值"

对于 int 这种内置类型,两种写法差别不大。但对于类类型的成员,差别巨大,下面看一个示例代码

InitializerTest.cpp只是简单的初始化了两个对象

cpp 复制代码
#include <iostream>
#include "WithInitializerList.h"
#include "WithoutInitializerList.h"

// 用日志观察成员对象的构造过程。
int main() {
    std::cout << "with initializer list:\n";
    WithInitializerList a;

    std::cout << "\nwithout initializer list:\n";
    WithoutInitializerList b;

    return 0;
}

WithInitializerList.h

cpp 复制代码
#ifndef WITH_INITIALIZER_LIST_H
#define WITH_INITIALIZER_LIST_H
#include "Number.h"
class WithInitializerList {
public:
    WithInitializerList() : number(10), id(1) {}
    ~WithInitializerList() {
        std::cout << "~WithInitializerList()\n";
    }
private:
    Number number;
    const int id;
};
#endif

WithoutInitializerList.h

cpp 复制代码
#ifndef WITHOUT_INITIALIZER_LIST_H
#define WITHOUT_INITIALIZER_LIST_H
#include "Number.h"
class WithoutInitializerList {
public:
    WithoutInitializerList() : id(1) {
        number = Number(10);
    }

    ~WithoutInitializerList() {
        std::cout << "~WithoutInitializerList()\n";
    }
private:
    Number number;
    const int id;
};
#endif

Number.h的代码会稍微多一点

cpp 复制代码
#ifndef NUMBER_H
#define NUMBER_H
#include <iostream>
class Number {
public:
    Number() : value(0), id(nextId) {
        nextId++;
        std::cout << "Number()      id=" << id << " this=" << this << "\n";
    }

    Number(int value) : value(value), id(nextId) {
        nextId++;
        std::cout << "Number(int)   id=" << id << " this=" << this
                  << " value=" << value << "\n";
    }

    ~Number() {
        std::cout << "~Number()     id=" << id << " this=" << this << "\n\n";
    }

private:
    int value;
    int id;
    inline static int nextId = 1;
};
#endif

运行结果如下

可以看到,相比用构造函数初始化列表,普通的构造方法居然构造了两次对象。而且代码是这么写的

cpp 复制代码
WithoutInitializerList() {
        number = Number(10);
    }

明明看着是显式调用了Number带参的构造方法,但是却调用了一次Number无参的构造方法!

这也是为什么会构造了两次对象的原因

那为什么会调用了一次无参的构造方法?要知道我们如果初始化一个类,正常在一个main方法这么写number = Number(10)。在同时存在有参和无参的构造方法时,是不会调用一次Number的无参构造方法的!!!!!

但是我们的场景是,在WithoutInitializerList的构造方法里面去初始化Number

编译器要求:执行WithoutInitializerList构造函数体时,成员 number 已经必须是一个完整对象了

所以,编译器隐含执行Number number; 先构造成员,调用 Number()。然后才执行你的构造函数体number = Number(10); // 创建临时 Number(10),再赋值

这里的 = 是赋值运算符,不是构造语法。它的意思是:

把一个已经存在的 Number 对象的内容,赋给另一个已经存在的 Number 对象

而 Number(10) 本身确实调用了带参构造函数,但它创建的是一个临时对象:

Number temporary(10);

number.operator=(temporary);

它不是在"构造 number"。

要在 number 第一次诞生时就调用 Number(int),必须写在外层类的初始化列表中

从运行结果来看,相比于初始化列表。普通改造在一个类存在类类型的成员的时候,多了临时对象的构造和销毁等步骤。出于性能考虑。当然是选择构造函数初始化列表比较好

原因二: 有些成员只能在初始化列表中初始化,无法在函数体内赋值

const 成员

"expression must be a modifiable lvalue" ,意思是:"表达式必须是一个可修改的左值。"

可以简单理解左值为可取地址的变量,这里int被const修饰了意味着x不能修改了,当然你可以直接这么写:

const int x = 10;

但是如果没有初始化列表。你const变量就没办法更改它的值了。初始化列表给你提供了这么一种方式,让你的变量既可以是const,也不需要你声明就赋值,可以等到初始化类对象的时候再去指定const变量的值

引用成员

"B::B(int &r)" provides no initializer for:
B.h(10, 14): reference member "B::ref"

意思是:构造函数 B::B(int &r) 没有为引用成员 B::ref 提供初始化

看起来有点奇怪,现在看着是你不给我初始化呀

**其实,在cpp中,**引用必须初始化。在这里想这么用本来使用引用的姿势就是错的

你可以直接在声明引用的时候就去初始化好

所以,对于引用成员,其实也和const 成员一样,初始化列表给我们提供了一种延迟和动态修改本来不可修改的变量的方式

没有默认构造函数的类成员

改造一下上面的B.h

no default constructor exists for class "A",意思是说A没有默认构造函数

为什么需要A有默认构造函数呢

因为假设你在外部调用B(A aaaa)这个构造方法,C++必须先想办法构造成员A

怎么构造 A?需要调用 A() ,但是 A 没有 A()所以只能报编译错误了

你可能会问:为什么不能先进入函数体?

因为 C++ 的构造顺序是固定的:

1.基类构造 2. 成员变量构造 3.执行构造函数体

一定是这个顺序。

所以:

cpp 复制代码
B(A aaaa) {
    a = aaaa;
}

不能理解成:

复制代码
1.创建 B  2. 进入 B 构造函数  3. a = aaaa  4. 再创建 A
复制代码
而是:
1. 创建 B  2. 创建 A a  3. 进入 B 构造函数体  4. 执行 a = aaaa

问题就出在第②步。

既然你说需要A有默认构造函数,那我就加上吧

服啦,又报了另一个错,说function "A::operator=(const A &)" (declared implicitly) cannot be referenced -- it is a deleted function

意思是编译器生成的 A::operator= 被删除了,导致无法赋值了

why?

原来,class A里面的const int x = 0;编译器担心你在B(A aaaa)这里传进去的A,由于A可以使用初始化列表修改x的值,那你再把aaaa赋值给a的时候,相当于通过赋值改变了一个const的值

解决办法很简单,将const int x = 0;的const去掉就好了

基类没有默认构造函数

cpp 复制代码
class Derived : public Base {
public:
    Derived(int v) : Base(v) {}  // 必须显式调用基类构造
};

原因三:语义正确性

初始化列表体现的是"初始化 "语义,而函数体内赋值体现的是"赋值 "语义。对于某些类型,这两者有本质区别(如 const、引用、无默认构造的类型)。

原因四:成员初始化顺序的确定性

cpp 复制代码
class A {
    int a, b;
public:
    A() : b(1), a(2) {}  // 实际按 a 再 b 的顺序初始化,可能有警告
};

原因五: 避免临时对象和潜在错误

对于需要在构造时立即确定初值的场景(如 const 成员、引用绑定),初始化列表是唯一正确的方式,避免了使用未初始化值的风险。

总结

原因 说明
效率 避免"默认构造 + 赋值"的额外开销
语法强制 const、引用、无默认构造的成员必须用
语义清晰 区分"初始化"与"赋值"
顺序确定 明确成员初始化顺序
正确性 保证对象构造时成员即处于有效状态

因此,C++ 中建议优先使用初始化列表,即使对内置类型也是如此,以保持一致性并避免遗漏必须初始化的成员。

相关推荐
努力实干3 小时前
面向对象设计原则——C++
开发语言·c++·数码相机
syagain_zsx6 小时前
jsoncpp 使用指南:编译链接、Json::Value 取值与高频坑
c++·json·库·http格式
布莱克6057 小时前
理解 = delete 与 = default:两种赋值语义的差异
c++
小小、码农7 小时前
[特殊字符]【C++11】类的新功能全面解析:移动构造、移动赋值、default、delete、final 与 override
linux·开发语言·网络·c++
库玛西8 小时前
外部排序全解析:败者树、置换-选择排序与最佳归并树
c语言·数据结构·c++·笔记·考研·算法
General_G10 小时前
创建型模式—原型(Prototype)模式
c++·设计模式·原型模式
syagain_zsx10 小时前
protobuf 实战:从 .proto 到 C++ 序列化的完整链路
c++·学习·程序人生·谷歌·protobuf
welfare96210 小时前
新号别搞:C++_list
开发语言·c++
xiaoye-duck10 小时前
《C++ 项目:手写大模型接入 SDK》云端 API 调用基础与 Apifox 调试实战
网络·c++·语言模型
坤坤子吖11 小时前
Linux 线程同步与互斥——条件变量与线程同步
linux·开发语言·c++·笔记·学习