C++11标准发布已有一段时间了, 维基百科上有对C++11新标准的变化和C++11新特性介绍的文章. 我是一名C++程序员,非常想了解一下C++11. 英文版的维基百科看起来非常费劲,而中文版维基百科不是知道是台湾还是香港人翻译的然后由工具转换成简体中文的,有些术语和语言习惯和大陆程序不一样! 我决定结合这两个版本按照我自己的习惯把这篇文章整理到我的博客中.分享给关注我和关注C++11的朋友们. 当然了, 本人水平有限,英语水平也很一般,就把这个过程当做学习C++11的过程吧.文章中肯定会有很多错误或描述不恰当的地方. 非常希望看到的朋友能给我指出来.
以下是关于C++11的英文版本和中文版本维基百科的链接:
http://en.wikipedia.org/wiki/C++11
http://zh.wikipedia.org/wiki/C++0x
目录- 0 简介
- 1候选变更
- 2C++核心语言的扩充
- 3核心语言的运行期表现强化
- 3.1右值引用和 move 语义
- 3.2泛化的常数表示式
- 3.3对POD定义的修正
- 4核心语言建构期表现的加强
- 4.1外部模板
- 5核心语言使用性的加强
- 5.1初始化列表
- 5.2统一的初始化
- 5.3类型推导
- 5.4基于范围的 for 循环
- 5.5Lambda函数与表达式
- 5.6一种新的函数语法
- 5.7对象建构的改良
- 5.8显式虚函数重载
- 5.9空指针常量
- 5.10强类型枚举
- 5.11 右尖括号
- 5.12显式类型转换操作符
- 5.13模板的别名
- 5.14无限制的unions
- 6核心语言性能的提升
- 6.1 可变参数模板
- 6.2新的字符串字面值
- 6.3用户自定义的字面值
- 6.4多线程内存模型
- 6.5 线程局部存储
- 6.6 对象的显式的特殊成员函数
- 6.7long long int类型
- 6.8静态断言
- 6.9允许sizeof运算符作用在类型的数据成员上,无须具体的对象
- 6.10 控制和查询对象的对齐方式
- 6.11 允许实现垃圾回收机制
- 6.12 属性
- 7C++标准程序库的变化
- 7.1标准库组件的升级
- 7.2 线程支持
- 7.3元组类型
- 7.4散列表
- 7.5正则表达式
- 7.6通用智能指针
- 7.7可扩展的随机数功能
- 7.8 封装引用
- 7.9多态函数对象包装器
- 7.10用于元编程的类型特征
- 7.11用于计算函数对象返回类型的统一方法
- 7.12 原本计划加入但没有加入C++11的特性
- 7.13被移除或废弃的特性
0.简介
C++11,之前被称作C++0x,即ISO/IEC 14882:2011,是目前的C++编程语言的正式标准。它取代第二版标准ISO/IEC 14882:2003(第一版ISO/IEC 14882:1998发布于1998年,第二版于2003年发布,分别通称C++98以及C++03,两者差异很小)。新的标准包含了几个核心语言增加的新特性,而且扩展C++标准程序库,并入了大部分的C++ Technical Report 1程序库(数学的特殊函数除外)。最新的消息被公布在 ISO C++ 委员会网站(英文)。 ISO/IEC JTC1/SC22/WG21 C++ 标准委员会计划在2010年8月之前完成对最终委员会草案的投票,以及于2011年3月召开的标准会议完成国际标准的最终草案。然而,WG21预期ISO将要花费六个月到一年的时间才能正式发布新的C++标准。为了能够如期完成,委员会决定致力于直至2006年为止的提案,忽略新的提案。最终,于2011年8月12日公布,并于2011年9月出版。2012年2月28日的国际标准草案(N3376)是最接近于现行标准的草案,差异仅有编辑上的修正。 像C++这样的编程语言,通过一种演化的过程来发展其定义。这个过程不可避免地将引发与现有代码的兼容问题。不过根据Bjarne Stroustrup(C++的创始人,标准委员会的一员)表示,新的标准将几乎100%兼容现有标准。
1.来自以往版本标准的变更(候选变更)
C++的修订范围包括核心语言以及标准程序库。在开发2011版标准的各个特性的过程中,标准委员会坚持以下指导思想: * 维持与C++98,可能的话还有C之间的兼容性与稳定性; * 尽可能通过通过标准程序库来引进新的特性, 而不是扩展核心语言; * 能够促进编程技术的变更优先; * 改进 C++ 以帮助系统和程序库的设计,而不是引进只对特定应用有用的新特性; * 增强类型安全,给现行不安全的技术提供更安全的替代方案; * 增强直接与硬件协同工作的性能和能力; * 为现实世界中的问题提供适当的解决方案; * 实行零负担原则(如果某些功能要求的额外支持,那么只有在该功能被用到时这些额外的支持才被用到); * 使C++易于教学
注重对初学者的关注,因为他们构成了计算机程序员的主体。也因为许多初学者不愿扩展他们的C++知识,他们仅限于掌握C++中自己所专精的部分.
2.核心语言的扩充
C++标准委员会的一个职责是开发语言核心. 核心语言被大幅改进的领域包括: 多线程支持, 泛型编程支持, 统一的初始化和提高性能. 这篇文章将核心语言的特性和变化大致分为: 提高运行期性能, 提高编译期性能, 增强可用性, 和新特性4大类. 某些特性可以被划分到多个分类中, 但只会在主要体现该特性的分类中讨论一次.
3.提高核心语言运行性能 以下特性主要是为了提高性能提或计算速度等设计的.
3.1 右值引用和移动构造 在C++03及之前的标准中,临时对象(称为右值"R-values",因为通常位于赋值运算符的右边)的值是不能改变的,和C语言一样, 且无法和 const T& 类型做出区分。尽管在某些情况下临时对象的确会被改变,甚至有时还被视为是一个有用的漏洞。C++11新增加了一个非常量引用(non-const reference)类型,称作右值引用(R-value reference),标记为T &&。右值引用所引用的临时对象可以在该临时对象被初始化之后做修改,这是为了允许 move 语义。 C++03 性能上被长期诟病的问题之一,就是其耗时且不必要的深拷贝。深拷贝会隐式地发生在以传值的方式传递对象的时候。例如,std::vector内部封装了一个C风格的数组和其元素个数,如果创建或是从函数返回一个std::vector的临时对象,要将其保存起来只能通过生成新的std::vector对象并且把该临时对象所有的数据复制进去。该临时对象和其拥有的內存会被销毁。(为简单起见,这里忽略了编译器的返回值优化) 在 C++11中,std::vector有一个"移动构造函数",对某个vector的右值引用可以单纯地从右值复制其内部C风格数组的指针到新的vector中,然后将右值中的指针置空。因为这个临时对象不会再被使用,没代码会再访问这个空指针, 而且因为这个临时对象的内部指针是NULL,所以当这个临时对象离开作用域时它的内存也不会被释放掉.所以,这个操作不仅没有代价高昂的深拷贝, 还是安全的,对用户不可见的!
这个操作不需要数组的复制,而且空的临时对象的析构也不会销毁内存。返回vector临时对象的函数只需要返回std::vector&&。如果vector没有move 构造函数,那么就会调用常规拷贝构造函数。如果有,那么就会优先调用move构造函数,这能够避免大量的内存分配和内存拷贝操作。
右值引用不用对标准库之外的代码做任何改动就可以为已有代码带来性能上的提升. 返回值类型为std::vector的函数返回了一个std::vector类型的临时对象,为了使用移动构造不需要显示地将返回值类型改为std::vector&&, 因为这样的临时对象会被自动当作右值引用. 但是在c++03中, std::vector没有移动构造函数, 带有const std::vector& 参数的拷贝构造会被调用, 这会导致大量内存分配和拷贝动作. 出于安全考虑, 需要施加一些限制! 一个已命名的变量即使声明为右值,也不会被视为右值.想要获取一个右值,应该使用模板函数std::move(). 右值引用也可以在特定情况下被修改, 主要是为了与移动构造函数一起使用! 由于"右值引用"这个词的自然语义,以及对"左值引用"(常规引用)这个词的修正, 右值引用可以让开发者提供完美的函数转发! 与可变参数模板结合时, 这个能力让模板函数能够完美地将参数转发给带有这些参数的另一个函数.这对构造函数的参数转发最为有用,创建一个能够根据特定的参数自动调用适当的构造函数的工厂函数.
3.2 泛化的常数表达式
C++一直以来都有常量表达式的概念.这种表达式就像3+4这种在编译期和运行时都能得到相同结果的表达式. 常量表达式给编译器提供了优化的机会, 编译器计算出他们的值并把结果硬编码到程序中. 并且C++规格文档中有很多地方要求使用常量表达式. 例如,定义一个数组需要常量表达式(来指定数组大小), 枚举值必须是常量表达式. 然而,常量表达式中从来都不允许调用函数或创建对象. 所以,像下面这样的简单代码却是非法的:
int get_five() {return 5;} int some_value[get_five() + 7]; // 创建一个包含12个整数的数组. 这种形式在C++中是非法的.
这在C++03中是非法的, 因为get_five() + 7不是常量表达式. C++03的编译器在编译期没办法知道get_five()是常量.因为从理论上讲, 这个函数可以影响(改变)一个全局变量或调用其它非运行时常量函数等. C++11引入了constexpr关键字, 允许用户去保证一个函数或对象的构造函数是一个编译期常量.上面的例子可以写成下面这样:
constexpr int get_five() {return 5;} int some_value[get_five() + 7]; // 创建一个包含12个整数的数组. 这种形式在C++11中是合法的.
这样可以让编译器理解并验证get_five()是一个编译期常量!作用在函数上的constexpr关键字对函数的行为施加了一些限制. 首先, 这个函数的返回值类型不能是void; 其次, 在函数体中不能声明变量或新类型; 第三, 函数体内只能包含声明语句,空语句和单个return语句且,return语句中的表达式也必须是常量表达式. 在c++11之前, 变量的值只有在变量被声明为const类型,有常量表达式初始化它, 并且是整型或枚举类型时才能用在常量表达式中. C++11去掉必须是整数或枚举的限制,如果定义变量时用了constexpr关键字:
constexpr double earth_gravitational_acceleration = 9.8; constexpr double moon_gravitational_acceleration = earth_gravitational_acceleration / 6.0;
这种数据变量是隐式的常量, 必须用常量表达式初始化.想要构造用户定义类型的常量值,构造函数也必须用constexpr声明.
3.3 对POD定义的修正 在C++03中, 类或结构体必须遵守几条规则才能认为是POD类型.符合这种定义的类型能够产生与C兼容的对象(内存)布局, 而且可以被静态初始化.C++03标准对与C兼容或可以静态初始化的类型有严格的限制,尽管不是因技术原因导致编译器不接受这样的编码.如果创建了一个C++03的POD类型,又想要增加一个非虚成员函数, 那么这个类型就不再是POD类型了, 不能静态初始化,并且与C不兼容了, 尽管没有改变内存布局.
C++11将POD的概念拆分成了两个独立的概念:平凡的(trivial)和标准布局(standard-layout), 放宽了几条POD规则. 一个平凡的(trivial)类型可以静态初始化. 这意味着可以通过memcpy来拷贝数据,而不必通过拷贝构造函数. 平凡类型的变量的生命期是从分配存储空间开始的,而不是从构造完成开始.
平凡的类和结构体定义如下:
1 有一个平凡的默认构造函数. 这可以使用默认构造函数语法, 例如SomeConstructor() = default;
2 有平凡的拷贝和移动构造函数, 可以使用默认语法.
3 有平凡的拷贝和移动赋值运算符, 可以使用转变语法.
4 有一个平凡的析构函数, 必须是非虚函数.
只有当一个类没有虚函数,没虚基类时它的构造函数才是平凡的. 拷贝和移动操作还要求类的所有非静态数据成员都是平凡的.
一个类型是标准布局的,就意味着它将以与C兼容的方式来排列和打包它的成员.标准布局的类和结构体定义如下:
1 没有虚函数
2 没有虚基类
3 所有的非静态数据成员都有相同的访问控制 (public, private, protected)
4 所有的非静态数据成员, 包括基类中的, 都要在继承体系中的同一个类中.
5 以上规则也适用于类体系中的所有基类和所有非静态数据成员.
6 没有和第一个定义的静态数据成员相同类型的基类
如果一个类型是平凡的(trivial),是标准布局的, 并且所有的非静态数据成员和基类都是POD类型的, 那么这个类型就是POD类型.
4 核心语言建构期性能提高
4.1 外部模板 在C++03中,只要在编译单元内遇到被完整定义的模板,编译器都必须将其实例化(instantiate)。这会大大增加编译时间,特别是模板在许多编译单元内使用相同的参数实例化。没有办法告诉C++不要引发模板的实例化.C++11引入了外部模板声明, 就像外部数据声明一样. C++03用下面的语法迫使编译器实例化一个模板:
template class std::vector; 而C++11提供下面的语法告诉编译器在当前编译单元中不要实例化这个模板:
extern templateclass std::vector;
5 核心语言可用性的增强
这些特性存在的主要目的是为了让C++更使用. 这些特性可以改进类型安全, 最小化代码重复, 尽可能减少错误代码等.
5.1 初始化列表
C++03从C语言继承了初始化列表这一特性. 在一对大括号中列出参数的方式来给出一个结构体或者数组, 这些参数值按照各个成员在结构体中的定义顺序来排列.这些初始化列表是递归的,所以一个结构体数组或包含另一个结构体的结构体可以使用它们.
struct Object { float first; int second; }; Object scalar = {0.43f, 10}; //One Object, with first=0.43f and second=10 Object anArray[] = {{13.4f, 3}, {43.28f, 29}, {5.934f, 17}}; //An array of three Objects
这对于静态初列表或者只想把结构体初始化为某个特定值而言是非常有用的. C++提供了构造函数来初始化对象, 但这没有初始化列表方便.C++03只允许符合POD定义的类型使用初始化列表,非POD的类型不能使用,就连相当有用的STL容器std::vector也不行.C++11扩展了初始化列表, 使用它可以用在所有类上,包括像vector这样的标准容器. C++11把初始化列表的概念绑到一个叫做std::initializer_list的模板上.这允许构造函数或其他函数将初始化列表做为参数.例如:
class SequenceClass { public: SequenceClass(std::initializer_list list); };
这使得可以从一串整数来创建SequenceClass对象, 例如: SequenceClass some_var = {1, 4, 5, 6}; 这个构造函数是一种特殊的构造函数,叫做初始化列表构造函数(initializer-list-constructor).有这种构造函数的类在统一初始化中会被特殊对待(详见5.2) 类型std::initializer_list是个第一级的C++11标准程序库类型. 但是,它们只能由C++11通过{}语法来静态构造!这个列表一经构造便可复制,虽然这只是copy-by-reference.初始化列表是常数;一旦被创建,其成员均不能被改变,成员中的数据也不能够被改变. 因为初始化列表是真实类型,除了构造函数之外还能够被用在其他地方。常规的函数能够使用初始化列表作为参数。例如:
void FunctionName(std::initializer_list list); FunctionName({1.0f, -3.45f, -0.4f}); 标准容器也能够以这种方式初始化: std::vector v = { "xyzzy", "plugh", "abracadabra" }; std::vector v({ "xyzzy", "plugh", "abracadabra" }); std::vector v{ "xyzzy", "plugh", "abracadabra" }; // 参见下面 "统一的初始化"
5.2 统一的初始化
C+03在初始化类型方面有着许多问题.初始化类型有数种方法,而且交换使用时不会都产生相同结果。传统的建构式语法,看起来像是函数声明,而且必须采取一些步骤保证不破坏编译器那些最让人恼火的解析规则.只有聚合体和POD类型能够用集合式初始化(通过SomeType var = {}; 形式的语法) C++11提供了一个完全统一的可以用在任何类型的对象的初始化语法. 它扩展了初始化列表语法:
struct BasicStruct { int x; double y; }; struct AltStruct { AltStruct(int x, double y) : x_{x}, y_{y} {} private: int x_; double y_; }; BasicStruct var1{5, 3.2}; AltStruct var2{2, 4.3};
var1初始化行为就像聚合初始化一样.也就是说,每个数据成员就是一个对对象, 按顺序从初始化列表中拷贝一个对应的值来初始化它们.如果有需要, 会进行隐式类型转换.如果存在向下类型转换(转换后的数据类型不能表示原数据类型,转换后可能有数据丢失,例如将unsigned转换成int), 那么这个程序就是病态的,会导致编译失败. var2的初始化则是简单地调用构造函数. 统一的初始化还可做下面这件事:
struct IdString { std::string name; int identifier; }; IdString get_string() { return {"foo", 42}; //注意,这里没有指定具体类型. }
统一初始化不会取代构造函数语法,还是有一些时候是需要构造函数语法的.如果一个类有初始化列表构造函数(TypeName(initializer_list);),假定它有资格成为构造函数之一(我们知道,一个类可以有多个构造函数),那么它的优先级会高于其它形式的构造函数.C++11版本的std::vector就有一个初始化列表构造函数.这意味着 std::vector the_vec{4};会调用初始化列表构造函数,而不是调用以vector大小为唯一参数的构造函数. 要访问后一个构造函数, 用户必须直接使用标准构造函数语法.
5.3 类型推导
在C++03(还有C)中,必须显式指定变量的类型.然而,随着模板类型和模板元编程技术的出现,某些东西的类型,尤其是函数的返回类型,可能不是那么容易表示的了. 在这种情况下,将中间结果存储在某个变量中是件很困难的事情.可能需要去了解特定的模板元编程库的内部实现. C++11提供两种方法来缓解上述问题. 一,定义有显式初始化的变量可以用auto关键字来自动确定变量类型,这将用初始化表达式的类型来创建变量:
auto some_strange_callable_type = boost::bind(&some_function, _2, _1, some_object); auto other_variable = 5;
some_strange_callable_type的类型很简单, 就是boost::bind模板函数返回值的类型.作为编译器语义分析责任的一部份,编译很容易确定这个类型,但程序员就没那么容易确定了.otherVariable 的类型同样也是定义明确的,程序员很容易就能判别。它是个int(整数),就和整数字面值的类型一样。
另外,关键字decltype可以用来在编译期确定表达式的类型.例如:
int some_int; decltype(some_int) other_integer_variable = 5;
decltype 和 auto 一起使用会更为有用,因为 auto 参数的类型只有编译器知道.然而 decltype对于那些大量运用运算符重载和类型特化来编码的表达式非常有用。auto对减少代码冗余也很有用.比如说, 程序员不用像下面这样写代码:
for (std::vector::const_iterator itr = myvec.cbegin(); itr != myvec.cend(); ++itr) //而可以用更简短的形式: for (auto itr = myvec.cbegin(); itr != myvec.cend(); ++itr)
这两种形式的差距会随着你使用的容器的嵌套层次而增加, 这种情况下typedef也是一种减少代码的好方法!由decltype得出的类型可以和由auto推导出的类型不同:
#include int main() { const std::vector v(1); auto a = v[0]; // a 是 int 类型 decltype(v[1]) b = 1; // b 是 const int& 类型, 是std::vector::operator[](size_type) const // 的返回类型 auto c = 0; // c 是 int 类型 auto d = c; // d 是 int 类型 decltype(c) e; // e 是 int 类型, c变量的类型 decltype((c)) f = c; // f 是int&类型, 因为(c)是一个左值 decltype(0) g; // g 是 int 类型, 因为0是一个右值 }
5.4 基于范围的for循环
在C++03中,要遍历一个list中的元素需要很多代码.其它语言实现支持"糖块句法",允许程序通过一个简单的"foreach"语句自动遍历list中的元素.其中之一就是Java语言, 它从5.0开始支持增强的for循环. C++11增加了一个类似的特性, for语句可以简单地遍历列表中的元素.
int my_array[5] = {1, 2, 3, 4, 5}; // double the value of each element in my_array: for (int &x : my_array) { x *= 2; }
这种形式的for语句叫作"基于范围的for语句",它会遍历列表中的每一个元素.可以用在C风格数组,初始化列表和那些带有能返回迭代器的begin()和end()函数的类型上.所有提供了begin/end的标准容器都可以使用基于范围的for语句.
5.5 Lamda函数与表达式
C++11提供了创建匿名函数的能力,叫做Lamda函数. 具体内容请参考: http://www.cnblogs.com/pzhfei/archive/2013/01/14/lambda_expression.html
5.6 一种新的函数语法
标准C的函数声明语法对C语言的特性集而言完全足够了. 因为C++从C发展而来, 保留了C的基本语法并在需要的地方进行了扩展. 然而,C++的结构变得更加复杂了,暴露出了很多的局限性,尤其是模板函数的声明.下面的例子在C++03中是不允许的:
template Ret adding_func(const Lhs &lhs, const Rhs &rhs) {return lhs + rhs;} //Ret must be the type of lhs+rhs
Ret的类型是lhs+rhs的结果的类型.就算用前面提到的C++11中的decltype,也是不行的:
template decltype(lhs+rhs) adding_func(const Lhs &lhs, const Rhs &rhs) {return lhs + rhs;} //Not legal C++11
这不是合法的C++,因为lhs和rhs还没定义;解析器解析完函数原型的剩余部分之前,它们还不是有效的标识符. 为此, C++11引入了一种新的函数声明语法,叫做后置返回类型(trailing-return-type).
template auto adding_func(const Lhs &lhs, const Rhs &rhs) -> decltype(lhs+rhs) {return lhs + rhs;}
这种语法可以用到更普通的函数声明和定义上:
struct SomeStruct { auto func_name(int x, int y) -> int; }; auto SomeStruct::func_name(int x, int y) -> int { return x + y; }
关键字auto的这种用法与在自动类型推导中有所不同.
5.7 对象构造的改进
C++03中类的构造函数不允许调用该类的其它构造函数;每个构造函数都必须自己或者调用一个公共的成员函数来构造类的全部成员.例如:
class SomeType { int number; public: SomeType(int new_number) : number(new_number) {} SomeType() : number(42) {} }; class SomeType { int number; private: void Construct(int new_number) { number = new_number; } public: SomeType(int new_number) { Construct(new_number); } SomeType() { Construct(42); } };
而且,基类的构造函数不能直接暴露给派生类;每个派生类必须实现自己的构造函数哪怕基类的构造函数已经够用了.非静态数据成员不能在声明的地方初始化.它们只能在构造函数中初始化.
C++11为这些问题提供了解决方案.C++11允许构造函数调用另一个构造函数(叫做委托构造).这允许构造函数利用其它构造函数的行为而只需增加少量的代码.C#,java和D语言都提供了这种功能. C++的语法如下:
class SomeType { int number; public: SomeType(int new_number) : number(new_number) {} SomeType() : SomeType(42) {} };
注意:这个例子可以通过给new_number设定一个默认参数来达到相同的效果.但是,这种新语法可以让这个默认值在实现中来设置而不是在接口中设置.这带来的一个好处就是,对库代码的维护者而言,在接口中(头文件中)声明默认值,这个默认值被嵌入到了调用端;要改变这个默认值的话,调用端的代码都需要重新编译.但委托构造可以在实现中(CPP文件中)来改变这个默认值, 这样调用端的代码就不需要重新编译,只用重新编译这个库就可以了. 还有一个附加说明: C++03认为,构造函数执行完了一个对象就被构造好了. 而C++11则认为,只要任何一个构造函数执行完了,对象就算构造完成了. 由于可能有多构造函数会被执行,C++11的这种做法就意味着,所有的委托构造函数都将在一个已经用它自己类型完全构造好的对象上执行.这句话什么意思呢?举个例子, B 继承自 A, B的构造函数里调用了A的构造函数;当A的构造函数执行完以后,就已经有一个A类的对象构造完成了.而这时B的构造函数不会再构造一个新对象,而是把那个A对象改造成B类的对象(这是我的推测).再举一个例子,类C有两个构造函数C1和C2, C2调用了C1. 当C1执行完后,已构造好了一个C类对象.而这时C2的代码会直接作用在这个对象上,不会再构造一个新对象.C++03就会构造2个对象,其中一个是临时对象. 对于基类的构造函数,C++11允许一个类指定要不要继承基类的构造函数.注意,这是一个"全部"或"全不"的特性,要么继承基类的全部构造函数,要么一个都不继承. 此外,对多重继承有一些限制,从多个基类继承而来的构造函数不可以有相同的函数签名(signature).而派生类的新加入的构造函数也不可以和继承而来的基类构造函数有相同的函数签名,因为这相当于重复声明.语法如下:
class BaseClass { public: BaseClass(int value); }; class DerivedClass : public BaseClass { public: using BaseClass::BaseClass; }; 对于成员初始化,C++11允许下面这样的语法: class SomeClass { public: SomeClass() {} explicit SomeClass(int new_value) : value(new_value) {} private: int value = 5; };
每一个构造函数都将把value初始化为5, 如果它们没用其它值来覆盖这个初始化的话.上面那个空的构造函数会把value初始化为类定义时的状态5.而那带有参数的构造函数会用指定的值来初始化value.成员的初始化也可以使用前面提到的统一初始化.
5.8 显式重写(覆盖,override)和final
在C++03中,很容易让你在本想重写基类某个函数的时候却意外地创建了另一个虚函数.例如:
struct Base { virtual void some_func(float); }; struct Derived : Base { virtual void some_func(int); };
本来Derived::some_func函数是想替代Base中那个函数的.但是因它的接口不同, 又创建了一个虚函数.这是个常见的问题, 特别是当用户想要修改基类的时候. C++11引入了新的语法来解决这个问题:
struct Base { virtual void some_func(float); }; struct Derived : Base { virtual void some_func(int) override; // 病态的,不会重写基类的方法 };
override 这个特殊的标识符意味编译器将去检查基类中有没有一个具有相同签名的虚函数,如果没有,编译器就会报错! C++11还增加了防止基类被继承和防止子类重写函数的能力.这是由特殊的标识符final来完成的,例如:
struct Base1 final { }; struct Derived1 : Base1 { }; // 病态的, 因为类Base1被标记为final了 struct Base2 { virtual void f() final; }; struct Derived2 : Base2 { void f(); // 病态的, 因为虚函数Base2::f 被标记为final了. };
在这个例子中, virtual void f() final;语句声明了一个虚函数却也阻止了子类重写这个函数.它还有一个作用,就是防止了子类将那个特殊的函数名与新的参数组合在一起. 需要注意的是,override和final都不是C++语言的关键字.他们是技术上的标识符,只有在它们被用在上面这些特定的上下文在才有特殊意义.用在其它地方他们仍然是有效标识符.
5.9 空指针常量
本节中出的"0"都将解释为"一个求值结果为0的int型常量表达式". 实际上任何整数类型都可以作为常量表达式. 自从1972年C诞生以来,常量0就有着int型常量和空指针的双重角色.C语言用预处理宏NULL来处理这个固有的歧义, NULL通常被定义为(void*)0或0.而C++不采用同样行为,只允许0做空指针常量.而这与函数重载配合时就显得有些弱智了.
void foo(char *); void foo(int);
如果NULL定义为0,那么foo(NULL);语句将会调用foo(int).这几乎必定不是程序员想要的,也不是代码直观上要表达的意图. C++11通过引入一个新的关键字nullptr充当单独的空指针常量来纠正这个问题.它的类型是nullptr_t,是一个可以隐式转换任意类型的指针或指向成员的指针的类型,并且可以和这些类型进行比较.它不能隐式转换为整型,也不能与整型做比较,bool类型除外.尽管最初的提议中一个nullptr类型的右值不应该能转换为bool类型,但是为了保持与常规指针类型的一致性,核心语言工作组还是认定这种转换是合理的. 为了向下兼容,0仍然是一个有效的空指针常量!
char *pc = nullptr; // OK int *pi = nullptr; // OK bool b = nullptr; // OK. b is false. int i = nullptr; // error foo(nullptr); // calls foo(char *), not foo(int);
5.10 强类型枚举
在C++03中,枚举不是类型安全的.他们实际上是整数,尽管他们是不同的枚举类型.这使得我们可以比较两种不同类型的枚举值.C++03提供的唯一安全性就是,一个整数或一个枚举类型的值不能隐式地转换成另一个枚举类型.此外,底层的具体的整数类型(short,long,int,...)是由实现(编译器)定义的,标准并无明确规定.因此,那些枚举变量的大小的代码将是不可移植的.最后,枚举值是暴露在外层作用域(直接包含枚举定义的作用域)中的.所以,两个不同枚举类型的成员不可能有相同的名字. C++11引入了一个没上述问题的特殊"枚举类".使用 enum class(也可以用同义词enum struct)来声明:
enum class Enumeration { Val1, Val2, Val3 = 100, Val4 // = 101 };
这种枚举是类型安全的;枚举值不能隐式地转换成整数,所以也不可以和整数做比较.表达式 Enumeration::Val4 == 101会报一个编译错误. 枚举类的底层类型总是已知的.默认是int型,这可以用其它整数类型来覆盖它.就像下面这个例子:
enum class Enum2 : unsigned int {Val1, Val2};
老式的枚举被放在直接包含该定义的作用域中.新式的枚举被放在枚举类的作用中.所以,上例中Val1是未定义的,而Enum2::Val1是已定义的. C++11还提供了一个过渡语法让老式的枚举类型可以提供显式的作用域和定义底层整数类型.语法如下:
enum Enum3 : unsigned long {Val1 = 1, Val2};
这个例子中枚举名字被定义在枚举类型的作用域内(Enum3::Val1),但是为了向下兼容它们也会被放在直接包含在Enum3所在的作用域中.
5.11 右尖括号
C++03的解析器都把">>"定义为右移运算符.但是,在嵌套的模板声明中,程序员往往倾向于忽略两个右尖括号之间的空格.这会导致编译器报一个语法错误. C++11改进了编译器的解析规则,尽可能地将多个右尖括号(>)解析成模板参数结束符.可以用圆括号来改变这个规则,圆号的优先级比它高.例如:
template class SomeType; std::vector> x1; // 被解析成 std::vector of SomeType 2>, // 这是错误的语法, 1 被当成 true 了. std::vector> x1; // 被解析成 std::vector of SomeType, // 在C++11中是合法的. (1>2) 这个表达式的结果为false.
5.12 显式类型转换操作符
C++98 增加了explicit关键字来防止单参数的构造函数被用作隐式的类型转换操作符.然而,却没有对真正的类型转换操作符这样做.例如,智能指针可能定义了operator bool(),让它的行为更像真原始指针.有了这个转换操作符就可以用if语句来测试:if (smart_ptr_variable),当这个指针不为空时结果为真,否则为假.但是,这也会引起其他一些非预期的转换操作.因C++中的bool被定义为一种算术类型,可以隐式地转换为整数甚至是浮点数进行数学运算. 拿转换出的布尔值进行非布尔计算的数学计算,往往不是程序员想要的. C++11中,explicit关键字也可以用在类型转换操作符上.和用在构造函数上一样,防止这种转换函数被于隐式类型转换.当然了,在语言的上下文明确要求使用布尔变量(如if语句和循环语句中的条件,还有逻辑运算符的操作数)的地方,会被当作显示转换.所以可以使用类型转换操作符.
5.13 模板的别名
在进入这个主题前,先弄清楚"模板"和"类型"的区别.类型,是具体的数据类型,可以直接用来定义变量. 模板,是类型的模板,根据这个模板可以产生具体的类型;模板是不能直接定义变量的;当指定了所有的模板参数后,就产生了一个具体的类型,就可以用来定义变量了. 在C++03中,只能为类型(包括完全特化的模板,也是一种类型)定义别名, 而不能为模板定义别名:
template class SomeType; template typedef SomeType TypedefName; // 在C++03中, 这是非法的.
C++11增加为模板定义别名的能力,用下面这样的语法:
template class SomeType; template using TypedefName = SomeType;
这种using语法也可以用来定义类型的别名:
typedef void (*FunctionType)(double); // 老式语法 using FunctionType = void (*)(double); // 新式语法
5.14 无限制的unions
C++03中,对哪些类型的对象能够作为联合的成员是有限制的.例如,联合不能包含定义了非平凡构造函数的对象.C++11废除了其中的一些限制: 现在,联合可以包含定义了非平凡构造函数的对象;如果包含了,那么联合就必须要显式定义一个构造函数.
#include // Required for placement 'new'. struct Point { Point() {} Point(int x, int y): x_(x), y_(y) {} int x_, y_; }; union U { int z; double w; Point p; // 非法的C++03; 合法的C++11. U() {new(&p) Point();} // 由于Point的原因, 必须定义构造函数. };
因为是放宽了现有的规则,所以不会对已有的代码造成影响.
6 核心语言功能的改进
这些特性让C++语言可以完成那些以前不可能的,极其繁琐的或者需要一些不可移植的库才能完成的事情.
6.1 可变参数模板
在 C++11 之前, 不论是类模板或是函数模板,都只能按其被声明时所指定的样子,接受一组数目固定的模板参数.C++11 加入新的表示法,允许任意个数,任意类型的模板参数,不必在定义时将参数的个数固定。
template class tuple;
模板类 tuple 的对象,能接受不限个数的 typename 作为它的模板形参:
class tuple someInstanceName;
实参的个数也可以是0,所以 class tuple someInstanceName 这样的定义也是可以的。
若不希望产生实参个数为 0 的变长参数模板,则可以采用以下的定义:
template class tuple;
变长参数模板也能运用到函数模板上。传统 C 中的 printf 函数,虽然也能做到不定个数的形参来调用,但其并非类型安全的。以下的例子中,C++11 除了能定义类型安全的变长参数函数外,还能让类似 printf 的函数能自然地处理自定义类型的对象。 除了在模板参数中能使用...表示不定长模板参数外,函数参数也使用同样的表示法代表不定长参数。
template void printf(const std::string &strFormat, Params... parameters);
其中,Params 与 parameters 分别代表模板与函数的变长参数集合,称之为参数包 (parameter pack).参数包必须要和运算符"..."搭配使用,避免语法上的歧义。
变长参数模板中,无法像在类或函数中那样使用参数包.因此典型的做法是以递归的方法取出可用参数,请看以下的 C++11 printf 例子:
void printf(const char *s) { while (*s) { if (*s == '%' && *(++s) != '%') throw std::runtime_error("invalid format string: missing arguments"); std::cout 3.14) && (GREEKPI < 3.15), "GREEKPI is inaccurate!"); template struct Check { static_assert(sizeof(int)关注打赏