一、初始化列表
1、概念
在 类和对象中篇 中我们学习了C++的六个默认成员函数,其中构造函数用于对对象进行初始化,即在创建对象时,编译器会自动调用构造函数,给对象中各个成员变量一个合适的初始值;
class Date { public: Date(int year = 1970, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } private: int _year; int _month; int _day; };
虽然上述构造函数调用之后,对象个成员变量中已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化,构造函数函数体中的语句只能将其称为赋初值,而不能称作初始化;因为初始化只能初始化一次,而构造函数体内可以进行多次赋值;那么成员变量在哪里初始化呢?
另外,我们知道类里面只是成员变量的声明,并不是成员变量的定义,因为类并不会在内存中占用空间;而只有当我们用类实例化出具体的对象时才会对成员变量进行定义;而对象是整体定义的,那么对象中具体的每一个成员变量又在哪里定义呢?
C++类对象中的成员变量在初始化列表处进行定义与初始化;初始化列表以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式;
class Date { public: Date(int year = 1970, int month = 1, int day = 1) : _year(year) //初始化列表 , _month(month) , _day(day) {} private: int _year; int _month; int _day; };
2、特性
初始化列表有如下几个特性特性:
1、初始化列表是每个成员变量定义和初始化的地方,所以每个成员变量 (内置类型和自定义类型) 都一定会走初始化列表,无论我们是否在初始化列表处写;并且初始化操作只能进行一次;
2、如果我们在初始化列表写了编译器就会用显式写的来初始化;如果我们没有在初始化列表写,那么对于内置类型,编译器会使用随机值来初始化,对于自定义类型,编译器会调用自定义类型的默认构造函数来初始化,如果没有默认构造编译器就会报错;
下面我们来验证上面的两点特性:
对于内置类型 _day,如果没有显式在初始化列表初始化,编译器会使用随机值来初始化;而对于自定义类型 aa,如果没有显式定义编译器会调用自定义类型的默认构造函数来初始化;
如果自定义类型既没有在初始化列表显式定义,也没有默认构造函数,编译器就会报错;
3、如果类中包含以下成员,则必须放在初始化列表初始化:
- 引用成员变量;
- const 成员变量;
- 没有默认构造的自定义类型;
在前面的学习中我们知道,引用是一个变量的别名,它必须在定义的时候初始化,并且一旦引用了一个变量就不能再去引用另一个变量;同样,const 作为只读常量,也必须在定义的时候初始化,且初始化之后不能在其他地方修改;
而通过上面的学习,构造函数函数体内执行的是赋值语句,成员变量只能在初始化列表进行定义与初始化:
所以对于使用 const 修饰的以及引用类型的成员变量,我们必须在初始化列表处对其进行初始化:
同样,对于没有默认构造函数的自定义类型来说,我们也必须在初始化列表处对其进行初始化,否则编译器就会报错,我们以MyQueue为例:
class Stack { public: Stack(int capacity) :_top(0) , _capacity(capacity) { _a = (int*)malloc(sizeof(int) * capacity); if (_a == nullptr) { perror("malloc fail\n"); exit(-1); } } ~Stack() { free(_a); _a = NULL; _top = _capacity = 0; } void Push(int x) { _a[_top++] = x; } private: int* _a; int _top; int _capacity; }; class MyQueue { public: MyQueue() {}; void Push(int x) { _pushST.Push(x); } Stack _pushST; Stack _popST; };
可以看到,我们这里的 Stack 类提供的是带参构造,并没有给缺省值,所以如果我们不在 MyQueue 构造函数的初始化列表中对 _pushST 与 _popST 进行初始化,编译器会直接报错;
另外,从 Stack 的构造函数中可以看到,构造函数的初始化列表与函数体是可以配合使用的,即可以让始化列表和函数体分别完成一部分工作;
4、尽量使用初始化列表初始化,因为无论我们否使用初始化列表,类的成员变量都会先使用初始化列表进行初始化;
例如 MyQueue 类 (此处的 Stack 具有默认构造函数):
我们可以看到,即使我们显式定义的构造函数什么也没有写,_pushST 和 _popST 也完成了初始化工作,因为无论我们是否在初始化类比处显示写,类的成员变量都会走初始化列表,其中类的自定义类型会调用它的默认构造来完成初始化工作;
5、C++11中对于内置类型打的补丁 – 内置类型成员变量可以在声明的时候给定一个缺省值,其在初始化列表处起作用;
我们之前在学习构造函数时,因为不知道初始化列表的存在,所以认为默认生成的构造函数对内置类型不处理,而C++11为了弥补这个缺陷打了一个补丁,即可以在声明的时候给一个缺省值;但现在我们知道了,内置类型也会在初始化列表进行初始化,只是因为初始化的是一个随机值,所以好像没有初始化一样;而C++11这个补丁就是在初始化列表处生效的;
6、成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关;
如下:
class A { public: A(int a) :_a1(a) , _a2(_a1) {} void Print() { cout << _a1 << " " << _a2 << endl; } private: int _a2; int _a1; };
由于在类中 _a2 的声明在 _a1 之前,所以在初始化列表处 _a2(_a1) 语句被先被执行,而此时 _a1 还是一个随机值,所以最终 _a2 输出随机值;