C++提供了多种初始化语法,从C语言继承的传统方式到现代C++引入的统一初始化,每种方式都有其适用场景和特点。理解这些初始化方式的差异,有助于编写更安全、更清晰的代码。

一、声明、定义、初始化与赋值

在介绍具体语法前,先区分四个容易混淆的概念。声明(declaration)是告诉编译器变量的类型和名称,不一定分配存储空间,如extern int x;。定义(definition)则为变量分配存储空间,创建变量实体,一个变量只能被定义一次。初始化(initialization)是在定义变量的同时赋予初始值,发生在对象创建的那一刻。赋值(assignment)是对已存在的对象重新设置值,发生在对象创建之后。

1
2
3
4
5
extern int x;      // 声明(不分配空间)
int x; // 定义(分配空间,但未初始化)
int y = 10; // 定义 + 初始化
int z;
z = 20; // 赋值,不是初始化(对象已存在)

初始化与赋值的关键区别在于时机。初始化发生在对象创建时,是对象生命周期的起点;赋值发生在对象已存在之后,是修改已有对象的值。

二、初始化方式

理解这些概念后,来看C++标准定义的几种初始化方式。下表展示了各种语法在不同类型上的应用:

初始化方式 基本类型 数组 类对象 容器
默认初始化 int a; int a[3]; Point p; vector<int> v;
值初始化 int a{}; int a[3]{}; Point p{}; vector<int> v{};
拷贝初始化 int a = 10; int a[] = {1,2}; Point p = Point(1,2); vector<int> v = {1,2};
直接初始化 int a(10); 不支持 Point p(1, 2); vector<int> v(10, 1);
列表初始化 int a{10}; int a[]{1,2,3}; Point p{1, 2}; vector<int> v{1,2,3};

各初始化方式的含义如下:

  • 默认初始化:不提供初始值,语法为T obj;。对于基本类型的局部变量,值未定义;对于类类型,调用默认构造函数。

  • 值初始化:使用空的花括号或圆括号,语法为T obj{};T obj = T();。这里的花括号{}和圆括号()称为初始化器(initializer),是变量名后面用于指定初始值的语法结构。值初始化保证对象有一个确定的值——基本类型初始化为零,类类型调用默认构造函数。与默认初始化的关键区别在于:对于没有构造函数的类,值初始化会将所有成员清零。

  • 拷贝初始化:使用等号,语法为T obj = value;。从右侧的值创建对象,可能涉及隐式类型转换。

  • 直接初始化:使用圆括号,语法为T obj(args);。直接调用匹配的构造函数,不考虑explicit限制之外的隐式转换。

  • 列表初始化:使用花括号作为初始化器,语法为T obj{args};T obj = {args};。C++11引入的统一语法,适用于所有类型,并且能在编译期检测窄化转换错误。当花括号内有多个元素时,编译器会优先匹配接受initializer_list参数的构造函数。

从历史角度看,默认初始化、拷贝初始化(使用等号)以及数组和结构体的花括号初始化是从C语言继承的传统方式。C++在此基础上扩展了直接初始化(圆括号语法),并在C++11中引入了列表初始化(花括号且无需等号)和值初始化(空初始化器保证零值)。

接下来分别介绍这些初始化方式在不同类型上的具体用法。

三、基本类型的初始化

从最简单的int、double等基本类型开始,C++支持以下几种初始化方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 默认初始化:局部变量值未定义,全局/静态变量为零
int a;

// 拷贝初始化:使用=号
int b = 10;

// 直接初始化:使用圆括号
int c(10);

// 列表初始化(C++11):使用花括号
int d{10};
int e = {10};

// 值初始化:初始化为零值
int f{}; // f = 0
int g = {}; // g = 0

列表初始化相比其他方式有一个重要特性:禁止窄化转换。以下代码会产生编译错误或警告:

1
2
3
int x = 3.14;    // 允许,x = 3(发生截断)
int y{3.14}; // 错误:窄化转换
int z(3.14); // 允许,z = 3(发生截断)

四、数组的初始化

数组的默认初始化int arr[3];不提供初始值,此时元素的状态取决于存储位置:局部数组的元素值未定义,全局或静态数组的元素会被零初始化。其他初始化方式使用花括号语法:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 完全初始化
int arr1[3] = {1, 2, 3};
int arr2[3]{1, 2, 3};

// 部分初始化:未指定元素初始化为0
int arr3[5] = {1, 2}; // {1, 2, 0, 0, 0}

// 零初始化
int arr4[5] = {}; // 全部为0
int arr5[5]{}; // 全部为0

// 自动推导大小
int arr6[] = {1, 2, 3, 4}; // 大小为4

多维数组的初始化可以嵌套花括号,也可以平铺:

1
2
int matrix1[2][3] = {{1, 2, 3}, {4, 5, 6}};
int matrix2[2][3] = {1, 2, 3, 4, 5, 6}; // 等价写法

五、类对象的初始化

类对象的初始化方式与基本类型类似,但行为取决于类的构造函数定义。

构造函数与初始化列表

首先定义一个示例类,展示构造函数与成员初始化列表的写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Point {
public:
int x, y;

// 默认构造函数:使用成员初始化列表将x和y初始化为0
// 语法为 构造函数() : 成员1(值1), 成员2(值2), ... { 函数体 }
Point() : x(0), y(0) {}

// 带参构造函数:参数名与成员名相同时,初始化列表中的x(x)表示用参数x初始化成员x
// 这种写法合法,因为初始化列表的作用域规则会优先将括号外的x解析为成员变量
Point(int x, int y) : x(x), y(y) {}

// 也可以使用不同的参数名来避免混淆
// Point(int _x, int _y) : x(_x), y(_y) {}
};

初始化列表的优势

成员初始化列表相比在构造函数体内赋值有两个优势。

第一,对于const成员、引用成员和没有默认构造函数的类类型成员,必须使用初始化列表。这是因为这三种成员都要求在定义时就完成初始化:const成员一旦初始化就不能再被赋值,引用必须在创建时绑定到目标对象,而没有默认构造函数的类无法被”先创建再赋值”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Example {
const int id; // const成员
int& ref; // 引用成员
std::string name; // 有默认构造函数,可以不在初始化列表中

public:
// 正确:必须用初始化列表初始化id和ref
Example(int i, int& r) : id(i), ref(r), name("default") {}

// 错误:以下写法无法编译
// Example(int i, int& r) {
// id = i; // 错误:不能给const赋值
// ref = r; // 错误:引用必须在初始化时绑定
// }
};

第二,对于类类型成员,使用初始化列表避免了先默认构造再赋值的额外开销。进入构造函数体之前,所有成员都会被初始化(要么通过初始化列表,要么调用默认构造函数)。如果在函数体内赋值,实际上执行了两步操作:

1
2
3
4
5
6
7
8
9
10
11
class Container {
std::vector<int> data;
public:
// 方式一:初始化列表,直接用{1,2,3}构造data
Container() : data{1, 2, 3} {}

// 方式二:函数体内赋值,先调用vector默认构造函数创建空data,再赋值
// Container() {
// data = {1, 2, 3}; // 多了一次默认构造的开销
// }
};

初始化方式

有了这个类定义,就可以使用多种方式创建对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 默认初始化:调用默认构造函数Point()
Point p1;

// 值初始化:使用空括号或空花括号
Point p2{}; // 调用默认构造函数
Point p3 = {}; // 同上

// 拷贝初始化
Point p4 = Point(1, 2);

// 直接初始化
Point p5(3, 4);

// 列表初始化
Point p6{5, 6};
Point p7 = {7, 8};

值初始化Point p{}与默认初始化Point p的区别在于:对于没有用户定义构造函数的类,值初始化会将所有成员零初始化,而默认初始化不会。例如:

1
2
3
4
5
6
7
8
struct Data {
int x;
double y;
};

Data d1; // 默认初始化:x和y的值未定义
Data d2{}; // 值初始化:x = 0, y = 0.0
Data d3(); // 注意:这是函数声明,不是对象定义(Most Vexing Parse)

聚合初始化

前面介绍的Point类有用户定义的构造函数,花括号中的值会作为参数传递给匹配的构造函数。但对于没有构造函数的简单结构体,花括号初始化遵循另一套规则——聚合初始化。

聚合类型指的是数组或满足特定条件的类(无用户定义构造函数、无私有/保护成员等)。对聚合类型使用花括号初始化时,编译器按成员声明顺序依次初始化各个成员。列表初始化是统一的语法形式,编译器会根据类型特征自动选择合适的初始化机制。

1
2
3
4
5
6
7
8
struct Config {
int timeout;
bool enabled;
double ratio;
};

Config cfg1 = {100, true, 0.5}; // C++98聚合初始化语法
Config cfg2{200, false, 0.8}; // C++11列表初始化语法,执行聚合初始化

指定初始化器

C++20引入了指定初始化器(Designated Initializers),允许通过成员名显式指定初始值,提升代码可读性并减少因成员顺序变化导致的错误:

1
2
3
4
5
6
7
Config cfg3{.timeout = 300, .enabled = true, .ratio = 0.9};

// 可以跳过中间成员,未指定的成员被值初始化
Config cfg4{.timeout = 500}; // enabled = false, ratio = 0.0

// 指定初始化器必须按成员声明顺序书写
// Config cfg5{.ratio = 0.5, .timeout = 100}; // 错误:顺序不对

指定初始化器有几个限制:初始化顺序必须与成员声明顺序一致;不能与普通初始化器混用;仅适用于聚合类型。尽管如此,它在配置结构体、选项参数等场景下非常实用,能够清晰表达每个字段的含义。

六、容器的初始化

vectormapstring等标准库容器本质上是类模板的实例化结果。例如vector<int>是模板vector<T>int实例化后得到的具体类,因此容器具备类的所有特性——有构造函数、析构函数和成员函数。容器的初始化方式与普通类对象一致,同时还支持initializer_list语法来指定初始元素。

初始化方式

标准库容器支持多种初始化方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <vector>
#include <map>
#include <string>

// 默认初始化与值初始化:创建空容器
std::vector<int> v1; // 默认初始化
std::vector<int> v2{}; // 值初始化,效果相同

// 直接初始化:指定大小和初始值
std::vector<int> v3(10); // 10个元素,值初始化为0
std::vector<int> v4(10, 1); // 10个元素,每个值为1

// 列表初始化:指定元素内容
std::vector<int> v5 = {1, 2, 3, 4, 5};
std::vector<int> v6{1, 2, 3, 4, 5};

// map的列表初始化
std::map<std::string, int> scores = {
{"Alice", 90},
{"Bob", 85},
{"Charlie", 92}
};

对于标准库容器,默认初始化与值初始化的效果相同,都会创建空容器。这与基本类型不同(int a;值未定义,int a{};保证为0)。原因在于vector等容器是类类型,其类定义中显式声明了默认构造函数。根据C++标准,当类显式定义了默认构造函数时,默认初始化和值初始化都会调用该构造函数,而容器的默认构造函数会将内部状态正确初始化为空容器。

initializer_list与构造函数选择

使用圆括号和花括号初始化vector时,行为可能出乎意料:

1
2
std::vector<int> v1(10, 1);   // 10个元素,每个值为1
std::vector<int> v2{10, 1}; // 2个元素:10和1

v1调用的是vector(size_t count, const T& value)构造函数,创建指定数量的元素。而v2却只有两个元素,这是因为花括号触发了不同的匹配规则。

这种差异源于std::initializer_list,这是C++11引入的轻量级模板类,定义在<initializer_list>头文件中。当编译器遇到花括号初始化时,会将其中的元素打包成一个initializer_list对象。如果类定义了接受initializer_list参数的构造函数,花括号初始化会优先匹配该构造函数。

initializer_list内部只存储指向元素的指针和长度,是只读的轻量容器,其生命周期与创建它的花括号表达式绑定。自定义类型也可以接受initializer_list参数:

1
2
3
4
5
6
7
8
9
#include <initializer_list>

void print(std::initializer_list<int> list) {
for (int x : list) {
std::cout << x << " ";
}
}

print({1, 2, 3, 4}); // 编译器自动构造initializer_list

七、特殊场景的初始化

除了普通变量定义,动态内存分配、类成员定义、auto类型推导等场景也涉及初始化语法。这些场景遵循相似的规则,但各有需要注意的细节。

动态分配

前面介绍的初始化语法同样适用于new表达式。使用new在堆上分配内存时,在类型后面添加相应的初始化器即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 默认初始化
int* p1 = new int; // 值未定义
Point* obj1 = new Point; // 调用默认构造函数

// 值初始化
int* p2 = new int(); // *p2 = 0
int* p3 = new int{}; // *p3 = 0

// 直接初始化
int* p4 = new int(42);
Point* obj2 = new Point(1, 2);

// 列表初始化
int* p5 = new int{42};
Point* obj3 = new Point{3, 4};

// 数组初始化
int* arr1 = new int[5](); // 全部为0
int* arr2 = new int[5]{1, 2, 3}; // {1, 2, 3, 0, 0}

成员初始化

类成员可以在声明处提供默认值(C++11),也可以在构造函数初始化列表中初始化。如果两者都没有,成员的初始状态取决于其类型:基本类型(int、double、指针等)的值未定义,包含垃圾值;类类型成员会调用其默认构造函数,若无默认构造函数则编译报错;const成员和引用成员必须在初始化列表中初始化,否则编译报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Widget {
private:
int count = 0; // 类内初始化(C++11)
std::string name{"default"}; // 类内列表初始化
std::vector<int> data;
const int id;
int& ref;

public:
// 初始化列表:用于const成员、引用成员或需要特定值的成员
Widget(int i, int& r) : id(i), ref(r), data{1, 2, 3} {
// 此时成员已完成初始化
count = 100; // 这是赋值,不是初始化
}
};

初始化列表的执行顺序与成员声明顺序一致,与列表中的书写顺序无关:

1
2
3
4
5
6
7
class Example {
int a;
int b;
public:
// 实际初始化顺序:先a后b(按声明顺序)
Example() : b(1), a(b) {} // 危险:a使用了未初始化的b
};

自动类型推导

使用auto时,不同语法会影响推导结果:

1
2
3
4
5
auto a = 10;        // int
auto b(10); // int
auto c{10}; // int(C++17),std::initializer_list<int>(C++11/14)
auto d = {10}; // std::initializer_list<int>
auto e = {1, 2, 3}; // std::initializer_list<int>

C++17统一了单元素花括号初始化的行为,使auto c{10}推导为int而非initializer_list。

八、选择建议

场景 推荐方式 说明
基本类型 列表初始化 防止窄化转换
类对象 列表初始化或直接初始化 列表初始化防止窄化,直接初始化避免initializer_list歧义
STL容器 列表初始化 简洁直观
vector指定大小 圆括号 避免与元素列表混淆
成员初始化 类内初始化 + 初始化列表 类内提供默认值,列表用于依赖构造参数的成员
聚合类型 指定初始化器 C++20起,提升可读性
auto推导 等号初始化 避免花括号导致的initializer_list推导

统一使用列表初始化是现代C++的推荐做法,它语法统一且能在编译期捕获潜在的类型转换问题。但需要注意以下例外情况:

  • 调用特定构造函数(如vector的size构造函数)时使用圆括号语法,避免被解析为元素列表
  • 使用auto推导时优先使用auto x = value形式,避免花括号导致意外推导为initializer_list
  • 对于配置类、选项结构体等聚合类型,C++20的指定初始化器能显著提升代码可读性