原文链接:一致性初始化、初值列(initializer_list)、成员变量初始化方式
一、列表初始化/一致性初始化
设计的目的:
- 在C++11之前,如何初始化一个变量或对象的概念比较混淆。初始化的场景可以发生在:小括号、大括号或赋值操作符中
- C++11引入了“列表初始化/一致性初始化”,意思为:面对任何初始化动作,你可以使用相同语法,就是使用大括号
- 例如下面的都是正确的:
int units_sold = 0; int units_sold = { 0 }; int units_sold{ 0 }; int units_sold(0); int values[]{ 1,2,3 }; std::vector<int> v{ 2,3,5,7,11,13,17 }; std::vector<std::string> cites{ "Berlin","New York","London" }
局部基础数据类型的默认初始化
- 对于局部变量的定义,如果我们没有给出初始值,那么这个变量定义的值是不明确的未知的
- 如果我们使用{}对普通数据类型的变量进行定义,即使没有给出初始值,那么会根据数据类型进行默认初始化
- 例如:
int i; //乱值 int j{}; //0 int *p; //p指向未知地址 int *q{}; //q为nullptr
禁止窄化
“窄化”意为:精度降低或造成数值变动
- 对于一般的赋值与初始化,允许窄化的发生
- 对于大括号的初始化,其不允许窄化的发生,如果有,那么编译将不通过。主要的原因是为了防止数据在转换中的丢失
- 例如:
int x1(5.3); //正确,允许double向int转变,但是x1为5 int x2 = 5.3; //正确,同上 int x3{ 5.3 }; //错误,不允许,因为造成了数据丢失 char c1{ 7 }; //正确,7也属于char char c2{ 99999 }; //错误,99999不属于char的范围,不允许转换 std::vector<int> v1{ 1,2,3,4,5 }; //正确 std::vector<int> v2{ 1.2,8,1.5 }; //错误,其中含有double数据
二、std::initializer_list
- 为了支持“用户自定义类型之初值列”概念,C++11提供了class template std::initializer_list<>,用来支持以一些列值进行初始化
- initializer_list是一种标准库类型,用于某种特定类型的值的数组,并且initializer_list中的元素永远是常量值,我们无法改变initializer_list对象中元素的值
- initializer_list定义在头文件<initializer_list>中,它提供的操作如下:
一些演示案例
- std::initializer_list提供begin()和end()两个成员。例如:
void print(std::initializer_list<int> vals)
{
for (auto p = vals.begin(); p != vals.end(); ++p)
std::cout << *p << std::endl;
}
print({ 12,3,5,4,8 });
- 在类的构造函数使用std::initializer_list。例如:
class P {
public:
P(int, int);
P(std::initializer_list<int>);
};
int main()
{
P p(77, 5); //调用给P(int, int)
P p2(77, 5, 20); //错误,
P p3{ 77,5 }; //调用P(std::initializer_list<int>)
P p4 = { 77,5 }; //调用P(std::initializer_list<int>)
P p5{ 77,5,10,15 }; //调用P(std::initializer_list<int>)
return 0;
}
//备注,如果P没有提供P(std::initializer_list<int>),那么上面的p3、p4将
//调用P(int, int),p5初始化将错误
- explicit将抑制类的构造函数转换行为。例如:
class P {
public:
P(int, int);
explicit P(int, int, int);
};
int main()
{
P p(77, 5); //正确,调用P(int, int)
P p2(77, 5, 20); //正确,调用explicit P(int, int, int)
P p4 = { 77,5 }; //正确,调用P(int, int)
//错误,explicit关键字抑制initializer_list列表使用P(int, int, int)构造函数
P p4 = { 77,5,10 };
return 0;
}
成员变量初始化方式
成员变量初始化有三种方式:
- 在构造函数体内赋值初始化
- 在自定义的公有函数体中赋值初始化(一般用于成员变量的初始化)
- 在构造函数的成员初始化列表初始化
一、构造函数体内初始化
- 说明:在构造函数体内的初始化方式,本质是是为成员变量赋值,而不是真正意义上的初始化,这点要特别注意!(下面介绍成员初始化列表时会有演示案例对比说明)
class Cperson { private: int m_age; float m_height; char* m_name; public: Cperson(int age,float height,const char* name) { m_age=age; m_height=height; if(m_name)//先判断当前是否为空 delete[] m_name; if(name)//如果外部传入的不为空 { int len=strlen(name); m_name=new char[len+1];//创建内存 strcpy(t m_name,name); } else m_name=nullptr; } }
二、自定义的公有函数体中赋值初始化
- 说明:与构造函数体内初始化方式一样,此种方式本质上也是赋值,而不是初始化
class Cperson { private: int m_age; float m_height; char* m_name; public: void setPerson(int age,float height,const char* name) { m_age=age; m_height=height; if(m_name)//先判断当前是否为空 delete[] m_name; if(name)//如果外部传入的不为空 { int len=strlen(name); m_name=new char[len+1];//创建内存 strcpy(t m_name,name); } else m_name=nullptr; } }
三、成员初始化列表初始化
- 特点:
- 写在构造函数的后面,随着构造函数的执行而执行
- 初始化顺序:
- 初始化顺序与书写的在构造函数后的顺序无关,而与成员变量的定义顺序有关(下面有演示案例)
- 初始化列表初始化优先于构造函数内的代码执行顺序
- 多个成员之间用逗号隔开,括号内为形参
- 一般只对无动态内存的成员、const成员、引用初始化(const成员、引用成员必须在初始化列表初始化)
- 成员初始化列表初始化效率更高(下面有演示案例)
- 有动态内存的成员必须在构造函数内部进行初始化(为什么?因为动态内存不能进行简单的赋值,因此所存在的地址不同,要自己申请动态内存并初始化)。牢记:内部数据内部处理,外部数据外部处理
class Cperson { private: int m_age; float m_height; char* m_name; public: Cperson(int age,float height,const char* name); } //m_name为指针类型,需要自己申请空间 Cperson::Cperson(int age,float height,const char* name):m_age(age),m_height(height) { if(m_name)//先判断当前是否为空 delete[] m_name; if(name)//如果外部传入的不为空 { int len=strlen(name); m_name=new char[len+1];//创建内存 strcpy(m_name,name); } else m_name=nullptr; }
成员的初始化顺序
- 成员初始化的顺序,与在构造函数后面书写的顺序无关。而与成员变量定义的顺序有关
- 例如下面的代码,在构造函数花括号后m_height放在m_age前面,但是先初始化m_age再初始化m_height,因为m_age先定义
class Cperson { private: int m_age; float m_height; public: Cperson(int age,float height); } Cperson::Cperson(int age,float height):m_height(height),m_age(age) {}
错误事例(初始化顺序导致的错误)
- 一个特殊情况:如果用一个成员变量去初始化另一个成员变量,就要注意初始化顺序了
- 因此,我们在初始化的时候,尽量避免用某些成员去初始化另一个成员
//下面代码中,i先被初始化,但是i是根据j初始化的,但j后初始化,就会产生不好的后果 class X { int i; int j; public: X(int value):j(value),i(j) {} };
- 更正:因为初始化列表初始化比构造函数内初始化早,所以可以将上面的代码改为下面的形式就不会出错了
class X { int i; int j; public: //j先初始化,i再初始化 X(int value):j(value) { i(j); } }
错误事例(针对const成员与引用成员)
- 此案例强调是的,const成员和引用必须在成员初始化列表进行初始化
class Person { private: const int id; int& m_id; public: Person(int i); }; Person::Person(int i) { id=i;//错误,const成员变量必须在成员初始化列表初始化 m_id=id;//错误,引用也必须在成员初始化列表初始化 }
演示案例(成员初始化列表初始化效率更高)
- 例如下面在构造函数内对两个成员进行初始化
class Word{ string _name; int _cnt; public: Word(){ _name=0;//先创建一个临时string对象,赋值为0,然后拷贝给_name _cnt=0; //构造函数结束之后,临时对象析构释放 } }
- 但是如果使用下面的成员初始化列表初始化,那么就省去了创建临时对象再拷贝的过程,因此成员初始化列表初始化的效率更高
class Word{ string _name; int _cnt; public: Word():_name(0),_cnt(0){} //直接初始化_name,不创建临时对象 };
初始化方式总结
- 根据上面的三种方式,总结出:成员初始化列表初始化成员才是真正意义上的初始化,其他两种方式都是为赋值
- 初始化和赋值涉及到底层效率的问题:初始化是直接初始化。而赋值是先初始化一个临时变量,再赋值。前者效率高