C++变量初始化详解(C++98和C++11)


在C++11以前,C++的初始化基本上是在C的初始化方式基础上加上了类和类成员的初始化,有很多不太方便的地方。C++11一口气修改了大多数问题,但是也让初始化问题变得更复杂,所以我觉得有必要梳理一下。

C++98的变量初始化

C++98的初始化大部分继承自C,也支持数组,struct和union复合类型利用{}初始化列表来初始化。需要注意的是,初始化列表是顺序初始化,即按照内存顺序一个一个初始化对应成员,这点在下面我们将会看到。

1、POD类型,指针,引用初始化:

  • 可用=或者()
1
2
3
4
5
6
int a = 10;
int b(10);
int* pa = &a;
int* pb(&b);
int& ra = a;
int& rb(b);

2、复合类型结构体和联合体初始化:

  • 非堆上的对象(不是new出来的),可用=形式的{}初始化列表和同类型对象初始化
  • 堆上的对象(new出来的),用空括号()初始化,括号不能传参
  • 注意,复合类型结构体不能有类对象成员或构造函数;否则就成了类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct Man {
int age;
int weight;
};
Man a0; //未初始化,成员变量随机值
Man a1 = {10}; //初始化列表,顺序初始化,a1.age=10, a1.weight=0
Man a2 = {20, 120}; //同上,a1.age=20, a1.weight=120
Man a3 = {.age = 10, .weight = 50}; //成员指示符,C99标准 (GCC支持,VS不支持)
Man a4 = {age: 10, weight: 50}; //冒号成员指示符,GCC的扩展
Man a5 = a4;
Man* pm0 = new Man; //未初始化,成员变量随机值
Man* pm1 = new Man(); //成员变量初始化为默认值0

union UMan {
Man m;
int i;
char c[8];
};
UMan um = {10}; //没有成员指示符,默认初始化union的第一个成员m, um.m.age = 10, um.m.weight = 0
UMan um = {10, 120}; //同上,um.m.age = 10, um.m.weight = 120
UMan um = {.i = 22}; //有成员指示符,初始化指定成员
UMan um = {.c = "hello"}; //同上

3、类对象初始化:

  • 可用=形式或者()形式,不可用{}初始化列表,其实都是调用构造函数或拷贝构造函数
  • 成员变量通过构造函数初始化列表初始化;没有显式初始化的成员将不会被初始化
1
2
3
4
5
6
7
8
9
struct CMan {
int age;
int weight;
CMan(): age(0), weight(0) {}
};

CMan a0; //调用默认构造函数
CMan a1 = a0; //调用拷贝构造函数
CMan a2(a0); //调用拷贝构造函数

3、数组的初始化:

  • 非堆上的数组(不是new出来的),可用=形式的{}初始化列表,字符串数组可以直接用常量字符串初始化
  • 堆上的数组(new出来的),用空括号()初始化,括号不能传参
  • 类对象数组的成员一定会调用构造函数初始化
1
2
3
4
5
6
7
8
9
10
11
12
13
int a[10] = {1,2};                     //顺序初始化,a[0]=1,a[1]=2,其余置0
char b[10] = "hello"; //字符串数组初始化

int a[10][11] = {1,2,3,4}; //顺序初始化,a[0][0]=1,a[0][1]=2,a[0][2]=3,a[0][3]=4,其余为0
int a[10][11] = {{1,2},{3,4}}; //顺序初始化,a[0][0]=1,a[0][1]=2,a[1][0]=3,a[1][1]=4,其余为0

UMan aum[8] = {10, 110}; //顺序初始化,aum[0].m.age = 10,aum[0].m.weight = 110
UMan aum[8] = {{10}, {0,110}}; //顺序初始化,aum[0].m.age = 10,aum[1].m.weight = 110

int* c = new int[12]; //未初始化
int* d = new int[12](); //元素值初始化为默认值0

string a[10] = {"abc"}; //每个元素都调用构造函数

4、复杂类型初始化

  • 对于复杂类型,其自身和成员的初始化应满足以上规则和方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//Compond1是复合结构体,可以用初始化列表
struct Compond1 {
UMan um;
Man m[10];
int i;
};
Compond1 cc1 = {{10}, {11}}; //OK, cc1.um.m.age = 10, cc1.m[0].age=11, 其余为0

//Compond2有类对象成员,会自动合成默认构造函数,所以不能用初始化列表初始化
struct Compond2 {
UMan um;
CMan cm[10];
int i;
};
Compond2 cc2; //OK,这里cc2的合成默认构造函数啥也没干,那么um和i是否初始化了呢? 答案是没有!um和i都是随机值。

C++11的变量初始化

C++11在变量初始化上做了很大拓展,主要有这么两方面:

  1. 将初始化列表进一步完善成**统一初始化器(Uniform initialization)**;
  2. 类成员的原地初始化;

另外:初始化列表前的等号可以省略了。

1、统一初始化器

统一初始化器依然保持初始化列表的形式,兼容C++98的用法。从上面C++98我们可以看到,原来的{}初始化列表不能用来初始化new出来的结构对象或数组,也不能用来初始化类对象。C++11中这两个弊端被解决。所以我们只需要了解下统一初始化器在这些地方的使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
int i{3};     //和括号类似
int* pi{&i}; //和括号类似
int a[]{ 1, 2, 3, 4 }; // 等号可以省略了
int* pa = new int[]{1,2,3,4,5};

struct Man {
int age;
int weight;
};
Man m{10,110};
Man* pm = new Man{10};
Man m[10]{10}; //pam[0].age=10

//堆上的数组或对象并非顺序初始化,需要按照对象定义初始化列表的层次结构
Man* pam = new Man[5]{10,11,12}; //pam[0].age=10,pam[0].weight=11,pam[1].age=12 | GCC ERROR!!!, VS incorrect!!
Man* pam = new Man[5]{ {10,11},{12} }; //pam[0].age=10,pam[0].weight=11,pam[1].age=12 | GCC OK!, vs incorrect!!

//-------------------------------------------------
//类对象的初始化列表用法
struct CMan {
int age;
int weight;
CMan(): age(0), weight(0){}
CMan(int a, int w): age(a), weight(w) {}
};
CMan m{ 10, 110 };
CMan* pm = new Man{ 10, 120}; //调用2个参数的构造函数
CMan am[10] = {{10, 110}, {11, 110}}; //数组前两个元素调用两个参数的构造函数,剩余的调用默认构造函数;相应构造函数不存在会报错!

对于类对象来说,如果利用初始化列表来初始化:

  • 如果类存在接受std::initializer_list参数的构造函数,则优先调用该构造函数;
  • 如果类不存在接受std::initializer_list参数的构造函数,则调用参数个数和类型与初始化列表元素个数相等,类型相符的构造函数;
  • 如果类中找不到以上两种匹配的构造函数则报错;
  • 一个例外是,如果一个自定义类既有默认构造函数,也有std::initializer_list作为参数的构造函数,则使用{}作为初始化值构造对象的话,C++标准显式规定了调用其默认构造函数,如果想要以空列表的语义调用第二个版本,则可以使用({})的方式进行初始化。

另外,以std::initializer_list作为形参的话,其实参列表中的元素不要求和T完全匹配,而只需要能转换成T即可,此时只要转换后满足要求,编译器都会优先使用std::initializer_list作为形参的重载版本。在转换的过程中,如果类型提升满足要求则会正常调用;如果发生了窄化转换则调用会失败报错;只有诸如字符串和数字这类无法转换的类型相互重载时候,重载机制才可能正常工作。

1
2
3
4
5
6
7
struct Widget {
Widget(int i, bool b) { cout << "1" << endl; }
Widget(int i, double d) { cout << "2" << endl; }
Widget(std::initializer_list<bool> il) { cout << "3" << endl; }
};
Widget w1{1, true}; // 1转换成bool不丢失数据,故OK
Widget w2{9, true}; // Error, int类型9到bool丢失数据,类型变窄,故不能转换

2、类成员就地初始化

在C++11之前,只能对结构体或类的静态常量成员进行就地初始化。在C++11中,结构体和类的数据成员在申明时可以直接赋予一个默认值,初始化的方式有两种,一是使用等号"=",二是使用{}列表初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//C++98
class C {
private:
static const int a=10; //yes
int a=10; //no
};

//C++11
class C {
private:
int a=7; //C++11 only
int b{7}; //或int b={7}; C++11 only
int c(7); //error
};

C++11标准支持了就地初始化非静态数据成员的同时,构造函数初始化列表的方式也被保留下来,也就是说既可以使用就地初始化,也可以使用初始化列表来完成数据成员的初始化工作。当二者同时使用时,并不冲突,初始化列表发生在就地初始化之后,即最终的初始化结果以构造函数初始化列表为准。参考如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
using namespace std;

class Mem
{
public:
Mem(int i,int j):m1(i),m2(j) {}

int m1 = 1;
int m2 = {2};
};

int main()
{
Mem mem(11,22);
cout<<"m1="<< mem.m1<<" m2="<<mem.m2<<endl; //m1=11 m2=22
}

一些需要注意的问题

C++11的初始化确实给我们带来了许多方便,不过有些情况的存在,也很容易让我们忽视和误解,这里举一些例子。

  1. 比如对于vector变量定义,有这么两种形式:

    1
    2
    vector<int> va(99);  //调用接受size为参数的构造函数,初始化99个元素,默认值都是0
    vector<int> vb{99}; //调用接受initializer_list参数的构造函数,初始化一个元素,值为99
  2. 用auto定义的变量:

    1
    2
    auto z1 {99}; // typeof(z1) = initializer_list<int>
    auto z2 = 99; // typeof(z2) = int