文章目录
- 初始化和赋值的区别
- 什么是默认初始化?
- 列表初始化
- 列表初始化的使用场景
- 不适合使用列表初始化的场景
- 类内初始值
- 混用string对象和C风格字符串
- 数组与vector对象
- 关于vector对象
- 两者间的初始化关系
- 直接初始化与拷贝初始化
初始化和赋值的区别
- 初始化的含义是创建变量时赋予其一个初始值
- 赋值的含义时把对象的当前值擦除,而已一个新值来替代。
什么是默认初始化?
如果定义变量时没有指定初值,则变量被默认初始化,此时变量被赋予了 “默认值”。默认值到底是什么由变量类型决定,同时定义变量的位置也会对此有影响。
内置类型的默认值由定义的位置决定, 定义于任何函数体之外的变量被初始化为0;定义于函数体内部的内置类型将不被初始化。一个未被初始化的内置类型变量的值是未定义的,试图拷贝或以其他形式访问此类值将引发错误。
列表初始化
C++定义了初始化的好几种不同形式,通常定义一个变量并初始化的方式有以下四种:
int x = 0;
int x = {0};
int x{0};
int x(0);
使用花括号来初始化变量在C++11新标准中得到了全面应用。这种初始化的形式被程为列表初始化(list initialization)。现在,无论是初始化对象,还是某些时候为对象赋新值,都可以使用列表初始化。
列表初始化的使用场景
- 列表初始化可被用于以下场景:
// Vector 接收了一个初始化列表。
vector<string> v{"foo", "bar"};// 不考虑细节上的微妙差别,大致上相同。
vector<string> v = {"foo", "bar"};// 可以配合 new 一起用。
auto p = new vector<string>{"foo", "bar"};// map 接收了一些 pair, 列表初始化大显神威。
map<int, string> m = {{1, "one"}, {2, "2"}};// 初始化列表也可以用在返回类型上的隐式转换。
vector<int> test_function() { return {1, 2, 3}; }// 初始化列表可迭代。
for (int i : {-1, -2, -3}) {}// 在函数调用里用列表初始化。
void TestFunction2(vector<int> v) {}
TestFunction2({1, 2, 3});
- 用户自定义类型也可以定义接收
std::initializer_list<T>
的构造函数和赋值运算符,以自动列表初始化:
class MyType {public:// std::initializer_list 专门接收 init 列表。MyType(std::initializer_list<int> init_list) {for (int i : init_list) append(i);}MyType& operator=(std::initializer_list<int> init_list) {clear();for (int i : init_list) append(i);}
};
MyType m{2, 3, 5, 7};
- 最后,列表初始化也适用于常规数据类型的构造,哪怕没有接收
std::initializer_list<T>
的构造函数。
// MyOtherType 没有 std::initializer_list 构造函数,
// 直接上接收常规类型的构造函数。
class MyOtherType {public:explicit MyOtherType(string);MyOtherType(int, string);
};
MyOtherType m = {1, "b"};
// 不过如果构造函数是显式的(explict),就不能用 `= {}` 了。
MyOtherType m{"b"};
不适合使用列表初始化的场景
值得注意的是,当用于内置类型的变量时,如果使用列表初始化且初始值存在丢失信息的风险,则编译器将报错:
long double ld = 3.1415926536;
int a{ld}, b = {ld}; // 错误:转换未执行,因为存在丢失信息的危险
int c(ld), d = ld; // 正确:转换执行,且确实丢失了部分值
使用 long double
的值初始化 int
变量时可能丢失数据,所以编译器拒绝了 a
和 b
的初始化请求。其中,至少 ld
的小数部分会丢失掉,而且某些情况下 int
也可能存不下 ld
的整数部分。
同时,千万别直接列表初始化 auto
变量,因为可读性不高:
auto d = {1.23}; // d 类型是 std::initializer_list<double>
auto d = double{1.23}; // d 类型为 double, 并非 std::initializer_list.
类内初始值
C++11标准规定,可以为数据成员提供一个类内初始值(in-class initializer)。创建对象时,类内初始值将用于初始化数据成员。没有初始值的成员将被默认初始化。
对类内初始值的限制如下:
- 放在花括号里
- 放在等号右边
- 不能使用圆括号
因为我们无法避免这样的情况,有时函数声明也会用到圆括号:
class Widget
{
private: typedef int x;int z(x);
};
因此用圆括号为类内成员提供类内初始值容易产生二义性,编译器会觉得该语句语义不明。
混用string对象和C风格字符串
我们都知道允许使用字符串字面值来初始化string对象:
string s("Hello World!");
C++规定,任何出现字符串字面值的地方都可以用以空字符结束的字符数组来替代:
- 允许使用以空字符结束的字符数组来初始化string对象或为string对象赋值。
- 在string对象的加法运算中允许使用以空字符结束的字符数组作为其中一个运算对象(不能两个对象都是);在string对象的复合赋值运算中允许是用以空字符结束的字符数组作为右侧的运算对象。
上述性质反过来并不成立:如果程序的某处需要一个C风格字符串,无法直接用string对象来替代它。
例如:不能使用string对象直接初始化指向字符的指针。为了实现这一功能,string专门提供了一个名为c_str的成员函数:
char *str = s; // 错误:不能用string对象初始化char*
const char *str = s.c_str; // 正确
函数返回结果使用一个指针,该指针指向一个以空字符结束的字符数组,而这个数组所存的数据恰好与哪个string对象的一样。结果指针的类型是const char*,从而确保我们不会改变字符数组的内容。
PS:由于我们无法保证c_str函数返回的数组一直有效,如果后续的操作改变了s的值就可能让之前返回的数组失去效用。因此,如果执行完c_str()函数后程序想一直都能使用其返回的数组,最好将该数组重新拷贝一份。
数组与vector对象
关于vector对象
vector是模板而非类型,由vector生成的类型必须包含vector中元素的类型,如:
vector<int>
两者间的初始化关系
- 不允许使用一个数组为另一个内置类型的数组赋初值
- 不允许使用vector对象初始化数组
- 允许使用数组来初始化vector对象
实现第三点只需要指明要拷贝区域的首元素地址和尾后地址就可以了:
int int_arr[] = {0, 1, 2, 3, 4, 5};
vector<int> ivec(begin(int_arr), end(int_arr));
用于创建ivec 的两个指针实际上指明了用来初始化的值在数组int_arr中的位置,分别用标准库函数begin和end来计算int_arr的首指针和尾后指针。在最终结果中,ivec将包含6个元素,它们的次序和值都与数组int_arr完全一样。
亦可使用数组的一部分来初始化vector对象:
vector<int> subVec(int_arr + 1, int_arr + 4);
// 拷贝三个元素:int_arr[1]、int_arr[2]、int_arr[3]
直接初始化与拷贝初始化
- 使用直接初始化时,编译器进行函数匹配来选择与我们提供的参数最匹配的构造函数。
- 使用拷贝初始化时,编译器将右侧运算对象拷贝到正在创建的对象中,按需选择是否进行类型转换。
拷贝初始化通常使用拷贝构造函数来完成。
拷贝初始化不仅在我们用 = 定义变量时会发生,也会在下列情况中发生:
- 将一个对象作为实参传递给一个非引用类型的形参
- 从一个返回类型为非引用类型的函数返回一个对象
- 用花括号列表初始化一个数组中的元素或一个聚合类中的成员
拷贝初始化受到explicit类型的构造函数的限制:
需要类型转化的拷贝初始化的过程是这样的:
- 先隐式调用给定类型的构造函数,将等号右边的值作为实参传递给构造函数,从而生成一个临时的给定类型的对象。(类型转换通过本步完成,如果是无需类型转换的拷贝初始化则没有本步)
- 再让想要初始化的对象隐式调用拷贝构造函数,并将临时对象作为实参传给拷贝构造函数,从而完成初始化。
这样的操作对于explicit类型的构造函数是行不通的,因为无法隐式调用一个explicit的构造函数生成一个临时对象。 换言之,explicit类型的构造函数是抑制隐式类型转换的。
因此面对explicit类型的构造函数只能执行直接初始化、或者是无需类型转换的拷贝初始化:
// error:vector接受大小参数的构造函数是explicit的
vector<int> vi = 10; vector<int> vi(10); // 正确:直接初始化string s(10, 'x'); // 正确:直接初始化
string s1 = s; // 正确:无需类型转换的拷贝初始化
如果希望使用有给explicit的构造函数,必须显式地使用:
void f(vector<int>); // f的参数进行拷贝初始化
f(10);
// error:不能隐式地使用explicit的构造函数构造一个临时vector
// 因为无法执行从int(也就是10的类型)到vector的类型转换
f(vector<int>(10));
// 正确:显式地使用explicit的构造函数
// 为vectoc的构造函数传入10作为实参,构造一个临时的vector
// 用临时的vector初始化f