第18 章探讨 C++新标准 复习前面介绍过的 C++11 功能

第18 章探讨 C++新标准 复习前面介绍过的 C++11 功能


第18 章探讨 C++新标准

本章首先复习前面介绍过的C++11功能,然后介绍如下主题:

  • 移动语义和右值引用。
  • Lambda 表达式。
  • 包装器模板 function。
  • 可变参数模板。

本章重点介绍C++11对C++所做的改进。本书前面介绍过多项C++11功能,本章首先复习这些功能并详细介绍其他一些功能。然后,指出一些超出了本书范围的C++11新增功能(考虑到C++11 草案的篇幅比C++98长 98%,本书无法全面介绍)。最后,将简要地探讨 BOOST库。

18.1复习前面介绍过的 C++11 功能

本书前面介绍过很多C++11改进,但您现在可能忘了,本节简要地复习这些改进

18.1.1 新类型

C++11新增了类型 long long和 unsigned longlong,以支持64位(或更宽)的整型;新增了类型 charl6t和 char32t,以支持16位和 32位的字符表示;还新增了“原始”字符串。第3章讨论了这些新增的类型。

18.8.2 统一的初始化

C++11扩大了用大括号括起的列表(初始化列表)的适用范围,使其可用于所有内置类型和用户定义的类型(即类对象)。使用初始化列表时,可添加等号(=),也可不添加:

int x={5):
double y{2.75};
short quar[5](4,5,2,76,1};

另外,列表初始化语法也可用于new表达式中:

int *ar =new int [4](2.4.6.7):// C++11

创建对象时,也可使用大括号(而不是圆括号)括起的列表来调用构造函数:

class stump
{
private:
int roots;
double weight;
public:
Stump(int r,double w):roots(r)weight(w){}
}
Stump s1(3,15.6);// old styie
Stump s2{543.4};// C++11
Stumps3={432.1};//C++11

然而,如果类有将模板 std::initializerlist 作为参数的构造函数,则只有该构造函数可以使用列表初始化形式。第3章、4章、9章、10章和第16章讨论了列表初始化的各个方面。

1.缩窄

初始化列表语法可防止缩窄,即禁止将数值赋给无法存储它的数值变量。常规初始化允许程序员执行可能没有意义的操作:

char c1=1.57e27;//double-to-char,undefined behavior
char c2459585821://int-to-char,undefined behavior

然而,如果使用初始化列表语法,编译器将禁止进行这样的类型转换,即将值存储到比它“窄”的变
量中:

char cl(1.57e27};//double-to-char,compile-time error
char c2=(459585821}://int-to-char,out of range,compile-time error

但允许转换为更宽的类型。另外,只要值在较窄类型的取值范围内,将其转换为较窄的类型也是允许的:

char c1{66};//int-to-char,in range,allowed
double c2{=66};//int-to-double, allowed

2.std::initializer list

C++11提供了模板类 imnitializer list,可将其用作构造函数的参数,这在第16章讨论过。如果类有接受initializer_list 作为参数的构造函数,则初始化列表语法就只能用于该构造函数。列表中的元素必须是同一种类型或可转换为同一种类型。STL容器提供了将initializerlist作为参数的构造函数:

vector<int> al(10);//uninitialized vector with 10 elements
vector<int> a2{10};//initializer-list,a2 has lelement set to 10
vector<int>a34,6,1}://3 elements set to 4.6,1

头文件 initializer list 提供了对模板类 initializer list 的支持。这个类包含成员函数 begin()和 cnd(),可用于获悉列表的范围。除用于构造函数外,还可将initializerlist用作常规函数的参数:#include double sum(std::initializer listi1l);

int main()
{
	double total=sum((2.5,3.1,4});//4 converted to 4.0
	...
}
double sum(std::initializer list<double>i1)
{
	double tot =0;
	for(auto p=il.begin();p!=il.end();p++)
		tot += *p;
return tot;
}

18.1.3 声明

C++11提供了多种简化声明的功能,尤其在使用模板时。

1.auto

以前,关键字 auto是一个存储类型说明符(见第9章),C++11将其用于实现自动类型推断(见第3章)。这要求进行显式初始化,让编译器能够将变量的类型设置为初始值的类型:

auto maton=112;//maton is type int
auto pt&maton;//pt is type int*
double fm(double,int);
auto pf = fm;// pf is type double(*)(double,int)

关键字 auto 还可简化模板声明。例如,如果i是一个 std:initializer_list对象,则可将下述代码:

for(std::initializer list<double>::iterator p=il.begin();
p !=i1.end();p++)

替换为如下代码:

for(auto p=il.begin();p!=il.end();p++)

2. decltype

关键字 decltype 将变量的类型声明为表达式指定的类型。下面的语句的含义是,让y的类型与x相同。其中x是一个表达式:

decltype(x)Y;

下面是几个示例:

double x;
int n;
decltype(x*n)q://qsame type as x*n,i.e., double
decltype(&x)pd;//pd sameas x,i.e.,double*

这在定义模板时特别有用,因为只有等到模板被实例化时才能确定类型:

templatectypename T,typename U)
void ef(T t.U u)
decltype(T*U)tu;

其中 tu 将为表达式TU的类型,这里假定定义了运算TU。例如,如果T为char,U为short,则tu将为 int,这是由整型算术自动执行整型提升导致的。
decltype的工作原理比 auto复杂,根据使用的表达式,指定的类型可以为引用和const。下面是几个
示例:

int j- 3:
int &k = j
const int &n =j;
decltype(n)i1;
decltype(j)i2;
decltype((j))i3;
// i1 type const int &
// i2 type int
// i3 type int &
decltype(k+1)i4://i4 type int

有关导致上述结果的规则的详细信息,请参阅第8章。

3.返回类型后置

C++11新增了一种函数声明语法:在函数名和参数列表后面(而不是前面)指定返回类型:


auto f2(double,int)->double; // new syntax, return type is double
double fl(double,int);//traditional syntax

就常规函数的可读性而言,这种新语法好像是倒退,但让您能够使用 decltype 来指定模板函数的返回类型:

template<typename T,typename U)
auto eff(Tt,uu)->decltype(T*U)

这里解决的问题是,在编译器遇到e的参数列表前,T和U还不在作用域内,因此必须在参数列表后使用 decltype。这种新语法使得能够这样做。

4.模板别名:using=

对于冗长或复杂的标识符,如果能够创建其别名将很方便。以前,C++为此提供了typedef:

typedef std::vector<std::string>::iterator itType;

C++11提供了另一种创建别名的语法,这在第14章讨论过:

using itType =std::vector<std::string>::iterator;

差别在于,新语法也可用于模板部分具体化,但typedef不能:

template<typename T>
using arr12std::array<T,12>;//template for multiple aliases

上述语句具体化模板 array<T,int>(将参数int设置为12)。例如,对于下述声明:

std::array<double12>al;
std::array<std::string,12>a2;

可将它们替换为如下声明:

arr12<double> a1:
arr12(std::string> a2;

5. nullptr

空指针是不会指向有效数据的指针。以前,C++在源代码中使用0表示这种指针,但内部表示可能不同。这带来了一些问题,因为这使得0即可表示指针常量,又可表示整型常量。正如第12章讨论的,C++11新增了关键字 nullptr,用于表示空指针:它是指针类型,不能转换为整型类型。为向后兼容,C++11仍允许使用0来表示空指针,因此表达式nullptr=-0为tue,但使用nulptr而不是0提供了更高的类型安全。例如,可将0传递给接受int参数的函数,但如果您试图将nullpt传递给这样的函数,编译器将此视为错误。因此,出于清晰和安全考虑,请使用nullptr–如果您的编译器支持它。

18.1.4 智能指针

如果在程序中使用new从堆(自由存储区)分配内存,等到不再需要时,应使用delete将其释放。C++引入了智能指针 autoptr,以帮助自动完成这个过程。随后的编程体验(尤其是使用STL时)表明,需要有更精致的机制。基于程序员的编程体验和 BOOST库提供的解决方案,C++11据弃了autoptr,并新增了三种智能指针:uniqueptr、sharedptr和weakptr,第16章讨论了前两种。所有新增的智能指针都能与STL容器和移动语义协同工作。

18.1.5 异常规范方面的修改

以前,C++提供了一种语法,可用于指出函数可能引发哪些异常(参见第15章):

void f501(int)throw(bad dog);// can throw type bad dog exceptionvoid f733(long long)throw()://doesn't throw an exceptionauto ptr 一样,C++编程社区的集体经验表明,异常规范的效果没有预期的好。因此,C++11摒弃的异常规范。然而,标准委员会认为,指出函数不会引发异常有一定的价值,他们为此添加了关键字noexcept:

```cpp
void f875(short,short)noexcept;// doesn't throw an exception

18.1.6 作用域内枚举

传统的 C++枚举提供了一种创建名称常量的方式,但其类型检查相当低级。另外,举名的作用域为枚举定义所属的作用域,这意味着如果在同一个作用域内定义两个枚举,它们的枚举成员不能同名。最后,枚举可能不是可完全移植的,因为不同的实现可能选择不同的底层类型。为解决这些问题,C++11新增了一种枚举。这种枚举使用class或struct定义:

enum Oldl {yes,no,maybe};// traditional form
enum class Newl(never,sometimes,often,always;//new form// new form
enum struct New2never,lever,sever};

新枚举要求进行显式限定,以免发生名称冲突。因此,引用特定枚举时,需要使用New1::never和New2::never等。更详细的信息请参阅第10章。

18.1.7 对类的修改

为简化和扩展类设计,C++11做了多项改进。这包括允许构造函数被继承和彼此调用、更佳的方法访问控制方式以及移动构造函数和移动赋值运算符,这些都将在本章介绍。下面先来复习本书前面介绍过的改进。

1,显式转换运算符

有趣的是,C++很早就支持对象自动转换。但随着编程经验的积累,程序员逐渐认识到,自动类型转换可能导致意外转换的问题。为解决这种问题,C++引入了关键字explicit,以禁止单参数构造函数导致的自动转换:

class Plebe
{
Plebe(int);//automatic int-to-plebe conversion
explicit Plebe(double);//requires explicit use
...
};
...
Plebe a,b;
a=5;
//implicit conversion, call Plebe(5)// not allowed
b =0.5;
b= Plebe(0.5);//explicit conversion

C++11拓展了 cxplicit的这种用法,使得可对转换函数做类似的处理(参见第11章):

class Plebe
 {
 ...
//conversion functions
operator int()const;
explicit operator double()const;
 };
 ...
Plebe a,b;
int n=a;//int-to-Plebe automatic conversion
double xb;// not allowed
x= double(b);//explicit conversion,allowed

2.类内成员初始化

很多首次使用C++的用户都会问,为何不能在类定义中初始化成员?现在可以这样做了,其语法类似于下面这样:

class Session
{
 
int meml =10;// in-class initialization
double mem2= 1966.54
};//in-class initializationshort mem3;
public :
Session()}//#1
Session(shorts):mem3(s){}// #2
Session(int n,double d,shorts):meml(n),mem2(d),mem3(s){}//#3
}

可使用等号或大括号版本的初始化,但不能使用圆括号版本的初始化。其结果与给前两个构造函数提供成员初始化列表,并指定mem1和mem2的值相同:

Session():mem1(10)mem2(1966.54){}
Session(short s):meml(10)mem2(1966.54)mem3(s)}

通过使用类内初始化,可避免在构造函数中编写重复的代码,从而降低了程序员的工作量、厌倦情绪和出错的机会。
如果构造函数在成员初始化列表中提供了相应的值,这些默认值将被覆盖,因此第三个构造函数覆盖了类内成员初始化。

18.1.8 模板和 STL方面的修改

为改善模板和标准模板库的可用性,C++11做了多个改进;有些是库本身,有些与易用性相关。本章前面提到了模板别名和适用于 STL的智能指针。

1.基于范围的 for 循环

对于内置数组以及包含方法begin()和end()的类(如 std::string)和 STL 容器,基于范围的 for 循环(第5章和第16章讨论过)可简化为它们编写循环的工作。这种环对数组或容器中的每个元素执行指定的操作:

double prices[5]={4.9910.996.877.998.49};for(double x:prices)
std::cout << x<< std::endl;

其中,x将依次为 prices 中每个元素的值。x的类型应与数组元素的类型匹配。一种更容易、更安全的方式是,使用 auto来声明x,这样编译器将根据 prices 声明中的信息来推断x的类型:

double prices[s]=4.9910.996.877.998.49};for (auto x:prices)
std::cout << x << std::endl;

如果要在循环中修改数组或容器的每个元素,可使用引用类型:

std::vectorsint> vi(6);
for (auto & x:vi)x= std::rand();
//use a reference if loop alters contents

2.新的 STL容器

C++11新增了STL, 容器 forward list、unordered map、 unordered multimap、 unordered set 和l umnordered multisct(参见第16章)。容器 forward list是一种单向链表,只能沿一个方向遍历;与双向链接的 list 容器相比,
它更简单,在占用存储空间方面更经济。其他四种容器都是使用哈希表实现的。C++11还新增了模板 amay(这在第4和16章讨论过)。要实例化这种模板,可指定元素类型和固定的元素数:

std::array<int,360>ar;//array of 360 ints

这个模板类没有满足所有的常规模板需求。例如,由于长度固定,您不能使用任何修改容器大小的方法,如 put_back()。但 aray确实有方法 begin()和 end(),这让您能够对 aray 对象使用众多基于范围的 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新增了关键字cxpor,旨在提供一种途径,让程序员能够将模板定义放在接口文件和实现文件中,其中前者包含原型和模板声明,而后者包含模板函数和方法的定义。实践证明这不现实,因此C++11终止了这种用法,但仍保留了关键字 expon,供以后使用。

6.尖括号

为避免与运算符>>混淆,C++要求在声明嵌套模板时使用空格将尖括号分开:
std::vector<std::list>vl;//>> not ok
C++11不再这样要求:
std::vector<std::list>vl;//>>ok in C++11

18.1.9 右值引用

传统的C++引用(现在称为左值引用)使得标识符关联到左值。左值是一个表示数据的表达式(如变景名或解除引用的指针),程序可获取其地址。最初,左值可出现在赋值语句的左边,但修饰符const的出现使得可以声明这样的标识符,即不能给它赋值,但可获取其地址:

int n;
int *pt = new int;
const intb=101;//can't assign to b,but &b is valid
int & rn=n;//n identifies datum at address &n
int & rt = *pt;//*pt identifies datum at address ptconst 
int &rbb;//bidentifies 
const datum at address &b

C++11新增了右值引用(这在第8章讨论过),这是使用&&表示的。右值引用可关联到右值,即可出现在赋值表达式右边,但不能对其应用地址运算符的值。右值包括字面常量(C-风格字符串除外,它表示地址)、诸如x+y等表达式以及返回值的函数(条件是该函数返回的不是引用):

int x= 10;
int y= 23;
int && r1 = 13;
int && r2 = x + y;
double && r3= std::sqrt(2.0);

注意,r2关联到的是当时计算x+y得到的结果。也就是说,r2关联到的是23,即使以后修改了x或y,也不会影响到r2。
有趣的是,将右值关联到右值引用导致该右值被存储到特定的位置,且可以获取该位置的地址。也就是说,虽然不能将运算符&用于13,但可将其用于r1。通过将数据与特定的地址关联,使得可以通过右值引用来访问该数据。
程序清单18.1是一个简短的示例,演示了上述有关右值引用的要点。

程序清单 18.1 rvref.cpp

// rvref.cpp -- simple uses of rvalue references
#include <iostream>

inline double f(double tf) {return 5.0*(tf-32.0)/9.0;};
int main()
{
    using namespace std;
    double tc = 21.5;
    double && rd1 = 7.07;
    double && rd2 = 1.8 * tc + 32.0;
    double && rd3 = f(rd2);
    cout << " tc value and address: " << tc <<", " << &tc << endl;
    cout << "rd1 value and address: " << rd1 <<", " << &rd1 << endl;
    cout << "rd2 value and address: " << rd2 <<", " << &rd2 << endl;
    cout << "rd3 value and address: " << rd3 <<", " << &rd3 << endl;
    // cin.get();
    return 0;
}

该程序的输出如下:

引入右值引用的主要目的之一是实现移动语义,这是在这里插入图片描述
本章将讨论的下一个主题。

  • 26
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值