目录
一.初始化和赋值
初始化不是赋值。
初始化:创建变量时赋予其一个初始值。
赋值:把变量的当前值擦除,用一个新值替代。
C++初始化有几种不同的形式,比如给int型变量x初始化,以下四个方式都可以。
二、初始化变量的各种方式
初始化变量的语句,可分为四部分组成,变量类型 变量名 运算符 初始化值。
变量类型可分为自定义类型和内置类型,变量名可分为数组名和非数组名,运算符只有赋值运算符,初始化值对应着变量名,如果是初始化数组,那么初始化值可以是多个,不是则初始化值只能是1个或0个。
1.拷贝初始化和直接初始化
我们根据有无赋值符号,将使用赋值符号初始化一个变量的方式,称为拷贝初始化(copy initialization),编译器会把等号右侧的值拷贝到新创建的对象中(实际中大多数编译器会对此行为进行优化,优化成直接初始化);不使用等号初始化的方式,称为直接初始化(direct initialization)。
string s1 = "hello";//拷贝初始化
string s2("hello");//直接初始化
string s3(7,'c');//直接初始化
2.匿名初始化(匿名对象)
还可以根据有无变量名分为匿名初始化和非匿名初始化。
struct Point
{
int _x;
int _y;
};
class Date
{
public:
Date(int year, int month, int day)
:_year(year)
, _month(month)
, _day(day)
{
cout << "Date(int year, int month, int day)" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
cout << int() << endl;//匿名变量初始化
cout << Point()._x << endl;//匿名对象初始化
Date();//报错,没有默认构造函数支持Date()匿名初始化
return 0;
}
内置类型都支持匿名初始化,自定义类型一定要有默认构造函数才支持匿名实例化(初始化)。
3.默认初始化和值初始化
根据有无初始化值分为默认初始化和值初始化。
//默认初始化和值初始化
int main()
{
//默认初始化
int i1;
Point p1;
//Date d1;//报错,没有默认构造函数
//值初始化
int i2 = 1;
Point p2(1, 1);//报错:没有对应的 构造函数
Date d2(2024, 6, 23);
return 0;
}
4.C++98语法中初始化的缺陷
a.会将一些初始化识成函数声明
按照值初始化的方式,初始化值应该放在小括号里,不传值的初始化(默认初始化),只要在小括号里不写初始化值就行,但这种方式编译器会识别为函数声明而不是默认初始化,所以当对一个变量进行不传值初始化时(默认初始化时),要省略变量名后的括号。
int x1; // 默认初始化
int x2(); //编译器会识别成函数声明
int x3 = 7; // 值初始化
int x4(1); //
int a[] = { 7,8 }; // 聚合初始化
//自定义类型
string s; // 由默认构造函数初始化
vector<int> v(10); // 由构造函数初始化
b.初始化时可能导致信息损失
long double ld = 3.1415926;
int c(ld);//调用构造函数初始化,不报错,丢失精度
int d = ld;//调用拷贝构造初始化,不报错,丢失精度
return 0;
c.数组可以用列表初始化,但vector不行
int a[] = {7,8}; // 可以
vector<int> v = {7,8}; // 应该可以,但是不行
为了统一并优化初始化的方式,C++标准委员会在C++11中推出了列表初始化的方式。
三、列表初始化的概念
列表初始化:用花括号括起来的初始值列表初始化变量的方式,叫做列表初始化。(注意:我们把花括号里的值统称为初始化器列表,简称列表,形如{1,2,3}中的1、2、3。)。
列表初始化的优势:
long double ld = 3.1415926;
//列表初始化:禁止窄化转换
int a{ ld };//报错:信息存在丢失
int b = { ld };//vs2019_error:从“long double”转换到“int”需要收缩转换
//非列表初始化
int c(ld);//调用构造函数初始化,不报错,丢失精度
int d = ld;//调用拷贝构造初始化,不报错,丢失精度
int x();//函数声明
//列表初始化:避免解析错误
int x1{};//列表初始化
//vector可列表初始化赋值符号可略
int arr1[] = { 1,2,3 };//编译器会优化成直接初始化
vector<int> arr2 = { 1,2,3 };//编译器会优化成直接初始化
同时,列表初始化还支持更复杂的场景:
//列表初始化后的变量可作函数参数(可是匿名变量)
int f(vector<int>);
int i = f({ 1,2,3 }); // 函数参数
//在初始化列表中作成员初始化器
struct X
{
vector<int> v;
int a[];
X() : v{ 1,2 }, a{ 3,4 } {} // 成员初始化器
X(int);
// ...
};
//为使用 new 创建的对象提供初始化器列表
vector<int>* p = new vector<int>{ 1,2,3,4 }; // new 表达式
X x{}; // 默认初始化
//列表初始化可作模板参数
template<typename T>
int foo(T);
int z = foo(X{ 1 }); // 显式构造
//列表初始化作实参和返回值
struct S { string s; int i; };
S foo(S s)
{
// ...
return { string{"foo"},13 };//作返回值
}
S x = foo({ string{"alpha"},12 });//作实参
如果说编译器默认支持内置类型变量的列表初始化,那么自定义类型变量是如何支持列表初始化的呢?
四、std::initializer_list
为了支持自定义类型变量列表初始化,C++11语法中将initializer_list加入标准库中,
用作初始化器列表构造函数的参数类型。cplusplus.com/reference/initializer_list/initializer_list/
举例:
template<typename T> class vector {
public:
vector(initializer_list<T>); // 初始化器列表构造函数
// ...
};
vector<int> v3 {1,2,3,4,5}; // 具有 5 个元素的 vector
1.std::initializer_list使用场景:
std::initializer_list一般是作为构造函数的参数,C++11对STL中的不少容器就增加std::initializer_list作为参数的构造函数,这样初始化容器对象就更方便了。也可以作为operator=的参数,这样就可以用花括号赋值了。
vector<int> v = { 1,2,3,4 };
list<int> lt = { 1,2 };
// 这里{"sort", "排序"}会先初始化构造一个pair对象
map<string, string> dict = { {"sort", "排序"}, {"insert", "插入"} };
// 使用花括号对容器赋值
v = {10, 20, 30};
我们实现一个日期类Date,然后用列表初始化该类型变量。
class Date
{
public:
Date(int year, int month, int day)
:_year(year)
, _month(month)
, _day(day)
{
cout << "Date(int year, int month, int day)" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1{ 2000,0,0 };
auto d2 = { 2001,1,1 };
cout << typeid(d2).name() << endl;
return 0;
}
//output:
//Date(int year, int month, int day)
//class std::initializer_list<int>
我们发现列表的类型是initializer_list,但我们并没有显示构造以initializer_list为参数的构造函数,d1却照样可以列表初始化,因为对于列表初始化,编译器会优先识别,如果没有初始化器列表作参数的构造函数,编译器会调用其他的构造函数,这里调用的是“Date(int year, int month, int day) {}”。
但对于一些自定义类型,如果我们没有显示实现以初始化器列表为参数的构造函数,使用列表初始化时编译器会报错,如下:
template<class T>
class my_vector
{
public:
typedef T* iterator;
my_vector()
{}
private:
iterator _start = nullptr;
iterator _finish = nullptr;
iterator _end_of_storage = nullptr;
};
int main()
{
std::vector<int> v1(1, 2, 3, 4, 5);//报错,没有对应构造函数
my_vector<int> v2 = (1, 2, 3, 4, 5);//报错,没有对应构造函数
std::vector<int> v3{ 1, 2, 3, 4, 5 };//不报错,标准库里vector实现了以初始化器列表为参数的构造函数
my_vector<int> v4{ 1, 2, 3, 4, 5 };//报错,没有显示实现以初始化器列表为参数的构造函数
std::vector<int> v5 = { 1, 2, 3, 4, 5 };//不报错
my_vector<int> v6 = { 1, 2, 3, 4, 5 };//报错
}
当我们显示实现以初始化器列表为参数的构造函数后,
template<class T>
class my_vector
{
public:
typedef T* iterator;
my_vector()
{}
my_vector(initializer_list<T> l)
{
_start = new T[l.size()];
_finish = _start + l.size();
_end_of_storage = _start + l.size();
iterator vit = _start;
typename initializer_list<T>::iterator lit = l.begin();
while (lit != l.end())
{
*vit++ = *lit++;
}
cout << "my_vector" << endl;
}
private:
iterator _start = nullptr;
iterator _finish = nullptr;
iterator _end_of_storage = nullptr;
};
int main()
{
//std::vector<int> v1(1, 2, 3, 4, 5);//报错,没有对应构造函数
//my_vector<int> v2 = (1, 2, 3, 4, 5);//报错,没有对应构造函数
std::vector<int> v3{ 1, 2, 3, 4, 5 };//不报错,标准库里vector实现了以初始化器列表为参数的构造函数
my_vector<int> v4{ 1, 2, 3, 4, 5 };//不报错,没有显示实现以初始化器列表为参数的构造函数
std::vector<int> v5 = { 1, 2, 3, 4, 5 };//不报错
my_vector<int> v6 = { 1, 2, 3, 4, 5 };//不报错
}
vs2019会输出:
我们也可以显示实现以初始化器列表为参数的赋值运算符重载,如下:
template<class T>
class my_vector
{
public:
typedef T* iterator;
my_vector()
{}
my_vector(initializer_list<T> l)
{
_start = new T[l.size()];
_finish = _start + l.size();
_end_of_storage = _start + l.size();
iterator vit = _start;
typename initializer_list<T>::iterator lit = l.begin();
while (lit != l.end())
{
*vit++ = *lit++;
}
//for (auto e : l)
// *vit++ = e;
cout << "my_vector(initializer_list<T> l)" << endl;
}
vector<T>& operator=(initializer_list<T> l)
{
vector<T> tmp(l);
std::swap(_start, tmp._start);
std::swap(_finish, tmp._finish);
std::swap(_end_of_storage, tmp._end_of_storage);
cout << "vector<T>& operator=(initializer_list<T> l)" << endl;
return *this;
}
private:
iterator _start = nullptr;
iterator _finish = nullptr;
iterator _end_of_storage = nullptr;
};
int main()
{
my_vector<int> v4{ 1, 2, 3, 4, 5 };
my_vector<int> v6 = { 1, 2, 3, 4, 5 };//编译器会优化成直接初始化
}
可以看到,vs2019会对拷贝初始化优化,优化成直接初始化。
五、列表初始化中的“歧义”
在某些情况下,初始化的真实含义依赖于传递初始值时用的是花括号还是圆括号。例如,用一个整数来初始化vector<int>时,整数的含义可能是vector 对象的容量也可能是元素的值。类似的,用两个整数来初始化vector<int>时,这两个整数可能一个是vector 对象的容量,另一个是元素的初值,也可能它们是容量为2的vector 对象中两个元素的初值。通过使用花括号或圆括号可以区分上述这些含义:
vector<int> v1(10); // v1有10个元素,每个的值都是0
vector<int> v2{ 10 }; //v2有1个元素,该元素的值是10
vector<int> v3(10,1);//v3有10个元素,每个的值都是1
vector<int> v4{ 10,1 };//v4有2个元素,值分别是10和1
如果用的是圆括号,可以说提供的值是用来构造(construct)vector 对象的。例如,v1的初始值说明了 vector 对象的容量;v3的两个初始值则分别说明了 vector 对象的容量和元素的初值。
如果用的是花括号,可以表述成我们想列表初始化(list initialize)该 vector 对象。也就是说,初始化过程会尽可能地把花括号内的值当成是元素初始值的列表来处理,只有在无法执行列表初始化时才会考虑其他初始化方式。在上例中,给v2和v4提供的初始值都能作为元素的值,所以它们都会执行列表初始化,vector 对象v2包含一个元素而vector 对象v4包含两个元素。
另一方面,如果初始化时使用了花括号的形式但是提供的值又不能用来列表初始化,就要考虑用这样的值来构造 vector 对象了。例如,要想列表初始化一个含有string对象的 vector 对象,应该提供能赋给 string 对象的初值。此时不难区分到底是要列表初始化 vector 对象的元素还是用给定的容量值来构造 vector 对象:
vector<string> v5{"hi"}; //列表初始化:v5有一个元素
vector<string> v6("hi"); //错误:不能使用字符串字面值构建 vector 对象
vector<string> v7(10); // v7有10个默认初始化的元素
vector<string> v8{ 10,"hi" }; //v8有10个值为"hi"的元素
尽管在上面的例子中除了第二条语句之外都用了花括号,但其实只有v5是列表初始化。要想列表初始化 vector 对象,花括号里的值必须与元素类型相同。显然不能用 int 初始化 string对象,所以v7和v8提供的值不能作为元素的初始值。确认无法执行列表初始化后,编译器会尝试用默认值初始化 vector 对象。
注意:
vector<string> vl{ "a","an","the" };//列表初始化
vector<string> v2("a","an","the");//错误
六、主要参考资料:
1. Cxx_HOPL4_zh/04.md at main · Cpp-Club/Cxx_HOPL4_zh (github.com)
3. 《C++ Primer》中文版(第五版)
4.《C Primer Plus》第六版
文中若有错误之处,还请各位私信或评论区批评指正,感谢大家!