以前,C++可以使用小括号、大括号、复制操作符等来初始化一个变量, 现在(从C++11开始算起),在C++初始化一个变量时,建议都使用
{}
来初始化
问题:
C++11之前, 初始化方式,乱七八糟,不同编译器的对待方式也不太一样。总的来说,不同的对象有着不同的初始化方法
这些不同的方法都针对各自对象,不能通用
std::string ("hello");
std::string s="hello";
struct tm today={0};
解决:
(1)C++11引入了一致性初始化语法,也就是用{}
来一统江湖
int values[]{ 1,2,3,4,5,6,7,8,9 };
std::vector<char> vec{'a','b' ,'c' ,'d' ,'e' };
std::map<int, std::string>m1{ { 1,"a" },{ 2,"b" },{ 3,"c" },{ 4,"d" } };
int* a = new int[3] { 1, 2, 0 }; //C++11 only
class X
{
private:
int a[4];
public:
X(): a{1,2,3,4} {//具体的实现逻辑} //C++11, 初始化数组成员
}
};
class C
{
private:
int a = 10;
int b;
public:
C(int i, int j);
};
C c {0,0}; //C++11 only. 相当于 C c(0,0);
(2)另外,为了让用户自定义类型也可以使用{}
,C++11提供了std::initializer_list<>
#include
#include
#include
class MaginFoo{
public:
std::vector<int> vec;
MaginFoo(std::initializer_list<int> list){
for(std::initializer_list<int>::iterator it = list.begin();
it != list.end(); ++it){
vec.push_back(*it);
}
}
};
int main() {
MaginFoo maginFoo = {1, 2, 3, 4};
for(auto i : maginFoo.vec){
printf("%d\t", i);
}
}
C++11首先把初始化列表的概念绑定到了类型上,并将之称为std::initializer_list
(实现方式为容器),允许构造函数和其他函数像参数一样使用初始化列表,这就为类对象的初始化和普通数值和POD的初始化方法提供了同一的桥梁,比如:
这种构造函数被叫做初始化列表构造函数,具有这种构造参数的类型将在初始化时被特殊关照
(3)std::initializer_list<>
可以用在任何你想要处理一系列值的初始化的地方
void print(std::initializer_list<int> value){
for(auto it: value){
printf("%d\t", it);
}
}
f({3, 4, 8, 9});
接受单一std::initializer_list参数的构造函数称为初始化器列表构造函数
(1)标准库容器都有初始化器列表构造函数、初始化器列表赋值运算符等成员:
vector<double> v = {1, 2, 3.14, 4.26, 6.55};
list<pair<string, string>> languages = {
{"Nyguaad", "Simala"}, {"Richjhia", "BCLP"}, {"itchie", "C"}
};
(2)自定义类型的初始化器构造列表怎么写?
// ---定义----
class A{
public:
A(std::initializer_list<int> list){
......
}
}
// --- 使用---
A a = {1, 2, 3, 4};
(3)自定义函数的初始化构造器怎么写?
void print(std::initializer_list<int> value){
for(auto it: value){
printf("%d\t", it);
}
}
f({1, 2});
f({3, 4, 8, 9});
f({})
小结:一个初始化器列表构造函数使用一个{}列表作为其初始化值来构造对象
注意:
当指明参数个数和initializer_list的构造函数同时存在,initializer_list优先
class MaginFoo{
public:
MaginFoo(int i, int j){
printf("(i, j)\r\n");
}
MaginFoo(std::initializer_list<int> list){
printf("(std::initializer_list list)\r\n" );
}
};
int main(int argc,char *argv[]){
MaginFoo x(77, 5); // (i, j)
MaginFoo X1{77, 5}; // (std::initializer_list list)
MaginFoo X2{77, 5, 6}; // (std::initializer_list list)
MaginFoo X3 = {77, 5}; // (std::initializer_list list)
MaginFoo X4 {77.5, 5, 66}; // 不允许隐式转换
return 0;
}
如果一个类有多个构造函数,则编译期会使用常规的重载解析规则根据给定参数选择一个正确的构造函数。
当选择构造函数时,默认构造函数和初始化器列表构造函数优先。
struct X{
X(initializer_list<int>);
X();
X(int);
};
X x0{}; //默认构造函数
X x1{1}; //初始化器列表构造函数
具体规则如下:
第一条规则”优先选择默认构造函数“是合理的:只要可能,就选择最简单的构造函数。而且,如果你定义的初始化器列表构造函数在接受空列表时所做的事情与默认构造函数不同,那么你可能犯了一个设计错误
第二天规则”优先选择初始化器列表构造函数“是必要的,可以避免依据不同元素数产生不同的解析结果:
vector<int> v1{1}; // 1个元素
vector<int> v2{1, 2}; // 2个元素
vector<int> v3{1, 2, 3}; // 3个元素
vector<string> vs1 {"one"}; //一个元素
vector<string> vs2{"one", "two"};
这段代码中所有初始化操作都必须使用初始化器列表构造函数。如果我们真的希望调用接受一个或者两个整型参数的构造函数,就必须使用()语法:
vector<int> v1(1); //构造一个元素,具有默认值0
vector<int> v2(1, 2); //构造一个值为2的元素
使用initializer_list
可以将接受一个initializer_list< T> 参数作为一个序列来访问,即通过成员函数begin()、end()、size()访问:
void f(initializer_list<ing> args){
for(int i = 0; i != args.size(); ++i){
cout << args.begin()[i] << "\n"
}
}
注意,initializer_list不提供下标操作。
initializer_list< T> 是以值方式传递的。这是重载解析规则锁要求的,而且不会带来额外开销,因为一个initializer_list< T> 对象只是一个小句柄,指向一个元素类型为T的数组。
上面相当于:
void f(initializer_list<ing> args){
for(auto p=args.begin(); p!=args.end(); ++p) {
std::cout << *p << std::endl;
}
}
或者是:
void f(initializer_list<ing> args){
for(auto x: args) {
std::cout << x << std::endl;
}
}
为了显式使用一个initializer_list,你必须在定义它的地方包含头文件#include
。但是,由于vector、map等使用initializer_list,他们的头文件比如< vector>、< map>等已经#include
,因此你很少需要直接包含此头文件。
initializer_list的元素是不可变的,不要考虑修改它们的值:
void f(initializer_list<ing> args){
*args.begin() = 11; //error:试图改变初始化器列表的值
}
由于initializer_list是不可变的,因此不能对齐使用移动构造函数。
一个容器可能像下面这样实现初始化器列表构造函数:
template<typename E>
class Vector{
public:
Vector(std::initializer_list<E> s); //初始化器列表构造函数
private:
int sz;
E * elem;
};
template<typename E>
Vector::Vector(std::initializer_list<E> s) : sz(s.size()){
reserve(sz);
uninitialized_copy(s.begin(), s.end(), elem); //初始化elem[0, size()]中的元素
}
初始化器列表是通用和一致初始化设计的一部分。
直接初始化和拷贝初始化
{}初始化也存在直接初始化和拷贝初始化的区别。对一个容器来说,这意味着这种区别对容器自身以及其中的元素都有作用:
对于一个vector< vector< double>>,我们可以直接看到直接初始化和拷贝初始化的区别对元素所起的作用:
vector<vector<double>> vs = {
{10, 11, 2, 36, 78, 1}, //正确,6个元素的vector
{10}, //正确,一个元素的vector
10, //错误,vector(int)是explicit的
vector<double>{10, 11, 12, 13}, //正确,4个元素的vector
vector<double>{10}, //正确,1个元素的vector,元素值为10.0
vector<double>(10), //正确,10个元素的vector,元素值为0.0
};
一个容器可以有若干个显式构造函数和若干个隐式构造函数,标准库vector就是这样一个例子。比如std::vector< int>(int)是explicit,但是std::vector< int>(initialized_list< int>)不是:
vector<double> v1(7); //正确,v1有7个元素
vector<double> v2 = 9; //错误,不能从int转换为vector
void f(const vector<double>&);
void g(){
v1 = 9; //错误,不能从int转换为vector
f(9); //错误,不能从int转换为vector
}
把()换成{}
vector<double> v1{7}; //正确,v1有1个元素, 值为7
vector<double> v2 = {9}; //正确,v1有1个元素, 值为9
void f(const vector<double>&);
void g(){
v1 = {9}; //正确,v1有1个元素, 值为9
f({9}); //正确,调用f,将列表{}传递给它
}
std::initializer_list
作用:
std::initializer_list
引入了“一致性初始化”(uniform initialization
)概念,统一了初始化方式。 从此,我们面对任何初始化动作,都可以使用相同语法 { }
,进行一致性的初始化。所有的简单机制背后, 都有一个稍稍复杂的机制, 或者对你透明(隐藏)的工程细节.
其实借助了统一的初始化列表,即initializer_list
,在大括号内的所有内容,都会被装入initializer_list
然后用于给不同的变量&对象,进行初始化
initializer_list
背后关联了一个array容器,初始化时,array内部的元素会被编译器逐一传递给相关的变量&对象&容器, 如果初始化函数, 比如构造器的参数本身就是一个initializer_list, 那么你可以按照要求传递即可(此时不用从里面拆分元素了). 如果没有这样构造器来初始化变量&对象, 那么它会把大括号形成的initializer_list进行拆解, 然后去匹配相应的调用构造器.
其实从大括号的参数, 到构件完成一个initializer
对象, 编译器调用的是私有构造器, 这个时候才会在内部关联一个array对象. 或者说编译器再调用私有构造器之前就准备好了一个array用来盛放具体的初始化参数, 然后私有构造器接收的是array的头迭代器以及array的长度.
使用initializer有很多好处, 默认赋值, 安全检查等.
引入原因:统一初始化方式
默认初始化未0或者nullptr
int j{}; //默认赋值为0, 因为是int型
int* q{}; //默认赋值为nullptr
否则会造成未定义行为:
int i; // i 初始化为未定义值.
int * p; // p 初始化为未定义值.
以往的初始化, 有一些处理会难以察觉, 比如char类型赋值9999, 溢出了, 当然有的会自动截断, 例如: int x1 = 5.3(像这种精度降低或造成数值变动就叫做 窄化)
int x1(5.3); //OK, buf x1 become 5
int x2 = 5.3; // //OK, buf x2 become 5
但是使用initializer_list时, 情况就不一样, 它会让编译器报错(或者警告):
int x1{5.3}; // error: narrowing conversion of '5.2999999999999998e+0' from 'double' to 'int' inside { } [-Wnarrowing]
int x2 = {5.3}; //error: narrowing conversion of '5.2999999999999998e+0' from 'double' to 'int' inside { } [-Wnarrowing]
char c1{7};
char c2{9999}; // error: narrowing conversion of '9999' from 'int' to 'char' inside { } [-Wnarrowing]
std::vector<int> v1 {1, 2, 3, 4};
std::vector<int > v2 {1, 2.2}; // error: narrowing conversion of '2.2000000000000002e+0' from 'double' to 'int' inside { } [-Wnarrowing]
初始化列表除了用在对象构造上,也可以作为普通函数的形参,也能用于实现变参模板(毕竟参数个数不限定), 但是比variadic templates弱的是, 所有参数的类型要一样.
#include
#include
void print(std::initializer_list<int> vals)
{
for(auto p=vals.begin(); p!=vals.end(); ++p) {
std::cout << *p << std::endl;
}
}
int main() {
print({1, 2, 3});
return 0;
}
只要你加上了大括号, 调用重载的时候, 都是优先找参数为std::initializer_list
的构造器.
#include
#include
class P{
public:
P(int ,int) { std::cout << "P(int ,int)" << std::endl;}; // 指明实参个数
P(std::initializer_list<int>) { std::cout << "P(std::initializer_list)" << std::endl;}; //指明初值类
};
int main() {
P p(77, 7); // P(int ,int)
P q{77, 5}; //P(std::initializer_list)
P q2{77, 5, 22}; //P(std::initializer_list)
P s = {77, 5}; //P(std::initializer_list)
return 0;
}
如果没有提供std::initializer_list
构造函数,则{}
会去找匹配个数的()
构造函数:
#include
#include
class P{
public:
P(int ,int) { std::cout << "P(int ,int)" << std::endl;}; // 指明实参个数
};
int main() {
P p(77, 7); // P(int ,int)
P q{77, 5}; // P(int ,int)
// P q2{77, 5, 22}; // error: no matching function for call to 'P::P()'
P s = {77, 5}; // P(int ,int)
return 0;
}
#include
#include
class P{
public:
P(int ,int) { std::cout << "P(int ,int)" << std::endl;}; // 指明实参个数
explicit P(int a, int b, int c){
std::cout << " explicit P(int a, int b, int c)" << std::endl;
};
};
int main() {
P p(77, 7); // P(int ,int)
P q{77, 5}; // P(int ,int)
P q2{77, 5, 22}; // explicit P(int a, int b, int c)
P s = {77, 5}; // P(int ,int)
P s1 = {77, 5, 42}; // error: converting to 'P' from initializer list would use explicit constructor 'P::P(int, int, int)'
return 0;
}