跳转至

📔 第 18 章 探讨C++新标准 学习笔记

1. 复习C++11功能

1.1 新类型

C++11新增类型:

  • 整型
    • long long
    • unsigned long long
  • 字符表示
    • char16_t
    • char32_t

1.2 统一的初始化

C++11 扩大了用大括号起的列表(初始化列表)的适用范围。使其可用于所有内置类型用户定义类型(即类对象)。列表初始化的三种方式:

  • 使用初始化列表时,可添加等号(=),也可不添加。
int x = {5};
short quar[5] {4,5,6,2,1};
  • 可以用于new表达式
int *ar = new int [4] {2,4,6,7}; C++11
  • 创建对象时,可使用大括号(而不是圆括号)括起来的列表来调用构造函数。
class Stump
{
  private:
    int roots;
    double weight;
  public:
    Stump(int r,rouble w) : roots(r), weight(w) {}
};
Stump s1(3,15.6); // old style
Stump s2{5,43.4}; // C++11
Stump s3{4,32.1}; // C++11

1.2.1 缩窄

  • 初始化列表语法可防止缩窄,即禁止将数值赋给无法存储它的数值变量,即将值存储到比它“窄”的变量中。

  • 编译器会禁止变窄类型的转换,但允许在范围内变宽的转换。

1.2.2 std::initializer_list

  • C++11中提供 initializer_list模板类,可用作构造函数的参数。

  • 如果类有接受 initializer_list 作为参数的构造函数,则初始化列表语法就只能用于该构造函数。

  • 列表中的元素必须是同一种类型或者可转换为同一种类型

1.3 声明

1.3.1 auto

  • C++11使用auto关键字实现自动类型推断
  • 要求进行显式初始化,让编译器能够将变量的类型设置为初始值的类型。

  • auto关键字也可简化模板声明

1.3.2 decltype

  • decltype关键字:将变量的类型声明为表达式指定的类型。
1
2
3
4
5
6
template <typename T, typename U>
void ef(T t, U u)
{
  decltype(T*U) tu;
  ...
}

1.3.3 返回类型后置

  • C++11新增函数声明语法:在函数名和参数列表后面指定返回类型。
double f1(double,int); // 传统方式
auto f2(double,int)->double; // C++11中的新语法
  • 使用 decltype来指定模板函数的返回类型:
1
2
3
4
5
6
template<typename T, typename U>
auto eff(T t, U u) -> decltype(T*U)
{
  // 当编译器遇到 ef的参数列表时,T和 U不再作用域内,因此必须在参数列表后使用decltype。
  ....
}

1.3.4 模板别名:using

解决标识符冗长或复杂的问题。

  • 早期使用 typedef

    • 不能用于模板部分具体化
    typedef std::vector<std::string>::iterator itType;
    
  • C++11提供 using=

    • 可用于模板部分具体化
    template <typename T>
    using arr = std::array<T,int>; // 模板部分具体化
    

1.3.5 nullptr

C++11 新增关键字 nullptr来表示空指针。它是指针类型不能转换为整型类型

为向后兼容,C++11仍允许使用 0 来表示空指针,因此表达式 nullptr==0true

1.4 智能指针

因为在程序中使用 new 分配内存,未将其及时 delete释放。引入智能指针auto_ptr

C++11抛弃auto_ptr,并新增了三种智能指针:unique_ptrshared_ptrweak_ptr

1.5 异常规范的修改

指出函数不会引发异常有一定的价值,所以C++11添加了关键字noexpect

void f875(short, short) noexpect; // 不会抛出异常

1.6 作用域内枚举

在C++11中新增一种枚举。使用 class 或者 struc t来定义。

1
2
3
enum old1 {yes,no,maybe}; // 传统方式
enum class New1 {never,sometimes,often,always}; // 新方式
enum struct New2 {never,lever,sever}; // 新方式

新枚举要求进行显式限定,以避免名称冲突。所以在引用枚举时,则使用

New1::never;
New2::never;

1.7 对类的修改

  • 显式转换运算符

    • 早期的自动类型转换会导致意外问题,所以在C++11中引入关键字 explicit,以禁止单参数构造函数导致的自动转换
  • 类内成员初始化

    • 可使用等号或者大括号版本的初始化,但不能使用圆括号版本的初始化。
    • 类内成员初始化,可避免在构造函数中编写重复的代码。

1.8 模板和STL方面的修改

  • 基于范围的for循环

    1
    2
    3
    double price[5] = {4.99,10.99,6.87,7.99,8.49};
    for (auto x : price)
      std::out << x << std::endl;
    
    • 如果要在循环中修改数组或容器中的每个元素,可使用引用类型。
  • 新的STL容器

    • C++11新增STL容器:forward_listunordered_mapunordered_multimapunordered_setunordered_multiset
    • C++11新增模板array,实例化时,可指定元素类型和固定的元素数。
  • 新的STL方法

    • C++11新增STL方法
      • cbegin()
      • cend()
      • crbegin()
      • crend()
    • 其中 crbegin()crend()rbegin()rend()const版本。
    • 除传统的复制构造函数常规赋值运算符外,STL容器还有移动构造函数移动赋值运算符
  • valarray升级

    • C++11中添加了两个函数 begin() 和 end()。都接受 valarray 作为参数,并返回迭代器。
  • 抛弃export

    • C++98新增
    • C++终止用法,但为了兼容还保留关键字 export。
  • 尖括号

    • C++在声明嵌套模板时不再需要使用空格将尖括号分开。

1.9 右值引用

  • 左值引用

    • 左值:一个表示数据的表达式,程序可获取其地址。
    • 左值出现在赋值语句的左边,但修饰符的出现const使得声明的标识符,即不能给它赋值,但可获取其地址。
    1
    2
    3
    4
    5
    6
    int n;
    int * pt = new int;
    const int b = 101; // 不能赋值,
    int & rn = n;
    int & rt = *pt;
    const int &rb = b; // b标识符const引用地址
    
  • 右值引用

    • C++11新增右值引用,使用 && 表示。右值引用可以关联到右值,即可出现在赋值表达式右边,但不能对其应用地址运算符的值
    • 右值包括:字面常量表达式返回值的函数(函数返回的不能是引用)。
    • 引入右值引用的主要目的之一:实现移动语义
    1
    2
    3
    4
    5
    int x = 10;
    int y = 23;
    int && r1 = 13; // 字面常量
    int && r2 = x + y;  // 表达式
    double && r3 = std::sqrt(2.0); // 函数返回值
    

2. 移动语义和右值引用

2.1 需要移动语义的原因

  • 常规复制构造函数

    • 使用 const左值引用作为参数,使得引用关联到左值实参
    • 可执行深复制
  • 移动构造函数

    • 使用右值引用作为参数,将引用关联到右值实参
    • 只调整记录
    • 在将所有权转移给新对象的过程中,移动构造函数可能修改其实参,意味着右值引用参数不应是const

2.2 移动构造函数

代码示例

2.3 赋值

  • 适用于构造函数的移动语义考虑也适用于赋值运算符。

3. 新的类功能

3.1 特殊的成员函数

  • 原有的4个特殊成员函数

    • 默认构造函数
    • 复制构造函数
    • 复制赋值运算符
    • 析构函数
  • C++新增的2个

    • 移动构造函数
    • 移动赋值运算符

这些成员函数是编译器在各种情况下自动提供的。

  • 如果提供了析构函数、复制构造函数或者复制赋值运算符,编译器将不会自动提供移动构造函数移动赋值运算符
  • 如果提供了移动构造函数移动赋值运算符,编译器将不会自动提供复制构造函数复制赋值运算符

3.2 默认的方法和禁用的方法

  • C++11中如果提供了移动构造函数,所以编译器不会自动创建默认的构造函数、复制构造函数和复制赋值构造函数。可使用关键字 default 显式声明方法的默认版本。

1
2
3
4
5
6
7
8
9
class Someclass
{
  public:
    Someclass(Someclass &&);
    Someclass() = default;
    Someclass(const Someclass &) = default;
    Someclass & operator=(const Someclass &) = default;
    ...
};
- 关键字 delete 可用于禁止编译器使用特定方法。 - 也可禁止特定的转换

  • 关键字 default只能用于6个特殊成员函数,但delete可用于任何成员函数

3.3 委托构造函数

  • C++11 允许在一个构造函数的定义中使用另一个构造函数,这种方式称为 委托
Notes::Notes(int kk,double xx) : Notes(kk,xx,"Uh") {/**/}

3.4 继承构造函数

  • C++98 使用名称空间中函数可用的方式。

  • C++11 中使用派生类继承基类的所有构造函数(默认构造函数、复制构造函数和移动构造函数除外),但不会使用与派生类构造函数的特征标匹配的构造函数

3.5 管理虚方法:overridefinal

  • 在C++11中,可使用虚说明符override指出要覆盖一个函数,将其放在参数列表后面。如果声明与基类方法不匹配,编译器将视为错误。

  • final说明符禁止派生类覆盖特定的虚方法,所以在参数列表后面加上final

  • overridefinal非关键字,而是具有特殊含义的标识符

4. Lambda函数

Lambda 函数,也叫做Lambda表达式。

匿名函数无需给出函数命名。

4.1 比较函数指针、函数符和Lambda函数

在C++11中,对于接受函数指针或者函数符的函数,可使用匿名函数定义作为其参数。

匿名函数与函数的区别:

  • 使用 [] 替代 函数名
  • 没有声明返回类型(返回类型相当于使用decltype根据返回值推断得到)
  • 如果lambda不包含返回语句,推断出的返回类型将为void
[] (int x) {return x % 3 == 0;}

当且仅当, lambda表达式完全由一条返回语句组成时,自动类型推断才管用。否则,需要使用新增的返回类型后置语法:

[] (double x)-> double{int y = x; return x - y;} // 返回类型为double

匿名函数代码示例

4.2 使用Lambda原因

四个角度来分析

  • 距离
    • 函数定义离使用地点近。
  • 简洁
    • 函数符代码比函数和lambda代码更繁琐。
  • 效率
    • 三种方式的相对效率取决于编译器内联。
  • 功能
    • lambda可访问作用域内任何动态变量
    • 要捕获使用的变量,可将其名称放在中括号内。
      • 如果只指定变量名,如[z],则将值访问变量
      • 如果在名称前加上 &,如[&count],则按引用访问变量
    • [&]引用访问所有动态变量
    • [=]值访问所有动态变量。

函数指针方法阻止内联,因为编译器传统上不会内联其他地址被获取的函数,因为函数地址的概念意味着非内联函数。而函数符lambda通常不会阻止内联

可以给lambda指定一个名称,然后使用名称来替代。

1
2
3
4
auto mod3 = [] (int x) {return x % 3 == 0;} // mod3 a name for the lambda

count1 = std::count_if(n1.begin(),n1,end(),mod3);
count2 = std::count_if(n2.begin(),n2.end(),mod3);

C++中引入lambda的主要目的:能将类似于函数的表达式用做接受函数指针或函数符的函数的参数。

5. 包装器

C++提供多个包装器wrapper,也叫做适配器(adapter))。

C++11中提供了其他的包装器:

  • bind
    • 可替代 bind1stbind2nd
  • mem_fn
    • 可将成员函数作为常规函数进行传递
  • reference_wrapper
    • 能创建行为像引用但可被复制的对象
  • function
    • 以统一的方式处理多种类似于函数的形式

5.1 包装器function及模板的低效性

模板function 是在头文件functional 中声明,从调用特征标的角度定义了一个对象,可用于包装调用特征相同的函数指针、函数对象或者lambda表达式。

// 接受一个char参数和一个int参数,并返回一个double值的任何函数指针、函数对象或者lambda表达式赋值给它
std::function<double>(char, int) > fdci;

6. 可变参数模板

要创建可变参数模板,需要理解以下要点:

  • 模板参数包(parameter pack)
  • 函数参数包
  • 展开参数包
  • 递归

6.1 模板和函数参数包

C++11中提供省略号表示的元运算符(meta-operator)。

  • 模板参数包:可以声明表示模板参数包的标识符,模板参数包是一个类型列表
  • 函数参数包:可以声明表示函数参数包的标识符,函数参数包是一个值列表
1
2
3
4
5
6
// Args可以与任意数量的类型匹配
template <typename... Args> // Args 是模板参数包
void show_list(Args...args) // args 是函数参数包
{
  ...
}

6.2 展开参数包中使用递归

将函数参数包展开,对列表中的第一项进行处理,再将余下的内容传递给递归调用,以此类推,直到列表为空。

template <typename T ,typename...Args>
void show_list(T value, Args..args) 
{
  show_list(args...);
}

// 对于按值传递来说,效率低,所以可更改为;
void show_list1(const T& value, const Args&... args)
{
  show_list1(args...);
}

7. C++11新增的功能

7.1 并行编程

为解决并行性的问题,C++定义了一个支持线程化执行的内存模型,添加了关键字 thread_local ,提供了相关库支持。

关键字 thread_local 将变量声明为静态存储,其持续性和特定线程相关,即定义变量的线程过期时,变量也过期

库支持由原子操作(automic、operation)库线程支持库组成。原子操作库提供的头文件:

  • thread
  • mutex
  • condition
  • variable
  • future

7.2 新增的库

在C++11中新增专用的头文件库

  • random:随机数扩展工具
  • chrono:处理时间间隔的途径
  • tuple:支持模板tuple。
  • ratio:编译阶段的有理数算术库
  • regex:正则表达式库。用于指定与文本字符串的内容匹配。

7.3 低级编程

低级编程中的“低级”指的是抽象程度,而不是编程质量。

C++11给低级编程人员提供的两个便利:

  • 放松了POD(Plain Old Data)的要求。
  • 允许共用体的成员构造函数析构函数