- 内容提要
- 前言
- 第 1 章 预备知识
- 第 2 章 开始学习 C++
- 第 3 章 处理数据
- 第 4 章 复合类型
- 第 5 章 循环和关系表达式
- 第 6 章 分支语句和逻辑运算符
- 第 7 章 函数——C++的编程模块
- 第 8 章 函数探幽
- 第 9 章 内存模型和名称空间
- 第 10 章 对象和类
- 第 11 章 使用类
- 第 12 章 类和动态内存分配
- 第 13 章 类继承
- 第 14 章 C++中的代码重用
- 第 15 章 友元、异常和其他
- 第 16 章 string 类和标准模板库
- 第 17 章 输入、输出和文件
- 第 18 章 探讨 C++新标准
- 附录 A 计数系统
- 附录 B C++保留字
- 附录 C ASCII 字符集
- 附录 D 运算符优先级
- 附录 E 其他运算符
- 附录 F 模板类 string
- 附录 G 标准模板库方法和函数
- 附录 H 精选读物和网上资源
- 附录 I 转换为 ISO 标准 C++
- 附录 J 复习题答案
18.1 复习前面介绍过的 C++11 功能
本书前面介绍过很多 C++11 改进,但您现在可能忘了,本节简要地复习这些改进。
18.1.1 新类型
C++11 新增了类型 long long 和 unsigned long long,以支持 64 位(或更宽)的整型;新增了类型 char16_t 和 char32_t,以支持 16 位和 32 位的字符表示;还新增了“原始”字符串。第 3 章讨论了这些新增的类型。
18.1.2 统一的初始化
C++11 扩大了用大括号括起的列表(初始化列表)的适用范围,使其可用于所有内置类型和用户定义的类型(即类对象)。使用初始化列表时,可添加等号(=),也可不添加:
另外,列表初始化语法也可用于 new 表达式中:
创建对象时,也可使用大括号(而不是圆括号)括起的列表来调用构造函数:
然而,如果类有将模板 std::initializer_list 作为参数的构造函数,则只有该构造函数可以使用列表初始化形式。第 3 章、4 章、9 章、10 章和第 16 章讨论了列表初始化的各个方面。
1.缩窄
初始化列表语法可防止缩窄,即禁止将数值赋给无法存储它的数值变量。常规初始化允许程序员执行可能没有意义的操作:
然而,如果使用初始化列表语法,编译器将禁止进行这样的类型转换,即将值存储到比它“窄”的变量中:
但允许转换为更宽的类型。另外,只要值在较窄类型的取值范围内,将其转换为较窄的类型也是允许的:
2.std::initializer_list
C++11 提供了模板类 initializer_list,可将其用作构造函数的参数,这在第 16 章讨论过。如果类有接受 initializer_list 作为参数的构造函数,则初始化列表语法就只能用于该构造函数。列表中的元素必须是同一种类型或可转换为同一种类型。STL 容器提供了将 initializer_list 作为参数的构造函数:
头文件 initializer_list 提供了对模板类 initializer_list 的支持。这个类包含成员函数 begin( ) 和 end( ),可用于获悉列表的范围。除用于构造函数外,还可将 initializer_list 用作常规函数的参数:
18.1.3 声明
C++11 提供了多种简化声明的功能,尤其在使用模板时。
1.auto
以前,关键字 auto 是一个存储类型说明符(见第 9 章),C++11 将其用于实现自动类型推断(见第 3 章)。这要求进行显式初始化,让编译器能够将变量的类型设置为初始值的类型:
关键字 auto 还可简化模板声明。例如,如果 il 是一个 std::initializer_list<double>对象,则可将下述代码:
替换为如下代码:
2.decltype
关键字 decltype 将变量的类型声明为表达式指定的类型。下面的语句的含义是,让 y 的类型与 x 相同,其中 x 是一个表达式:
下面是几个示例:
这在定义模板时特别有用,因为只有等到模板被实例化时才能确定类型:
其中 tu 将为表达式 TU 的类型,这里假定定义了运算 TU。例如,如果 T 为 char,U 为 short,则 tu 将为 int,这是由整型算术自动执行整型提升导致的。
decltype 的工作原理比 auto 复杂,根据使用的表达式,指定的类型可以为引用和 const。下面是几个示例:
有关导致上述结果的规则的详细信息,请参阅第 8 章。
3.返回类型后置
C++11 新增了一种函数声明语法:在函数名和参数列表后面(而不是前面)指定返回类型:
就常规函数的可读性而言,这种新语法好像是倒退,但让您能够使用 decltype 来指定模板函数的返回类型:
这里解决的问题是,在编译器遇到 eff 的参数列表前,T 和 U 还不在作用域内,因此必须在参数列表后使用 decltype。这种新语法使得能够这样做。
4.模板别名:using =
对于冗长或复杂的标识符,如果能够创建其别名将很方便。以前,C++为此提供了 typedef:
C++11 提供了另一种创建别名的语法,这在第 14 章讨论过:
差别在于,新语法也可用于模板部分具体化,但 typedef 不能:
上述语句具体化模板 array<T, int>(将参数 int 设置为 12)。例如,对于下述声明:
可将它们替换为如下声明:
5.nullptr
空指针是不会指向有效数据的指针。以前,C++在源代码中使用 0 表示这种指针,但内部表示可能不同。这带来了一些问题,因为这使得 0 即可表示指针常量,又可表示整型常量。正如第 12 章讨论的,C++11 新增了关键字 nullptr,用于表示空指针;它是指针类型,不能转换为整型类型。为向后兼容,C++11 仍允许使用 0 来表示空指针,因此表达式 nullptr == 0 为 true,但使用 nullptr 而不是 0 提供了更高的类型安全。例如,可将 0 传递给接受 int 参数的函数,但如果您试图将 nullptr 传递给这样的函数,编译器将此视为错误。因此,出于清晰和安全考虑,请使用 nullptr—如果您的编译器支持它。
18.1.4 智能指针
如果在程序中使用 new 从堆(自由存储区)分配内存,等到不再需要时,应使用 delete 将其释放。C++引入了智能指针 auto_ptr,以帮助自动完成这个过程。随后的编程体验(尤其是使用 STL 时)表明,需要有更精致的机制。基于程序员的编程体验和 BOOST 库提供的解决方案,C++11 摒弃了 auto_ptr,并新增了三种智能指针:unique_ptr、shared_ptr 和 weak_ptr,第 16 章讨论了前两种。
所有新增的智能指针都能与 STL 容器和移动语义协同工作。
18.1.5 异常规范方面的修改
以前,C++提供了一种语法,可用于指出函数可能引发哪些异常(参见第 15 章):
与 auto_ptr 一样,C++编程社区的集体经验表明,异常规范的效果没有预期的好。因此,C++11 摒弃的异常规范。然而,标准委员会认为,指出函数不会引发异常有一定的价值,他们为此添加了关键字 noexcept:
18.1.6 作用域内枚举
传统的 C++枚举提供了一种创建名称常量的方式,但其类型检查相当低级。另外,枚举名的作用域为枚举定义所属的作用域,这意味着如果在同一个作用域内定义两个枚举,它们的枚举成员不能同名。最后,枚举可能不是可完全移植的,因为不同的实现可能选择不同的底层类型。为解决这些问题,C++11 新增了一种枚举。这种枚举使用 class 或 struct 定义:
新枚举要求进行显式限定,以免发生名称冲突。因此,引用特定枚举时,需要使用 New1::never 和 New2::never 等。更详细的信息请参阅第 10 章。
18.1.7 对类的修改
为简化和扩展类设计,C++11 做了多项改进。这包括允许构造函数被继承和彼此调用、更佳的方法访问控制方式以及移动构造函数和移动赋值运算符,这些都将在本章介绍。下面先来复习本书前面介绍过的改进。
1.显式转换运算符
有趣的是,C++很早就支持对象自动转换。但随着编程经验的积累,程序员逐渐认识到,自动类型转换可能导致意外转换的问题。为解决这种问题,C++引入了关键字 explicit,以禁止单参数构造函数导致的自动转换:
C++11 拓展了 explicit 的这种用法,使得可对转换函数做类似的处理(参见第 11 章):
2.类内成员初始化
很多首次使用 C++的用户都会问,为何不能在类定义中初始化成员?现在可以这样做了,其语法类似于下面这样:
可使用等号或大括号版本的初始化,但不能使用圆括号版本的初始化。其结果与给前两个构造函数提供成员初始化列表,并指定 mem1 和 mem2 的值相同:
通过使用类内初始化,可避免在构造函数中编写重复的代码,从而降低了程序员的工作量、厌倦情绪和出错的机会。
如果构造函数在成员初始化列表中提供了相应的值,这些默认值将被覆盖,因此第三个构造函数覆盖了类内成员初始化。
18.1.8 模板和 STL 方面的修改
为改善模板和标准模板库的可用性,C++11 做了多个改进;有些是库本身,有些与易用性相关。本章前面提到了模板别名和适用于 STL 的智能指针。
1.基于范围的 for 循环
对于内置数组以及包含方法 begin( ) 和 end( ) 的类(如 std::string)和 STL 容器,基于范围的 for 循环(第 5 章和第 16 章讨论过)可简化为它们编写循环的工作。这种循环对数组或容器中的每个元素执行指定的操作:
其中,x 将依次为 prices 中每个元素的值。x 的类型应与数组元素的类型匹配。一种更容易、更安全的方式是,使用 auto 来声明 x,这样编译器将根据 prices 声明中的信息来推断 x 的类型:
如果要在循环中修改数组或容器的每个元素,可使用引用类型:
2.新的 STL 容器
C++11 新增了 STL 容器 forward_list、unordered_map、unordered_multimap、unordered_set 和 unordered_multiset(参见第 16 章)。容器 forward_list 是一种单向链表,只能沿一个方向遍历;与双向链接的 list 容器相比,它更简单,在占用存储空间方面更经济。其他四种容器都是使用哈希表实现的。
C++11 还新增了模板 array(这在第 4 和 16 章讨论过)。要实例化这种模板,可指定元素类型和固定的元素数:
这个模板类没有满足所有的常规模板需求。例如,由于长度固定,您不能使用任何修改容器大小的方法,如 put_back( )。但 array 确实有方法 begin( ) 和 end( ),这让您能够对 array 对象使用众多基于范围的 STL 算法。
3.新的 STL 方法
C++11 新增了 STL 方法 cbegin( ) 和 cend( )。与 begin( ) 和 end( ) 一样,这些新方法也返回一个迭代器,指向容器的第一个元素和最后一个元素的后面,因此可用于指定包含全部元素的区间。另外,这些新方法将元素视为 const。与此类似,crbegin( ) 和 crend( ) 是 rbegin( ) 和 rend( ) 的 const 版本。
更重要的是,除传统的复制构造函数和常规赋值运算符外,STL 容器现在还有移动构造函数和移动赋值运算符。移动语义将在本章后面介绍。
4.valarray 升级
模板 valarray 独立于 STL 开发的,其最初的设计导致无法将基于范围的 STL 算法用于 valarray 对象。C++11 添加了两个函数(begin( ) 和 end( )),它们都接受 valarray 作为参数,并返回迭代器,这些迭代器分别指向 valarray 对象的第一个元素和最后一个元素后面。这让您能够将基于范围的 STL 算法用于 valarray(参见第 16 章)。
5.摒弃 export
C++98 新增了关键字 export,旨在提供一种途径,让程序员能够将模板定义放在接口文件和实现文件中,其中前者包含原型和模板声明,而后者包含模板函数和方法的定义。实践证明这不现实,因此 C++11 终止了这种用法,但仍保留了关键字 export,供以后使用。
6.尖括号
为避免与运算符>>混淆,C++要求在声明嵌套模板时使用空格将尖括号分开:
C++11 不再这样要求:
18.1.9 右值引用
传统的 C++引用(现在称为左值引用)使得标识符关联到左值。左值是一个表示数据的表达式(如变量名或解除引用的指针),程序可获取其地址。最初,左值可出现在赋值语句的左边,但修饰符 const 的出现使得可以声明这样的标识符,即不能给它赋值,但可获取其地址:
C++11 新增了右值引用(这在第 8 章讨论过),这是使用&&表示的。右值引用可关联到右值,即可出现在赋值表达式右边,但不能对其应用地址运算符的值。右值包括字面常量(C-风格字符串除外,它表示地址)、诸如 x + y 等表达式以及返回值的函数(条件是该函数返回的不是引用):
注意,r2 关联到的是当时计算 x + y 得到的结果。也就是说,r2 关联到的是 23,即使以后修改了 x 或 y,也不会影响到 r2。
有趣的是,将右值关联到右值引用导致该右值被存储到特定的位置,且可以获取该位置的地址。也就是说,虽然不能将运算符&用于 13,但可将其用于 r1。通过将数据与特定的地址关联,使得可以通过右值引用来访问该数据。
程序清单 18.1 是一个简短的示例,演示了上述有关右值引用的要点。
程序清单 18.1 rvref.cpp
该程序的输出如下:
引入右值引用的主要目的之一是实现移动语义,这是本章将讨论的下一个主题。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论