使用列表初始化时发现记忆很模糊,写下来备忘顺便总结一下。
初始化的概念
初始化是指创建变量时赋予其一个初始值。特别要注意C++中初始化与赋值的区别,赋值的含义是抹去原有的值后赋予一个新值。不可以将赋值和初始化混为一谈。
比如对于一个类:
class Whatever {
public:
Whatever();
private:
int v1;
int v2;
std::string key;
OtherClass value;
};
考虑其构造函数两种定义形式的区别:
// 使用初始值列表
Whatever::Whatever() : v1(...), v2(...), key(...), value(...) {}
// 在构造函数内赋值
Whatever::Whatever() {
v1 = ...;
v2 = ...;
key = ...;
value = ...;
}
这两种方式的区别在于,只有使用了初始值列表的方式真正实现了初始化,而构造函数内赋值的做法等价于先默认初始化一个对象中的所有成员再分别对其中的成员赋值。由于我们创建一个对象后马上就会调用它的构造函数,这两种方法似乎并没有显著的区别。
考虑到初始化和赋值的过程,可以知道先初始化再赋值的开销至少是直接初始化的两倍。对于一个内置类型的成员而言这样的代价可以忽略不计,然而一个类类型成员的构造代价却有可能非常大。因此,定义构造函数时应尽量使用初始值列表的方式,这不单是为了节省这些性能,也是一个语义上的准确表达。
默认初始化
定义一个变量而不显式指定初始值时,变量被默认初始化。默认初始化的规则如下
- 类类型对象:由类的默认构造函数定义。
- 内置类型对象:
- 全局对象:初值为0。
- 非全局对象:不初始化,其值为未定义的,取决于分配到的内存块上已存在的值。
试图默认初始化一个不允许默认初始化的类类型对象将导致编译错误。
直接初始化
定义时显式地调用对象的构造函数称为直接初始化,例如:
int a(0);
std::string str("Hello");
注意,只有定义时调用构造函数才是初始化,通过构造函数修改已存在变量的值也是赋值操作。
列表初始化
在C++11之前,可以对POD(Plain Old Data,即可以使用memcpy拷贝的类型)类型和内置数组进行列表初始化,如:
int arr[3] = {1, 2, 3};
struct A {
int x;
int y;
};
// 列表初始化, x = 1, y = 2
A a = {1, 2};
// 列表初始化, x = 1, y默认构造
A a = {1};
C++11以后,这种初始化方式得到了普及,现在可以对任何对象使用列表初始化并且无需=
号。如:
int a{5};
int arr[3]{1, 2, 3};
std::string str{"hello"};
列表初始化有一个重要特性,内置类型不会进行隐式类型转换。比如:
int a = 1.5; // a == 1
int b(1.5); // b == 1
int c{1.5}; // error
列表初始化和初始化列表有关系吗?答案是大有关系。列表初始化就是根据定义了初始化列表的构造函数初始化对象的。比如:
struct A {
A(int a, int b) : x(a), y(b) {}
int x;
int y;
};
A{a, b} == A(a, b) // .x == a, .y == b
那么类似于数组形式的列表初始化是如何实现的呢?使用std::initializer_list
:
struct A {
A(initializer_list<int> list)
: size(list.size()), head(new int[size]) {
auto cur = head;
for (auto i : list) {
*cur = i;
++cur;
}
}
~A() {
delete[] head;
}
int size;
int *head;
}; // 一个简单的数组, 可以接受任意数量的参数
A{1, 2, 3, 4} == A({1, 2, 3, 4});
当同时存在接受参数的构造函数和接受std::initializer_list
的构造函数时,列表初始化优先调用接受std::initializer_list
的构造函数。
拷贝初始化
使用Type id = value
或Type id(value) // value 为一个Type类型的变量
的形式定义的初始化称为拷贝初始化。其实质是使用另一个对象的值来构造对象,使用拷贝赋值运算符函数定义其行为。