初始化的含义
所谓初始化,就是给一个变量一个初始值,在使用一个变量的值之前,需要对其进行初始化,否则,得到的就
是一个无意义的值。
初始化的方法
用圆括号:type var(expression-list)
圆括号中是我们提供给构造函数的参数,看下面的例子
1 | int i(1) ; |
这是一种显示初始化的方式,相当于主动调用对应类型的构造函数。
1 | int i(1.1) ; |
注意,如果想采用默认初始化方式初始化一个变量,下面的做法是错误的:
1 | int i() ; |
因为编译器会将其看作是函数声明,而不是变量定义。正确的做法应该1是:
1 | int i ; |
说到这里,就不得不提到 the Most Vexing Parse ,请看下面:
1 | class Timer |
代码来自 维基百科
上面代码的标注1的那行会被看作是一个名字为time_keep的函数, 返回值是 TimeKeeper 类型, 参数是一个返回值为Timer类型的函数指针, 而不是对象定义. 要实现后者, 可以这样:
1 | TimeKeeper time_keeper((Timer())); |
更好的做法是使用C++11定义的用{}来初始化对象,可以避免很多错误。
用等于号:type var = expression
对于内置类型来说, =初始化和()初始化几乎没有区别(我也不知道区别在哪里). 对于类类型, =初始化调用的是copy构造函数, 而赋值是调用重载的=操作符.
1 | MyClass c1 = c0 ; // call MyClass(const MyClass&) |
等于号经常和大括号{}一起用于初始化,
1 | MyClass c = {1 , "123"} ; |
当等号右边值的类型(源类型)与目标类型不同时, 会先寻找可用的类型转换方法将源类型转换为目标类型, 然后通过 direct-initilize (复制构造函数)来初始化目标变量. 最后进行优化, 直接在目标变量的内存上构造转换后对象, 从而减少了一次复制构造函数的调用. 注意, 在C++17之前, 即使这里没有使用到复制构造函数, 也要求其存在/可调用. 下面的代码可以验证. g++编译后会提示不能将 non-const lreference 绑定到 rvalue, 这个错误发生在将转换后对象传递给复制构造函数时. 转换后对象是一个右值, 这里故意将其参数定义为 non-const reference, 从而引发这个错误. 如果编译参数加上 --std=c++17 则不会出现此错误.
1 | struct Test |
详情参考 copy initilization.
用大括号:type var{initializer-list} //C++11推荐
C++11为了解决C++98混乱的初始化方式而提出的一种初始化方式: uniform initialization 保证它可以用于所有的初始化. 实现方式就是采用大括号. 下面介绍一些{}与而其他初始化语法的不同之处:
- 大括号初始化不允许对内置类型进行隐式的窄化类型转换(implicit narrowing conversion), 但是注意, 这个类型转换不仅仅依据类型, 还会依据值的大小.
1 | char c1{10} ; // 正确,10是int 类型,但是可以char大小可以保存 |
- 想要在定义非静态类成员时为成员赋初值(称为 default member initialize), 只能使用=和{}来初始化, 不允许使用().
- 初始化数组和标准库容器
1 | int arr[10]{1,2,3} ; |
- 初始化不可复制的对象(uncopyable objects), 比如 std::atomic, 这里就只能用 {} 和 (), 而不能用 =.
1 | std::atomic<int> ai_1{0} ; //正确 |
什么也不用:type var
有时候我们可能直接定义一个对象, 没有添加=,{},()以显式地初始化, 比如:
1 | int i ; |
这种情况下根据定义所在的位置和对象类型的不同,有不同的初始化行为。
对于类类型,会自动调用类的默认构造函数来初始化对象。
对于内置类型,当变量在全局作用域或者被定义为局部静态变量时, 会被默认初始化, 一般是零初始化.
1 | int i ; // i == 0 |
- 而对于自动变量,则默认初始化得到的是一个不定值。绝大多数情况下使用这个变量都会是一个未定义的行为,例外请参考:cppreference