列表初始化(List Initialization)
当初始化列表与内置类型的变量一起使用时,这种初始化形式具有一个重要属性:如果初始化器可能导致信息丢失,编译器将不允许我们列出内置类型的初始化变量。
Code:
long double ld = 3.1415926536;
int a{ld}, b = {ld}; // error:narrowing conversion required
int c(ld), d = ld; // ok: but value will be truncated
编译器拒绝初始化a
和b
,因为使用long double
初始化int
可能会丢失数据。至少ld
的小数部分将被截断,另外ld
中的整数部分可能太大而不适合int
。
这一新特性似乎与C++98区别不大,毕竟我们不太可能直接使用long double
初始化int
。然而在“模版编程和范型编程”中,可能无意中发生这样的初始化!
空指针(Null Pointers)
初始化指针最直接的方法是使用新标准引入的字面量nullptr初始化指针。 nullptr是一个具有特殊类型的字面量,可以转换为任何其他指针类型。 或者我们可以使用字面量0初始化一个指针。
较老的程序有时使用预处理程序变量NULL初始化指针。该变量在cstdlib
头文件中被定义为0。
现代C++程序通常应该避免使用NULL,而应该使用nullptr作为替代。
Code:
int *p1 = nullptr; // equivalent to int *p1 = 0;
int *p2 = 0; // directly initializes p2 from the literal constant 0
// #include <cstdlib>
int *p3 = NULL; // equivalent to int *p3 = 0;
将int
型变量赋给指针是非法的,即使该int
变量的值恰好为0。
Code:
int zero = 0;
int *pi = zero; // error: cannot assign an int to a pointer
初始化指针的注意事项
未初始化的指针是运行时错误(run-time errors)的常见原因。与任何其他未初始化的变量一样,当我们使用未初始化的指针时会发生什么是未定义的。 使用未初始化的指针几乎总会导致运行时崩溃。然而调试这种崩溃往往非常困难。
我们建议初始化所有变量特别是指针。 如果可能仅在定义了欲指向的对象之后定义指针。如果指针没有要指向的对象,那么就将指针初始化为nullptr
或0,这样程序就可以判断这个指针没有指向任何对象。
constexpr变量(constexpr Variables)
常量表达式是一个表达式,其值不能更改,并且可以在编译时进行计算。
Code:
const int max_files = 20; // max_files is a constant expression
const int limit = max_files + 1; // limit is a constant expression
const int sz = get_size(); // sz is not a constant expression
虽然sz
是一个const int
类型的变量,但它不是一个常量表达式,因为它的初始化器的值只有在运行时才能获得。
在新标准下,我们可以通过使用constexpr声明一个变量,从而要求编译器去验证该变量是否为常量表达式。
Code:
constexpr int mf = 20; // 20 is a constant expression
constexpr int limit = mf + 1; // mf + 1 is a constant expression
constexpr int sz = size(); // ok only if size is a constexpr function
虽然我们不能使用普通函数作为constexpr变量的初始化函数,但新标准允许我们将某些函数定义为constexpr。 这些函数必须足够简单,以便编译器可以在编译时对它们进行评估。 我们可以在constexpr变量的初始化器中使用constexpr函数。
通常,最好将constexpr用于打算用作常量表达式的变量。
类型别名(Type Aliases)
通常我们使用typedef
来定义类型别名,新标准引入了另一种通过别名声明来定义类型别名的方法:
Code:
using SI = Sales_item; // SI is a synonym for class Sales_item
别名声明以关键字using
开头,后跟别名和=
。 别名声明将=
左侧的名称定义为右侧显示的类型的别名。类型别名是类型名称,可以出现在类型名称出现的任何位置:
Code:
SI item; // same as Sales_item item
auto类型说明符(The auto Type Specifier)
auto类型说明符是C++11全新引入的。
编写程序过程中我们想要将表达式的值存储在变量中并不罕见。 要声明变量,我们必须知道该表达式的类型。 当我们编写程序时,确定表达式的类型可能会非常困难,有时甚至是不可能的。 在新标准下,通过使用auto类型说明符我们可以让编译器为我们找出类型。 与类型说明符(例如double)命名特定类型不同,auto告诉编译器要从初始化程序中推断出类型。 因为编译器对于auto说明符需要推断的依据,所以使用auto作为其类型说明符的变量必须具有初始化器:
Code:
// the type of item is deduced from the type of the result of adding val1 and val2
auto item = val1 + val2; // item initialized to the result of val1 + val2
此处编译器将根据val1 + val2
返回的类型来推断item
的类型。如果val1
和val2
是类Sales_item
的对象,那么item
将具有类型Sales_item
。如果val1
和val2
的类型是double
,那么item
将是类型double
的变量。
与任何其他类型说明符一样,我们可以使用auto定义多个变量。 因为声明只涉及单个基类型,所以声明中所有变量的初始化器必须具有彼此一致的类型:
Code:
auto i = 0, *p = &i; // ok: i is int and p is a pointer to int
auto sz = 0, pi = 3.14; // error: inconsistent types for sz and pi
符合类型,const和auto
编译器推断auto的类型并不总是与初始化程序的类型完全相同。 相反,编译器会调整类型以符合常规初始化规则。
首先,当我们使用引用时,我们实际上正在使用被引用的对象的一个引用。 特别是当我们使用引用作为初始化器时,初始化器是相应的对象。 编译器将该对象的类型用于auto的类型推导:
Code:
int i = 0, &r = i;
auto a = r; // a is an int (r is an alias for i, which has type int)
其次,auto通常会忽略顶层const。 但初始化时会保持低层const,例如初始化器是指向const的指针时:
Code:
const int ci = i, &cr = ci;
auto b = ci; // b is an int (top-level const in ci is dropped)
auto c = cr; // c is an int (cr is an alias for ci whose const is top-level)
auto d = &i; // d is an int*(& of an int object is int*)
auto e = &ci; // e is const int*(& of a const object is low-level const)
如果我们想要推理出保留顶层const的类型,需要进行显示声明:
Code:
const auto f = ci; // deduced type of ci is int; f has type const int
我们还可以指定对auto推理出的类型的引用。 通常的初始化规则仍然适用:
Code:
auto &g = ci; // g is a const int& that is bound to ci (can't ignore the top-level const in ci)
auto &h = 42; // error: we can't bind a plain reference to a literal
const auto &j = 42; // ok: we can bind a const reference to a literal
当我们要求引用由auto推理出的类型时,初始化器中的顶层const不会被忽略。 通常当我们将引用绑定到初始化器时,const就不是顶层的了。
当我们在同一个语句中定义多个变量时,重要的是要记住引用或指针是特定声明符的一部分,而不是声明的基本类型的一部分。 通常初始化器必须提供一致的自动推导类型:(注:auto用于推导基本类型)
Code:
auto k = ci, &l = i; // k is int; l is int&
auto &m = ci, *p = &ci; // m is a const int&;p is a pointer to const int
// error: type deduced from i is int; type deduced from &ci is const int
auto &n = i, *p2 = &ci;
注:使用过程中应该时刻注意由auto推理出的类型!
decltype类型说明符(The decltype Type Specifier)
有时我们想要定义一个变量,该变量具有编译器从表达式推导出的类型,但不希望使用该表达式来初始化变量。 对于这种情况,新标准引入了第二个类型说明符decltype
,它返回其操作数的类型。 编译器分析表达式以确定其类型,但不评估表达式:
Code:
decltype(f()) sum = x; // sum has whatever type f returns
这里,编译器不调用函数f()
,但它使用函数f()
返回的类型作为sum的类型。
decltype
处理顶层const
和引用的方式与auto
的处理方式略有不同。 当我们对一个变量应用decltype
时,decltype
返回该变量的类型,包括顶层const
和引用:
Code:
const int ci = 0, &cj = ci;
decltype(ci) x = 0; // x has type const int
decltype(cj) y = x; // y has type const int& and is bound to x
decltype(cj) z; // error: z is a reference and must be initialized
值得注意的是,decltype
是唯一一个定义为引用的变量不被视为它所引用的对象的同义词的说明符。
decltype和引用
当我们将decltype
应用于不是变量的表达式时,我们得到该表达式返回的类型。某些表达式将导致decltype
生成引用类型, 通常decltype
为该类表达式返回一个引用类型,这些表达式产生的对象可以作为左值:
Code:
// decltype of an expression can be a reference type
int i = 42, *p = &i, &r = i;
decltype(r + 0) b; // ok: addition yields an int; b is an (uninitialized) int
decltype(*p) c; // error: c is int& and must be initialized
此处r
是一个引用,所以decltype(r)
是一个引用类型。如果我们想要获得r
引用的类型,我们可以将r
应用到表达式中(如:r + 0
),这个表达式将产生一个不是引用类型的值。
另一方面,解除引用运算符是decltype
返回引用的表达式的一个例子。 正如我们所见,当我们解引用指针时,我们得到指针指向的对象。 而且,我们可以对该对象进行赋值。 因此,由decltype(*p)
推导出的类型是int&
,而不是无格式的int
。
decltype
和auto
之间的另一个重要区别是decltype
所做的推论取决于其给定表达式的形式。 令人困惑的是将变量名用括号括起来将会影响decltype
返回的类型。 当我们将decltype
应用于没有任何括号的变量时,我们得到该变量的类型。 如果我们将变量的名称包装在一组或多组括号中,编译器会将变量及括号计算为一个表达式。 变量是一个可以作为左值进行赋值的表达式。结果是decltype
作用于这样的表达式上将产生一个引用:
Code:
// decltype of a parenthesized variable is always a reference
decltype((i)) d; // error: d is int& and must be initialized
decltype(i) e; // ok: e is an (uninitialized) int
decltype((variable))
decltype(variable)
variable
参考文献
[1] Lippman S B , Josée Lajoie, Moo B E . C++ Primer (5th Edition)[J]. 2013.