【读书笔记:C++ primer plus 第六版 中文版】第18章 探讨C++新标准

时间:2021-01-17 14:15:28

转载请注明出处:http://blog.csdn.net/enyusmile/article/details/48679405

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

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

18.1 复习前面介绍过的C++11功能
18.1.1 新类型
18.1.2 统一的初始化

  • C++11扩大了用大括号括起的列表(初始化列表)的适用范围,使其可用于所有内置类型和用户定义的类型(即类对象).使用初始化列表时,可添加登好(=),也可不添加.
  • 1.缩窄
    • 初始化列表语法可防止缩窄,即禁止将数值赋给无法存储它的数值变量.
  • 2.std::initializer_list
    • C++11提供了模板类std::initializer_list,可将其用作构造函数的参数.

18.1.3 声明

  • C++11提供了多种简化声明的功能,尤其在使用模板时.
  • 1.auto
    • 以前,关键字auto是一个存储类型说明符,C++11将其用于实现自动类型推断.
    • 这要求进行显式初始化,让编译器能够将变量的类型设置为初始值的类型.
    • 关键字auto还可简化模板声明.
  • 2.decltype
    • 关键字decltype将变量的类型声明为表达式指定的类型.
    • decltype的工作原理比auto复杂,根据使用的表达式,指定的类型可以为引用和const.
  • 3.返回类型后置
    • 新增的一种函数声明语法:在函数名和参数列表后面(而不是前面)指定返回类型;
  • 4.模板别名:using =
    • typedef和using=的差别在于,新语法也可用于模板部分具体化,单typedef不能;
  • 5.nullptr
    • 空指针是不会指向有效数据的指针.以前,C++在源代码中使用0表示这种指针,但内部表示可能不同.
    • 它是指针类型,不能转换为整型类型.
    • 为向后兼容C++11仍允许使用0来表示空指针,因此表达式nullptr==0为true,但使用nullptr而不是0提供了更高的类型安全.

18.1.4 智能指针

  • 基于程序员的编程体验和BOOST库提供的解决方案,C++11摒弃了auto_ptr,并新增了三种智能指针:unique_ptr,shared_ptr和weak_ptr.
  • 所有新增的智能指针都能与STL容器和移动语义协同工作.

18.1.5 异常规范方面的修改

  • 与auto_ptr一样,C++编程社区的集体经验表明,异常规范的效果没有预期的好.因此,C++11摒弃了异常规范.然而,标准委员会认为,指出函数不会引发异常有一定的价值,他们为此添加了关键字noexcept;

18.1.6 作用域内枚举

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

18.1.7 对类的修改

  • 1.显式转换运算符
    • C++引入了关键字explicit,以禁止单参数构造函数导致的自动转换.
    • C++11扩展了explicit的这种用法,使得可对转换函数做类似的处理.
  • 2.类内成员初始化
    • 这样做,可避免在构造函数中编写重复的代码,从而降低了程序员的工作量,厌倦情绪和出错的机会.
    • 如果构造函数在成员初始化列表中提供了相应的值,这些默认值将被覆盖.

18.1.8 模板和STL方面的修改

  1. 基于范围的for循环
    • 如果要在循环中修改数组或容器的每个元素,可使用引用类型.
  2. 新的STL容器
    • C++11新增了STL容器forward_list,unordered_map,unordered_multimap,unordered_set和unordered_multiset.
    • C++11还新增了模板array.要实例化这种模板,可指定元素类型和固定的元素数.
  3. 新的STL方法
    • C++11新增了STL方法cbegin()和cend().这些新方法将元素视为const.
    • 与此类似,crbegin()和crend()是rbegin()和rend()的const版本.
  4. valarray升级
    • 模板valarray独立于STL开发的,其最初的设计导致无法将基于范围的STL算法用于valarray对象.C++11天假了两个函数(begin()和end()),它们都接收valarray作为参数,并返回迭代器,这些迭代器分别指向valarray对象的第一个元素和最后一个元素后面.
    • 这让你能够将基于范围的STL算法用于valarray.
  5. 摒弃export
    • 仍保留了关键字export,供以后使用
  6. 尖括号
    • 为避免与运算符>>混淆,C++要求在声明嵌套模板时使用空格将尖括号分开,C++11不再这样要求.

18.1.9 右值引用

  • C++11新增了右值引用,这是使用&&表示的.
  • 右值引用可关联到右值,即可出现在赋值表达式右边,但不能对其应用地址运算符的值.
  • 程序清单18.1 rvref.cpp

18.2 移动语义和右值引用
18.2.1 为何需要移动语义

  • 实际文件还留在原来的地方,而只修改记录.这种方法被称为移动语义.有点悖论的是,移动语义实际上避免了移动原始数据,而只是修改了记录.
  • 要实现移动语义,需要采取某种方式,让编译器知道什么时候需要复制,什么时候不需要.这就是右值引用发挥作用的地方.

18.2.2 一个移动示例

  • 程序清单18.2 useless.cpp
// useless.cpp -- an otherwise useless class with move semantics
#include <iostream>
using namespace std;
// interface
class Useless
{
private:
int n; // number of elements
char * pc; // pointer to data
static int ct; // number of objects
void ShowObject() const;
public:
Useless();
explicit Useless(int k);
Useless(int k, char ch);
Useless(const Useless & f); // regular copy constructor
Useless(Useless && f); // move constructor
~Useless();
Useless operator+(const Useless & f)const;
// need operator=() in copy and move versions
void ShowData() const;
};
// implementation
int Useless::ct = 0;
Useless::Useless()
{
++ct;
n = 0;
pc = nullptr;
cout << "default constructor called; number of objects: " << ct << endl;
ShowObject();
}
Useless::Useless(int k) : n(k)
{
++ct;
cout << "int constructor called; number of objects: " << ct << endl;
pc = new char[n];
ShowObject();
}
Useless::Useless(int k, char ch) : n(k)
{
++ct;
cout << "int, char constructor called; number of objects: " << ct << endl;
pc = new char[n];
for (int i = 0; i < n; i++)
pc[i] = ch;
ShowObject();
}
Useless::Useless(const Useless & f): n(f.n)
{
++ct;
cout << "copy const called; number of objects: " << ct << endl;
pc = new char[n];
for (int i = 0; i < n; i++)
pc[i] = f.pc[i];
ShowObject();
}
Useless::Useless(Useless && f): n(f.n)
{
++ct;
cout << "move constructor called; number of objects: " << ct << endl;
pc = f.pc; // steal address
f.pc = nullptr; // give old object nothing in return
f.n = 0;
ShowObject();
}
Useless::~Useless()
{
cout << "destructor called; objects left: " << --ct << endl;
cout << "deleted object:\n";
ShowObject();
delete [] pc;
}
Useless Useless::operator+(const Useless & f)const
{
cout << "Entering operator+()\n";
Useless temp = Useless(n + f.n);
for (int i = 0; i < n; i++)
temp.pc[i] = pc[i];
for (int i = n; i < temp.n; i++)
temp.pc[i] = f.pc[i - n];
cout << "temp object:\n";
cout << "Leaving operator+()\n";
return temp;
}
void Useless::ShowObject() const
{
cout << "Number of elements: " << n;
cout << " Data address: " << (void *) pc << endl;
}
void Useless::ShowData() const
{
if (n == 0)
cout << "(object empty)";
else
for (int i = 0; i < n; i++)
cout << pc[i];
cout << endl;
}
// application
int main()
{
{
Useless one(10, 'x');
Useless two = one; // calls copy constructor
Useless three(20, 'o');
Useless four(one + three); // calls operator+(), move constructor
cout << "object one: ";
one.ShowData();
cout << "object two: ";
two.ShowData();
cout << "object three: ";
three.ShowData();
cout << "object four: ";
four.ShowData();
}
// cin.get();
}
  • 其中最重要的是复制构造函数和移动构造函数的定义.
  • 注意到没有调用移动构造函数,且只创建了4个对象.创建对象four时,该编译器没有调用任何构造函数;相反,它推断出对象four是operator+()所做工作的受益人,因此将operator+()创建的对象转到four的名下.一般而言,编译器完全可以进行优化,只要结构与未优化时相同.即使省略该程序中的移动构造函数,并使用g++进行编译,结构也将相同.

18.2.3 移动构造函数解析

  • 虽然使用右值引用可支持移动语义,但这并不会神奇地发生.要让移动语义发生,需要两个步骤.
    • 首先,右值引用让编译器知道何时可使用移动语义.
    • 第二步,编写移动构造函数,使其提供所需的行为.
  • 总之,通过提供一个使用左值引用的构造函数和一个使用右值引用的构造函数,将初始化分成了量足.使用左值对象初始化对象时,将使用复制构造函数,而使用右值对象初始化对象时,将使用移动构造函数.程序员可根据需要赋予这些构造函数不同的行为.

18.2.4 赋值

  • 适用于构造函数的移动语义考虑也适用于赋值运算符.
  • 与移动构造函数一样,移动赋值运算符的参数也不能是const引用,因为这个方法修改了源对象.

18.2.5 强制移动

  • 可使用运算符static_cast<>将对象的类型强制转换为XXX &&,但C++11提供了一种更简单的方式—使用头文件utility中声明的函数std::move().
  • 程序清单18.3 stdmove.cpp
  • 需要知道的是,函数std::move()并非一定会导致移动操作.
  • 对大多数程序员来说,右值引用带来的主要好处并非是让他们能够编写使用右值引用的代码,而是能够使用利用右值引用实现移动语义的库代码.

18.3 新的类功能
18.3.1 特殊的成员函数

  • 在原有4个特殊成员函数(默认构造函数,赋值构造函数,复制赋值运算符和析构函数)的基础上,C++11新增了两个:移动构造函数和移动赋值运算符.

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

  • 使用关键字default显式地声明这些方法得默认版本.
  • 关键字delete可用于禁止编译器使用特定方法.
  • 关键字default只能用于6个特殊成员函数,但delete可用于任何成员函数.delete的一种可能用法是禁止特定的转换.

18.3.3 委托构造函数

  • C++11允许在一个构造函数的定义中使用另一个构造函数.这被称为委托.

18.3.4 集成构造函数

  • C++11提供了一种让派生类能够集成基类构造函数的机制.
  • 这让派生类集成基类的所有构造函数(默认构造函数,复制构造函数和移动构造函数除外),但不会使用与派生类构造函数的特征标匹配的构造函数.

18.3.5 管理虚方法:override和final

  • 说明符override和final并非关键字,而是具有特殊含义的标识符.

18.4 Lambda函数
18.4.1 比较函数指针,函数符和Lambda函数

  • 名称lambda来自lambda calculus(λ演算)—一种定义和应用函数的数学系统.这个系统让您能够使用匿名函数—即无需给函数命名.
  • 程序清单18.4 lambda0.cpp

18.4.2 为何使用lambda

  • 距离,简洁,效率和功能
  • 函数指针方法组织了内联,因为编译器传统上不会内联其他地址被获取的函数,因为函数地址的概念意味着非内联函数.而函数符和lambda通常不会阻止内联.
  • lambda可访问作用域内的任何动态变量.
  • 程序清单18.5 lambda1.cpp
// lambda1.cpp -- use captured variables
#include <iostream>
#include <vector>
#include <algorithm>
#include <cmath>
#include <ctime>
const long Size = 390000L;
int main()
{
using std::cout;
std::vector<int> numbers(Size);
std::srand(std::time(0));
std::generate(numbers.begin(), numbers.end(), std::rand);
cout << "Sample size = " << Size << '\n';
// using lambdas
int count3 = std::count_if(numbers.begin(), numbers.end(),
[](int x){return x % 3 == 0;});
cout << "Count of numbers divisible by 3: " << count3 << '\n';
int count13 = 0;
std::for_each(numbers.begin(), numbers.end(),
[&count13](int x){count13 += x % 13 == 0;});
cout << "Count of numbers divisible by 13: " << count13 << '\n';
// using a single lambda
count3 = count13 = 0;
std::for_each(numbers.begin(), numbers.end(),
[&](int x){count3 += x % 3 == 0; count13 += x % 13 == 0;});
cout << "Count of numbers divisible by 3: " << count3 << '\n';
cout << "Count of numbers divisible by 13: " << count13 << '\n';
// std::cin.get();
return 0;
}
  • 该程序使用的两种方法(两个独立的lambda和单个lambda)的结果相同.
  • 在C++中引入lambda的主要目的是,能够将类似于函数的表达式用作接收函数指针或函数符的函数的参数.因此,典型的lambda是测试表达式或比较表达式,可编写为一条返回语句.这使得lambda简洁而易于理解,且可自动推断返回类型.

18.5 包装器
18.5.1 包装器function及模板的低效性

  • 程序清单18.6 somedefs.h
  • 程序清单18.7 callable.cpp

18.5.2 修复问题

  • 程序清单18.8 wrapped.cpp

18.5.3 其他方式

18.6 可变参数模板

  • 要创建可变参数模板,需要理解几个要点:
    • 模板参数包(parameter pack);
    • 函数参数包;
    • 展开unpack参数包
    • 递归

18.6.1 模板和函数参数包

  • C++提供了一个用省略号表示的元运算符meta-operator,能够声明表示模板参数包的标识符,模板参数包基本上是一个类型列表.同样,它还让您能够声明表示函数参数包的标识符,而函数参数包基本上是一个值列表.

18.6.2 展开参数包
18.6.3 在可变参数模板函数中使用递归

  • 程序清单18.9 variadic1.cpp
  • 程序清单18.10 variadic2.cpp
// variadic2.cpp
#include <iostream>
#include <string>
// definition for 0 parameters
void show_list() {}
// definition for 1 parameter
template<typename T>
void show_list(const T& value)
{
std::cout << value << '\n';
}
// definition for 2 or more parameters
template<typename T, typename... Args>
void show_list(const T& value, const Args&... args)
{
std::cout << value << ", ";
show_list(args...);
}
int main()
{
int n = 14;
double x = 2.71828;
std::string mr = "Mr. String objects!";
show_list(n, x);
show_list(x*x, '!', 7, mr);
return 0;
}

18.7 C++11新增的其他功能
18.7.1 并行编程

  • 为解决并行性问题,C++定义了一个支持线程化执行的内存模型,添加了关键字thread_local,提供了相关的库文件.关键字thread_local将变量声明为静态存储,其持续性与特定线程相关;即定义这种变量的线程过期时,变量也将过期.
  • 库支持由原子操作atomic operation库和线程支持库组成,其中原子操作库提供了头文件atomic,而线程支持库提供了头文件thread,mutex,condition,variable和future.

18.7.2 新增的库
18.7.3 低级编程

  • 低级编程中的”低级”指的是抽象程度,而不是编程质量.
  • 变化之一是放松了POD(Plain Old Data)的要求.
  • 另一项修改时,允许共用体的成员有构造函数和析构函数,这让共用体更灵活.
  • C++11解决了内存对其问题.要获悉有关类型或对象的对其要求,可使用运算符alignof().要控制对其方式,可使用说明符alignas.
  • constexpr机制让编译器能够在编译阶段计算结果为常量的表达式,让const变量可存储在制度内存中,这对嵌入式编程来说很有用(在运行阶段初始化的变量存储在随机访问内存中).

18.7.4 杂项

18.8 语言变化
18.8.1 Boost项目

  • 该计划的基本理念是,创建一个充当开放论坛的网站,让人发布免费的C++库.这个项目提供有关许可和编程实践的指南,并要求对提议的库进行同行审阅.

18.8.2 TR1

  • Technical Report 1

18.8.3 使用Boost

  • 程序清单18.11 lexcast.cpp

18.9 接下来的任务

  • OOP有助于开发大型项目,并提高其可靠性.OOP方法的基本活动之一是发明能够表示正在模拟的情形(被称为问题域(problem domain))的类.

18.10 总结

  • 无论对新手还是专家来说,新标准都改善了C++的可用性和可靠性.

18.11 复习题
18.12 编程练习

本章源代码下载地址