当前位置:   article > 正文

【C++学习笔记】C++11新特性!你绝对不能错过的干货!

【C++学习笔记】C++11新特性!你绝对不能错过的干货!

[本节目标]

  • 1.C++11简介

  • 2. 列表初始化

  • 3. 变量类型推导

  • 4. 范围for循环

  • 5. final与override

  • 6. 智能指针

  • 7. 新增加容器---静态数组array、forward_list以及unordered系列

  • 8. 默认成员函数控制

  • 9. 右值引用

  • 10. lambda表达式

  • 11.包装器

  • 12. 线程库

1. C++11简介

        在2003年C++标准委员会曾经提交了一份技术勘误表(简称TC1),使得C++03这个名字已经取代了 C++98称为C++11之前的最新C++标准名称。不过由于C++03(TC1)主要是对C++98标准中的漏洞 进行修复,语言的核心部分则没有改动,因此人们习惯性的把两个标准合并称为C++98/03标准。 从C++0x到C++11,C++标准10年磨一剑,第二个真正意义上的标准珊珊来迟。相比于 C++98/03,C++11则带来了数量可观的变化,其中包含了约140个新特性,以及对C++03标准中 约600个缺陷的修正,这使得C++11更像是从C++98/03中孕育出的一种新语言。相比较而言, C++11能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更 强大,而且能提升程序员的开发效率,公司实际项目开发中也用得比较多,所以我们要作为一个 重点去学习。C++11增加的语法特性非常篇幅非常多,我们这里没办法一 一讲解,所以本节课程 主要讲解实际中比较实用的语法。

C++11 - cppreference.com

小故事:

1998年是C++标准委员会成立的第一年,本来计划以后每5年视实际需要更新一次标准,C++国际 标准委员会在研究C++ 03的下一个版本的时候,一开始计划是2007年发布,所以最初这个标准叫 C++ 07。但是到06年的时候,官方觉得2007年肯定完不成C++ 07,而且官方觉得2008年可能也 完不成。最后干脆叫C++ 0x。x的意思是不知道到底能在07还是08还是09年完成。结果2010年的 时候也没完成,最后在2011年终于完成了C++标准。所以最终定名为C++11。

2. 统一的列表初始化

2.1 {}初始化

在C++98中,标准允许使用花括号{}对数组或者结构体元素进行统一的列表初始值设定。比如:

  1. struct Point
  2. {
  3. struct Point
  4. {
  5. int _x;
  6. int _y;
  7. };
  8. int main()
  9. {
  10. int array1[] = { 1, 2, 3, 4, 5 };
  11. int array2[5] = { 0 };
  12. Point p = { 1, 2 };
  13. return 0;
  14. }

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

  1. struct Point
  2. {
  3. int _x;
  4. int _y;
  5. };
  6. int main()
  7. {
  8. int x1 = 1;
  9. int x2 = { 2 };
  10. int x3{ 3 };
  11. int x4 = int(4);//C98 构造
  12. int x5(5);// C98 拷贝构造
  13. int array1[]{ 1, 2, 3, 4, 5 };
  14. int array2[5]{ 0 };
  15. Point p{ 1, 2 };
  16. // C++11中列表初始化也可以适用于new表达式中
  17. int* pa = new int[4] { 0 };
  18. return 0;
  19. }

创建对象时也可以使用列表初始化方式调用构造函数初始化

  1. class Date
  2. {
  3. public:
  4. Date()
  5. :_year(1970)
  6. , _month(1)
  7. , _day(1)
  8. {}
  9. Date(int year, int month, int day)
  10. :_year(year)
  11. , _month(month)
  12. , _day(day)
  13. {
  14. cout << "Date(int year, int month, int day)" << endl;
  15. }
  16. private:
  17. int _year;
  18. int _month;
  19. int _day;
  20. };
  21. // 一切皆可用{}初始化
  22. int main()
  23. {
  24. Date d1(2022, 1, 1); // old style - C++98构造
  25. // C++11支持的列表初始化,这里会调用构造函数初始化
  26. // 多参数的隐式类型转换 构造 + 拷贝构造 -> 优化:直接构造
  27. Date d2{ 2022, 1, 2 };
  28. Date d3 = { 2022, 1, 3 };
  29. //Date d4 = (2022, 1, 3);//不支持
  30. // 构造
  31. string s1("11111");
  32. // 单参数支持隐式类型转换 构造 + 拷贝构造 -> 优化:直接构造
  33. string s2 = "11111";
  34. // 调用拷贝构造
  35. //old style,剩余的的元素会去调用默认构造进行初始化
  36. Date* darr1 = new Date[10]{ d1,d2,d3 };
  37. Date* darr2 = new Date(2022, 1, 1);//构造 + 拷贝构造->优化:直接构造
  38. Date* darr3 = new Date{ 2022, 1, 1 }; //构造 + 拷贝构造->优化:直接构造
  39. //构造 + 拷贝构造 -> 优化:直接构造
  40. Date* darr4 = new Date[3]{ {2022, 1, 1},{2022, 1, 2}, {2022, 1, 3} };
  41. return 0;
  42. }

2.2 std::initializer_list

std::initializer_list的介绍文档

cplusplus.com/reference/initializer_list/initializer_list/

std::initializer_list是什么类型:

  1. int main()
  2. {
  3. // the type of il is an initializer_list
  4. auto il = { 10, 20, 30 };
  5. cout << typeid(il).name() << endl;
  6. return 0;
  7. }

从上面的类型我们可以看出initializer_list本质是一个类模板,为不同的容器对象提供初始化的功能。

std::initializer_list的底层是什么:

initializer_list内部维护了一个指向堆上的数组以及该数组的长度,但它本身并不直接存储这些元素,而是通过其内部的常量指针来访问它们。由于这个指针是常量的,因此std::initializer_list中的元素是只读的,不能通过initializer_list对象来修改它们的值。现在我们就来看一个问题。

  1. int main()
  2. {
  3. //这两个有什么不同
  4. Data d1 = { 2024,2,24 };//
  5. vector<int> v1 = { 1,2,3,4,5,6,7,8,9 };
  6. return 0;
  7. }

我们再来看看当时我们自己写的vector库能不能支持上面的写法。

让模拟实现的vector也支持{}初始化

  1. vector(initializer_list<T>& v)
  2. {
  3. reserve(v.size());
  4. for (auto& e : il)
  5. {
  6. push_back(e);
  7. }
  8. }

std::initializer_list使用场景:

std::initializer_list一般是作为构造函数的参数,C++11对STL中的不少容器就增加 std::initializer_list作为参数的构造函数,这样初始化容器对象就更方便了。

  1. int main()
  2. {
  3. pair<string, string> kv1("sort", "排序");
  4. pair<string, string> kv2("string", "字符串");
  5. map<string, string> dict1 = { kv1, kv2 };
  6. map<string, string> dict2 = { {"sort", "排序"}, {"string", "字符串"} };
  7. pair<const char*, const char*> kv3("sort", "排序");
  8. pair<const string, string> kv4(kv3);
  9. return 0;
  10. }

根据initializer_list本质是一个类模板,这里dtict1就是一个initializer_list<pair<const string,string>>的类型,对于dict2却是一个initializer_list<pair<const char*,const char*>>,dict2initializer_list的初始化,也要转化为nitializer_list<pair<const string,string>>的类型,要完成那他们之前是怎么转化的呢?这里我们就要探究一下,我们先来看下pair的内部构造。

  1. template <class T1, class T2>
  2. struct pair
  3. {
  4. // pair<const char*, const char*> kv3("sort", "排序");
  5. pair(const T1& t1, const T2& t2)
  6. :first(t1)
  7. , second(t2)
  8. {}
  9. // pair<const string, string> kv4(kv3);
  10. // 拷贝构造没有要求同类型进行拷贝构造
  11. template<class U, class V>//这里U、V都是const char*
  12. pair(const pair<U, V>& kv)
  13. // const*能构造成string
  14. : first(kv.first)
  15. , second(kv.second)
  16. {}
  17. private:
  18. T1 first;
  19. T2 second;
  20. };

所以这里能转化的本质是pair的拷贝构造也是一个模板,并不一定要求是同类型的拷贝构造。

也可以作为operator= 的参数,这样就可以用大括号赋值。

  1. int main()
  2. {
  3. vector<int> v = { 1,2,3,4 };
  4. list<int> lt = { 1,2 };
  5. // 这里{"sort", "排序"}会先初始化构造一个pair对象
  6. map<string, string> dict = { {"sort", "排序"}, {"insert", "插入"} };
  7. // 使用大括号对容器赋值
  8. v = { 10, 20, 30 };
  9. return 0;
  10. }

让模拟实现的vector也支持赋值

  1. vector<T>& operator=(initializer_list<T> l)
  2. {
  3. vector<T> tmp(l);
  4. std::swap(_start, tmp._start);
  5. std::swap(_finish, tmp._finish);
  6. std::swap(_endofstorage, tmp._endofstorage);
  7. return *this;
  8. }

3. 声明

c++11提供了多种简化声明的方式,尤其是在使用模板时。

3.1 auto

在C++98中auto是一个存储类型的说明符,表明变量是局部自动存储类型,但是局部域中定义局 部的变量默认就是自动存储类型,所以auto就没什么价值了。C++11中废弃auto原来的用法,将 其用于实现自动类型推断。这样要求必须进行显示初始化,让编译器将定义对象的类型设置为初 始化值的类型。

在C++11之后,auto还可以用作返回值,但不建议使用。

  1. auto func2()
  2. {
  3. int y = 0;
  4. return y;
  5. }
  6. auto func1()
  7. {
  8. auto ret = func2();
  9. return ret;
  10. }
  11. int main()
  12. {
  13. auto x = func1();
  14. return 0;
  15. }

确实这个代码没有报错,但是我们要想知道x的类型,我们还要一层一层往上看返回值的类型,那这不是多此一举嘛!我直接写明数据类型不更好,虽然可能长一点,但是容易观察。

3.2 decltype

关键字decltype将变量的类型声明为表达式指定的类型。

  1. auto func1()
  2. {
  3. auto ret = 1;
  4. return ret;
  5. }
  6. int main()
  7. {
  8. const int x = 1;
  9. double y = 2.2;
  10. cout << typeid(x).name() << endl;
  11. cout << typeid(string).name() << endl;
  12. //typeid(string).name() s;//不能定义变量
  13. decltype(x) z = 1;
  14. cout << typeid(z).name() << endl;//输出int,会去掉顶层的const
  15. // 何为顶层:修饰本身,何为底层:修饰指向的内容
  16. const int* p1 = &x;
  17. cout << typeid(p1).name() << endl;
  18. decltype(p1) p2 = nullptr;
  19. cout << typeid(p2).name() << endl;//输出const int*,不会去掉顶层的const
  20. // 实际使用场景
  21. auto ret = func1();
  22. // 假设要用vector存func1的数据,此时不知道func1返回值的类型
  23. vector<decltype(ret)> v;
  24. return 0;
  25. }

3.3 nullptr

由于C++中NULL被定义成字面量0,这样就可能回带来一些问题,因为0既能指针常量,又能表示 整形常量。所以出于清晰和安全的角度考虑,C++11中新增了nullptr,用于表示空指针。

  1. #ifndef NULL
  2. #ifdef __cplusplus
  3. #define NULL 0
  4. #else
  5. #define NULL ((void *)0)
  6. #endif
  7. #endif

4 范围for循环

这个我们在前面的课程中已经进行了非常详细的讲解,这里就不进行讲解了,请参考C++入门 +STL容器部分的课件讲解。

5 智能指针

这个我们在智能指针课程中已经会进行了非常详细的讲解,这里就不进行讲解了,请参考智能指 针部分课件讲解。

6 STL中一些变化

新容器

用橘色圈起来是C++11中的一些几个新容器,但是实际最有用的是unordered_map和 unordered_set。这两个我们前面已经进行了非常详细的讲解,其他的大家了解一下即可。

容器中的一些新方法

如果我们再细细去看会发现基本每个容器中都增加了一些C++11的方法,但是其实很多都是用得 比较少的。

比如提供了cbegin和cend方法返回const迭代器等等,但是实际意义不大,因为begin和end也是可以返回const迭代器的,这些都是属于锦上添花的操作。

实际上C++11更新后,容器中增加的新方法最后用的插入接口函数的右值引用版本

但是这些接口到底意义在哪?网上都说他们能提高效率,他们是如何提高效率的? 请看下面的右值引用和移动语义章节的讲解。另外emplace还涉及模板的可变参数,也需要再继续深入学习后面章节的知识。

7 右值引用和移动语义

7.1 左值引用和右值引用

传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,所以从现在开始我们 之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名

什么是左值?左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址,一般情况下可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。定义时const修饰符后的左值,不能给他修改,但是可以取它的地址。那我就在这里提一个问题,operator++()属于左值嘛?我们可以想一下operator的返回值,T& operator++(),我们可以看到这里是返回对象的别名,所以这里也是左值,++obj也是左值。

什么是左值引用?左值引用就是给左值的引用,给左值取别名。

  1. int func1()
  2. {
  3. static int x = 0;
  4. return x;//返回x的拷贝 -> 右值
  5. }
  6. int& func2()
  7. {
  8. static int x = 0;
  9. return x;//返回x的引用 -> 左值
  10. }
  11. int main()
  12. {
  13. // 以下的p、b、c、*p, func2()返回值 都是左值
  14. int* p = new int(0);
  15. int b = 1;
  16. const int c = 2;
  17. const int* ptr1 = &c;
  18. int* ptr2 = &func2();
  19. printf("%p %p\n", ptr1, ptr2);
  20. // 左值引用就是给左值取别名
  21. int*& rp = p;
  22. int& rb = b;
  23. const int& rc = c;
  24. int& pvalue = *p;
  25. return 0;
  26. }

运行结果:

什么是右值?什么是右值引用? 右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。右值引用就是对右值的引用,给右值取别名。operator++(int)属于右值嘛?属于,T operator++(int),后置加加返回加加之后的值,是一个临时变量,此时传值返回,同样obj++也是右值。

  1. int func1()
  2. {
  3. static int x = 0;
  4. return x;//返回x的拷贝 -> 右值
  5. }
  6. int main()
  7. {
  8. double x = 1.1, y = 2.2;
  9. double& r1 = x;
  10. // 以下几个都是常见的右值,右值不能取地址
  11. 10;
  12. x + y;
  13. func1();
  14. // 以下几个都是对右值的右值引用
  15. int&& rr1 = 10;
  16. double&& rr2 = x + y;
  17. double&& rr3 = func1();;
  18. // 这里编译会报错:error C2106: “=”: 左操作数必须为左值
  19. /*10 = 1;
  20. x + y = 1;
  21. func1() = 1;*/
  22. return 0;
  23. }

我们来看一下底层原理

需要注意的是右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可 以取到该位置的地址,也就是说例如:不能取字面量10的地址,但是rr1引用后,可以对rr1取地 址,也可以修改rr1。如果不想rr1被修改,可以用const int&& rr1 去引用,是不是感觉很神奇, 这个了解一下实际中右值引用的使用场景并不在于此,这个特性也不重要。

7.2 左值引用与右值引用比较

左值引用能否给右值直接取别名?右值引用能否给左值直接取别名?都不行,但是有手段可以。

  1. int func1()
  2. {
  3. static int x = 0;
  4. return x;//返回x的拷贝 -> 右值
  5. }
  6. int main()
  7. {
  8. // 左值引用能否给右值取别名 不能
  9. // 但是const左值引用可以
  10. const int& r1 = func1();
  11. const int& r2 = 10;
  12. // 右值引用能否给左值取别名 不能
  13. // 但是右值引用可以给move以后的左值可以
  14. int x = 0;
  15. int&& rr1 = move(x);
  16. return 0;
  17. }

左值引用总结:

1. 左值引用只能引用左值,不能引用右值。

2. 但是const左值引用既可引用左值,也可引用右值。

  1. int main()
  2. {
  3. // 左值引用只能引用左值,不能引用右值。
  4. int a = 10;
  5. int& ra1 = a; // ra为a的别名
  6. //int& ra2 = 10; // 编译失败,因为10是右值
  7. // const左值引用既可引用左值,也可引用右值。
  8. const int& ra3 = 10;
  9. const int& ra4 = a;
  10. return 0;
  11. }

右值引用总结:

1. 右值引用只能右值,不能引用左值。

2. 但是右值引用可以move以后的左值。

  1. int main()
  2. {
  3. // 右值引用只能右值,不能引用左值。
  4. int&& r1 = 10;
  5. // error C2440: “初始化”: 无法从“int”转换为“int &&”
  6. // message : 无法将左值绑定到右值引用
  7. int a = 10;
  8. int&& r2 = a;
  9. // 右值引用可以引用move以后的左值
  10. int&& r3 = std::move(a);
  11. return 0;
  12. }

7.3 右值引用使用场景和意义

引用的本质就是减少拷贝。前面我们可以看到左值引用既可以引用左值和又可以引用右值,那为什么C++11还要提出右值引 用呢?是不是化蛇添足呢?下面我们来看看左值引用的短板,右值引用是如何补齐这个短板的!我们先来手撕一下string容器来详谈这个问题。

  1. namespace bit
  2. {
  3. class string
  4. {
  5. public:
  6. typedef char* iterator;
  7. iterator begin()
  8. {
  9. return _str;
  10. }
  11. iterator end()
  12. {
  13. return _str + _size;
  14. }
  15. string(const char* str = "")
  16. :_size(strlen(str))
  17. , _capacity(_size)
  18. {
  19. //cout << "string(char* str)" << endl;
  20. _str = new char[_capacity + 1];
  21. strcpy(_str, str);
  22. }
  23. // s1.swap(s2)
  24. void swap(string& s)
  25. {
  26. ::swap(_str, s._str);
  27. ::swap(_size, s._size);
  28. ::swap(_capacity, s._capacity);
  29. }
  30. // 拷贝构造
  31. string(const string& s)
  32. :_str(nullptr)
  33. {
  34. cout << "string(const string& s) -- 深拷贝" << endl;
  35. string tmp(s._str);
  36. swap(tmp);
  37. }
  38. // 赋值重载
  39. string& operator=(const string& s)
  40. {
  41. cout << "string& operator=(string s) -- 深拷贝" << endl;
  42. string tmp(s);
  43. swap(tmp);
  44. return *this;
  45. }
  46. ~string()
  47. {
  48. delete[] _str;
  49. _str = nullptr;
  50. }
  51. char& operator[](size_t pos)
  52. {
  53. assert(pos < _size);
  54. return _str[pos];
  55. }
  56. void reserve(size_t n)
  57. {
  58. if (n > _capacity)
  59. {
  60. char* tmp = new char[n + 1];
  61. strcpy(tmp, _str);
  62. delete[] _str;
  63. _str = tmp;
  64. _capacity = n;
  65. }
  66. }
  67. void push_back(char ch)
  68. {
  69. if (_size >= _capacity)
  70. {
  71. size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
  72. reserve(newcapacity);
  73. }
  74. _str[_size] = ch;
  75. ++_size;
  76. _str[_size] = '\0';
  77. }
  78. //string operator+=(char ch)
  79. string& operator+=(char ch)
  80. {
  81. push_back(ch);
  82. return *this;
  83. }
  84. const char* c_str() const
  85. {
  86. return _str;
  87. }
  88. private:
  89. char* _str = nullptr;
  90. size_t _size = 0;
  91. size_t _capacity = 0; // 不包含最后做标识的\0
  92. };
  93. bit::string to_string(int value)
  94. {
  95. bool flag = true;
  96. if (value < 0)
  97. {
  98. flag = false;
  99. value = 0 - value;
  100. }
  101. bit::string str;
  102. while (value > 0)
  103. {
  104. int x = value % 10;
  105. value /= 10;
  106. str += ('0' + x);
  107. }
  108. if (flag == false)
  109. {
  110. str += '-';
  111. }
  112. std::reverse(str.begin(), str.end());
  113. return str
  114. }
  115. }
  116. int main()
  117. {
  118. // 在bit::string to_string(int value)函数中可以看到,这里
  119. // 只能使用传值返回,传值返回会导致至少1次拷贝构造
  120. // (如果是一些旧一点的编译器可能是两次拷贝构造)。
  121. bit::string ret1 = bit::to_string(1234);
  122. bit::string ret2 = bit::to_string(-1234);
  123. return 0;
  124. }

左值引用的使用场景:做参数和做返回值都可以提高效率,解决了传参拷贝的问题,以及部分对象返回拷贝的问题(出了函数作用域,返回对象还在,可以左值引用返回,减少了拷贝)。

如果返回的对象是一个局部变量,出了函数作用域声明周期就到了,只能传值返回,就存在拷贝,比如返回的类型是vector<vector<int>>,消耗巨大,我们来举一个例子。 

  • 编译器没有进行优化的时候,存在两次拷贝构造,拷贝的消耗较大。

编译器进行优化,只存在依次拷贝构造,但是拷贝的消耗依然较大。

C++11之前,编译器已经做了不小努力,去减少拷贝但是还不够,C+ +增加右值引用进一步努力,再减少拷贝,传值返回就没有拷贝。我们先来看一下编译器的匹配问题。

  1. void func(const int& i)
  2. {
  3. cout << "void func(const int& i)" << endl;
  4. }
  5. void func(int&& i)
  6. {
  7. cout << "void func(int&& i)" << endl;
  8. }
  9. int main()
  10. {
  11. // 编译器匹配的问题
  12. int a = 0;
  13. func(a);//a是左值
  14. func(10);//10是右值
  15. return 0;
  16. }

看一下运行结果:

c++11之前对传值返回的解决优化。

左值(健康的人)和右值(暮年的人)的本质和移动构造的场景本质。

现在我们来看一下右值引用是如何解决的。

左值引用的短板:

但是当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回, 只能传值返回。例如:bit::string to_string(int value)函数中可以看到,这里只能使用传值返回, 传值返回会导致至少1次拷贝构造(如果是一些旧一点的编译器可能是两次拷贝构造)。

此时运行程序也就能发现确实调用了一次拷贝构造,此时是传值返回发生的一次赋值拷贝。

如果我们这里使用左值引用返回,那么此时程序肯定会出现问题,因为此时函数返回对象是一个局部变量,出了函数作用域就不存在了,此时已经调用析构函数了,但是我们此时仍然去进行拷贝构造,此时拷贝的对象就是有问题的,从而程序也就会崩溃。

那我们这里能不能使用右值返回呢?

此时程序还是有问题?这里我们需要注意,左值引用和右值引用都是返回str的别名,我们这里的本质还是因为函数返回对象是一个局部变量,出了函数作用域就不存在了,此时已经调用析构函数了,那么此时引用的对象是一个已经被销毁的对象。

我们就需要右值引用和移动语义解决上述问题: 在bit::string中增加移动构造,移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己。

  1. // 拷贝构造
  2. // 此时既会可以引用左值,也可以引用右值
  3. string(const string& s)
  4. :_str(nullptr)
  5. {
  6. cout << "string(const string& s) -- 深拷贝" << endl;
  7. string tmp(s._str);
  8. swap(tmp);
  9. }
  10. string(const string& s)// 传统写法
  11. :_str(nullptr)
  12. {
  13. cout << "string(const string& s) -- 深拷贝" << endl;
  14. _str = new char[s._capacity + 1];
  15. strcpy(_str, s._str);
  16. _size = s._size;
  17. _capacity = s._capacity;
  18. }
  19. // 此时更匹配引用右值
  20. // 右值都是临时构建的,所以也称作僵王值
  21. string(string&& s)
  22. :_str(nullptr)
  23. {
  24. cout << "string(string&& s) -- 移动拷贝" << endl;
  25. swap(s);
  26. }

再运行上面bit::to_string的两个调用,我们会发现,这里没有调用深拷贝的拷贝构造,而是调用 了移动构造,移动构造中没有新开空间,拷贝数据,所以效率提高了。注意我们这里右值引用并没有延长对象的生命周期,而是延长了对象里面资源的生命周期。如果我们分成两行来写呢?

我们先来分析一下tmp是左值和右值的情况。

  1. // 移动赋值
  2. string& operator=(string&& s)
  3. {
  4. cout << "string& operator=(string&& s) -- 移动语义" << endl;
  5. swap(s);
  6. return *this;
  7. }
  8. int main()
  9. {
  10. bit::string ret1;
  11. ret1 = bit::to_string(1234);
  12. return 0;
  13. }
  14. // 运行结果:
  15. // string(string&& s) -- 移动语义
  16. // string& operator=(string&& s) -- 移动语义

在bit::string类中增加移动赋值函数,再去调用bit::to_string(1234),不过这次是将 bit::to_string(1234)返回的右值对象赋值给ret1对象,这时调用的是移动构造,我们来看看此时的优化。

这里运行后,我们看到调用了一次移动构造和一次移动赋值。因为如果是用一个已经存在的对象 接收,编译器就没办法优化了。bit::to_string函数中会先用str生成构造生成一个临时对象,但是 我们可以看到,编译器很聪明的在这里把str识别成了右值,调用了移动拷贝。然后在把这个临时 对象做为bit::to_string函数调用的返回值赋值给ret1,这里调用的移动赋值。总结:左值引用没有解决的问题,右值引用解决了。深拷贝对象传值返回只需要移动资源,代价很低。

STL中的容器都是增加了移动构造和移动赋值:

7.4 右值引用引用左值及其一些更深入的使用场景分析

按照语法,右值引用只能引用右值,但右值引用一定不能引用左值吗?因为:有些场景下,可能 真的需要用右值去引用左值实现移动语义。当需要用右值引用一个左值时,可以通过move 函数将左值转化为右值。C++11中,std::move()函数位于头文件中,该函数名字具有迷惑性, 它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,然后实现移动语义。现在我们先来看一下库里面的list容器输出结果是怎么样?

  1. int main()
  2. {
  3. list<bit::string> lt;
  4. bit::string s1("11111");
  5. lt.push_back(s1);// 有名对象
  6. cout << "---------" << endl;
  7. lt.push_back(bit::string("2222"));// 匿名对象
  8. cout << "---------" << endl;
  9. lt.push_back("2222");// 隐式类型转换
  10. cout << "---------" << endl;
  11. bit::string s2 = "3333";//构造 + 拷贝构造 -> 合二为一直接构造
  12. const bit::string& s3 = "3333";//左值引用临时对象
  13. bit::string&& s4 = "3333";//右值引用临时对象
  14. return 0;
  15. }

我们来看一下运行结果:

我们发现这里插入的代价是不一样的,有名对象插入的是左值,而匿名对象和隐式类型转换都是插入的右值,因为匿名对象和隐式类型转换的时候都会出现临时对象,如果我们要解决这里就需要将s1变成右值,才能减少拷贝。

move一个左值并不能将左值本身直接变成右值,我们这里可以理解为强转类型转换一样。

我们可以看到上面确实减少了拷贝,但是我自己的资源却没有了,不要轻易的对左值move,除非你准备好这个左值资源被拿走。对于上面的解决这个问题我们首先就要解决到底是那个地方出现了拷贝。

这里本质是申请节点空间出现了拷贝,那我们先来用我们自己的库来解决这个问题,我们首先来看直接使用运行上面的代码的结果

首先我们这里都是拷贝构造,因为此时我们还没有为push_back提供右值插入的函数。

  1. // 左值插入
  2. void push_back(const T& x)
  3. {
  4. insert(end(), x);
  5. }
  6. // 右值插入
  7. void push_back(T&& x)
  8. {
  9. insert(end(), x);
  10. }

我们来看一下运行结果

此时还都是拷贝构造,因为我们的尾插复用了insert函数,我们需要对insert写一个右值插入的函数。

  1. iterator insert(iterator pos, const T& x)
  2. {
  3. Node* cur = pos._node;//访问__list_iterator类成员 - Node*
  4. Node* prev = cur->_prev;
  5. Node* newnode = new Node(x);
  6. //链接上面三个结点prev newnode cur
  7. prev->_next = newnode;
  8. newnode->_prev = prev;
  9. newnode->_next = cur;
  10. cur->_prev = newnode;
  11. return newnode;
  12. }
  13. iterator insert(iterator pos, T&& x)
  14. {
  15. Node* cur = pos._node;//访问__list_iterator类成员 - Node*
  16. Node* prev = cur->_prev;
  17. Node* newnode = new Node(x);
  18. //链接上面三个结点prev newnode cur
  19. prev->_next = newnode;
  20. newnode->_prev = prev;
  21. newnode->_next = cur;
  22. cur->_prev = newnode;
  23. return newnode;
  24. }

 我们来看一下运行结果

为什么还不对呢?因为我们这里申请了节点,调用构造函数,所以我们需要在构造函数那里提供右值的函数。

  1. ListNode(const T& x = T())
  2. :_next(nullptr)
  3. ,_prev(nullptr)
  4. ,_data(x)//左值调用拷贝构造
  5. {}
  6. ListNode(T&& x)
  7. :_next(nullptr)
  8. , _prev(nullptr)
  9. , _data(x)//右值调用移动构造
  10. {}

 我们来看一下运行结果

为什么还不对呢?我们调式发现push_back调用的是右值的,但是跳入到insert确实调用的左值的。这是我们有一个特性,我们来看下面的代码。

  1. int main()
  2. {
  3. // 右值被右值引用以后,右值引用r的属性是左值
  4. // 为什么要这样设计?
  5. int&& r = 10;
  6. r++; // no error
  7. return 0;
  8. }

那这里r修改的是谁呢?我们来看一下底层,右值引用的底层是指针,本质是把我们的右值拷贝到了一个临时空间,右值引用就是存的这个临时空间的地址,这里修改r,不能修改10,而是修改临时空间指向的内容,所以我们这里也就存在const右值引用,const右值引用所指向的临时空间不可以被修改。

但是为什么要这么设计呢?

所以这就可以解释上面的现象啦,右值传给我们的push_back的右值函数,这里是没有问题的,但是push_back内部调用的是insert(end(),x),此时x的属性就是左值了,从而会调用insert左值插入的函数,怎么解决呢?对x进行move一下,insert(end(),move(x)),但是此时进入insert时,此时进入的是右值插入,但是Node* newnode = new Node(x),此时x又变成左值了,所以还需要move一下,在构造函数那里也需要对_data(move(x))一下才可以。

7.5 完美转发

模板中的&& 万能引用

  1. template<typename T>
  2. void PerfectForward(T&& t)//右值版本
  3. {
  4. cout << "PerfectForward(T&& t)" << endl;
  5. }
  6. template<typename T>
  7. void PerfectForward(const T& t)//左值版本
  8. {
  9. cout << "PerfectForward(const T& t)" << endl;
  10. }
  11. int main()
  12. {
  13. PerfectForward(10); // 右值
  14. int a;
  15. PerfectForward(a); // 左值
  16. PerfectForward(std::move(a)); // 右值
  17. return 0;
  18. }

我们首先来看一下上面程序输出的结果:

我们发现一个现象,无论是左值还是右值,经过模板实例化之后,都是调用的右值的模板,本来应该是能区别的,但是C++11的委员会设计者不想这样进行区分,因为我们上面使用的是模板,我们并不像让它去被区分,让它能够自己推演。

  1. // 模板中的 && 不代表右值引用,而是万能引用,其既能接收左值又能接收右值
  2. // 模板的万能引用提供了能够接收同时接收左值引用和右值引用的能力
  3. template<typename T>
  4. void PerfectForward(T&& t)//万能版本
  5. {
  6. cout << "PerfectForward(T&& t)" << endl;
  7. }
  8. // 推演
  9. void PerfectForward(int&& t)//右值
  10. {
  11. cout << "PerfectForward(int&& t)" << endl;
  12. }
  13. void PerfectForward(int& t)//左值
  14. {
  15. cout << "PerfectForward(int& t)" << endl;
  16. }
  17. int main()
  18. {
  19. PerfectForward(10); // 右值
  20. int a;
  21. PerfectForward(a); // 左值
  22. PerfectForward(std::move(a)); // 右值
  23. return 0;
  24. }

我们来看一下运行结果:

如果是我们的const左值和const右值,我们的万能引用还可以嘛?

虽然万能引用能既能接收左值又能接收右值,但是经过推演之后的属性还是之前的属性。

  1. // 极度追求效率
  2. // 引用折叠
  3. // & && -> &
  4. // && && -> &&
  5. // 不能单纯把下面的模板理解成右值引用的模板
  6. // 万能引用-> 传左值,他就是左值引用
  7. // 万能引用-> 传右值,他就是右值引用
  8. template<typename T>
  9. void PerfectForward(T&& t)
  10. {
  11. cout << "void PerfectForward(T&& t)" << endl;
  12. }
  13. //实例化以下四个版本的函数
  14. void PerfectForward(int&& t)
  15. {
  16. cout << "右值引用" << endl;
  17. }
  18. void PerfectForward(int& t)
  19. {
  20. cout << "左值引用" << endl;
  21. }
  22. void PerfectForward(const int&& t)
  23. {
  24. cout << "const 右值引用" << endl;
  25. }
  26. void PerfectForward(const int& t)
  27. {
  28. cout << "const 左值引用" << endl;
  29. }
  30. int main()
  31. {
  32. PerfectForward(10); // 右值
  33. int a;
  34. PerfectForward(a); // 左值
  35. PerfectForward(std::move(a)); // 右值
  36. const int b = 8;
  37. PerfectForward(b); // const 左值
  38. PerfectForward(std::move(b)); // const 右值
  39. return 0;
  40. }

运行结果:

我们写个函数来看看到底是不是上面的四种形式

  1. void Fun(int& x) { cout << "左值引用" << endl; }
  2. void Fun(const int& x) { cout << "const 左值引用" << endl; }
  3. void Fun(int&& x) { cout << "右值引用" << endl; }
  4. void Fun(const int&& x) { cout << "const 右值引用" << endl; }
  5. // 模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。
  6. // 模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力,
  7. // 但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值,
  8. // 我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用我们下面学习的完美转发
  9. template<typename T>
  10. void PerfectForward(T&& t)
  11. {
  12. Fun(t);
  13. }
  14. int main()
  15. {
  16. PerfectForward(10); // 右值
  17. int a;
  18. PerfectForward(a); // 左值
  19. PerfectForward(std::move(a)); // 右值
  20. const int b = 8;
  21. PerfectForward(b); // const 左值
  22. PerfectForward(std::move(b)); // const 右值
  23. return 0;
  24. }

我们来看一下运行结果:

我们发现此时都变成左值了,对于左值传过去没有任何影响,但是对于右值,此时就会发生属性转变,变成左值,怎么解决呢?我们这里能不能move一下,这里是不能的,如果我们move,那么都会变成右值,要解决这里的问题,我们就要使用完美转发。

std::forward 完美转发在传参的过程中保留对象原生类型属性

  1. // move 左值属性->右值属性
  2. // forward 保持属性->本身是左值,就不变;
  3. // 本身是右值,右值引用后,属性是左值,转成右值,相当于move一下
  4. void Fun(int& x) { cout << "左值引用" << endl; }
  5. void Fun(const int& x) { cout << "const 左值引用" << endl; }
  6. void Fun(int&& x) { cout << "右值引用" << endl; }
  7. void Fun(const int&& x) { cout << "const 右值引用" << endl; }
  8. // std::forward<T>(t)在传参的过程中保持了t的原生类型属性。
  9. template<typename T>
  10. void PerfectForward(T&& t)
  11. {
  12. Fun(std::forward<T>(t));
  13. }
  14. int main()
  15. {
  16. PerfectForward(10); // 右值
  17. int a;
  18. PerfectForward(a); // 左值
  19. PerfectForward(std::move(a)); // 右值
  20. const int b = 8;
  21. PerfectForward(b); // const 左值
  22. PerfectForward(std::move(b)); // const 右值
  23. return 0;
  24. }

8 新的类功能

默认成员函数

原来C++类中,有6个默认成员函数:

  • 1. 构造函数
  • 2. 析构函数
  • 3. 拷贝构造函数
  • 4. 拷贝赋值重载
  • 5. 取地址重载
  • 6. const 取地址重载

最后重要的是前4个,后两个用处不大。默认成员函数就是我们不写编译器会生成一个默认的。

C++11 新增了两个:移动构造函数和移动赋值运算符重载。

针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下:

  1. class Person
  2. {
  3. public:
  4. Person(const char* name = "", int age = 0)
  5. :_name(name)
  6. , _age(age)
  7. {}
  8. private:
  9. std::string _name;
  10. int _age;
  11. };
  12. int main()
  13. {
  14. Person s1;
  15. // 拷贝构造
  16. // 内置类型会进行浅拷贝
  17. // 自定义类型会去调用它的拷贝构造
  18. Person s2 = s1;
  19. // 移动构造
  20. // 内置类型会进行浅拷贝
  21. // 自定义类型会去调用它的移动构造
  22. // 如果没有显示写,就会去调用拷贝构造
  23. Person s3 = std::move(s1);
  24. // 移动赋值
  25. Person s4;
  26. s4 = std::move(s2);
  27. return 0;
  28. }

运行结果:

如果string内部没有实现移动构造就会调用拷贝构造函数。

如果我们上面的Person类,没有实现移动构造,但是我们实现了析构函数、拷贝构造和拷贝赋值重载中的任意一种,那么此时就不会生成默认的移动构造,此时也不会去调用string的移动构造了。

类成员变量初始化

C++11允许在类定义时给成员变量初始缺省值,默认生成构造函数会使用这些缺省值初始化,这 个我们在类和对象默认就讲了,这里就不再细讲了。

强制生成默认函数的关键字default:

C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原 因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以 使用default关键字显示指定移动构造生成。

我们如果显示的写明了构造函数,那么编译器就不会生成默认的构造函数了,而拷贝构造也是构造函数的一种类型,此时编译器就不会生成默认的构造函数了。

  1. class Person
  2. {
  3. public:
  4. Person(const char* name = "", int age = 0)
  5. :_name(name)
  6. , _age(age)
  7. {}
  8. Person(Person&& p) = default;
  9. Person& operator=(Person&& p) = default;
  10. ~Person()
  11. {}
  12. private:
  13. std::string _name;
  14. int _age;
  15. };

但是此时出现了一个问题呢?

这是因为我们显示的生成了移动构造,那么编译器默认生成的拷贝构造就不会生成了,此时我们再强制生成拷贝构造就解决问题了。

  1. class Person
  2. {
  3. public:
  4. Person(const char* name = "", int age = 0)
  5. :_name(name)
  6. , _age(age)
  7. {}
  8. Person(const Person& p) = default;
  9. Person(Person&& p) = default;
  10. Person& operator=(Person&& p) = default;
  11. ~Person()
  12. {}
  13. private:
  14. std::string _name;
  15. int _age;
  16. };

禁止生成默认函数的关键字delete:

如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且只声明补丁 已,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上=delete即 可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。

一个类对象如果不想被拷贝,在C++98中,可以在类里面声明拷贝构造,类外面不实现,但是防不住。

  1. class A
  2. {
  3. public:
  4. // 只声明不实现 C++98
  5. A(const A& aa); //此时编译器不会生成默认的拷贝构造
  6. // 类里面声明,类外定义,但是类外没有定义,此时出现链接错误
  7. // 但是此时的构造函数也不会生成
  8. A() = default;
  9. private:
  10. int _a;
  11. };
  12. // 使用者看到是链接错误,认为写的人忘记实现了
  13. // 于是自己来实现
  14. A::A(const A& aa)
  15. {
  16. //实现
  17. }
  18. int main()
  19. {
  20. A aa1;
  21. A aa2 = aa1;//此时编译器会使用默认生成的拷贝构造进行拷贝
  22. // 不希望A类对象被拷贝
  23. return 0;
  24. }

在C++98中,是该函数设置成private来类对象不被拷贝。

注意:我们不能定义成私有,而是声明在私有,定义成私有成私有类里面依然可以使用,没有防死,同时我们不期待拷贝对象,那么定义的拷贝构造函数怎么实现呢。

继承和多态中的final与override关键字

这个我们在继承和多态章节已经进行了详细讲解这里就不再细讲,需要的话去复习继承和多台章 节吧。

9 可变参数模板

C++11的新特性可变参数模板能够让您创建可以接受可变参数的函数模板和类模板,相比 C++98/03,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改 进。然而由于可变模版参数比较抽象,使用起来需要一定的技巧,所以这块还是比较晦涩的。现 阶段呢,我们掌握一些基础的可变参数模板特性就够我们用了,所以这里我们点到为止,以后大 家如果有需要,再可以深入学习。

下面就是一个基本可变参数的函数模板

  1. // Args是一个模板参数包,args是一个函数形参参数包
  2. // 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
  3. template <class ...Args>
  4. void ShowList(Args... args)
  5. {
  6. // 计算参数包的数据个数
  7. cout << sizeof...(args) << endl;
  8. // 能不能像数组一样使用呢?不能
  9. // 模板是编译时解析
  10. // main函数的agrv是在运行时解析
  11. //for (size_t i = 0; i < length; i++)
  12. //{
  13. // cout << args[i] << " ";//运行时解析参数
  14. //}
  15. //cout << endl;
  16. // 那如何解析呢?递归逻辑
  17. }
  18. int main()
  19. {
  20. ShowList();
  21. ShowList(1);
  22. ShowList(1, 'A');
  23. ShowList(1, 'A', std::string("sort"));
  24. return 0;
  25. }

上面的参数args前面有省略号,所以它就是一个可变模版参数,我们把带省略号的参数称为“参数 包”,它里面包含了0到N(N>=0)个模版参数。我们无法直接获取参数包args中的每个参数的, 只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特 点,也是最大的难点,即如何展开可变模版参数。由于语法不支持使用args[i]这样方式获取可变 参数,所以我们的用一些奇招来一一获取参数包的值。

递归函数方式展开参数包

  1. // 编译时递归返回条件
  2. void _ShowList()
  3. {
  4. cout << endl;
  5. }
  6. // 如果想依次拿到每个参数类型和值,编译时递归解析
  7. template <class T, class ...Args>
  8. void _ShowList(const T& val, Args... args)
  9. {
  10. //这里不能用if判断递归条件,因为这里是编译时,不是运行时的
  11. cout << val << " ";// 解析第一个参数
  12. _ShowList(args...);// 传入剩下两个参数
  13. }
  14. template <class ...Args>
  15. void ShowList(Args... args)// 传入三个参数
  16. {
  17. _ShowList(args...);// 传入三个参数
  18. }
  19. int main()
  20. {
  21. ShowList();
  22. ShowList(1);
  23. ShowList(1, 'A');
  24. ShowList(1, 'A', std::string("sort"));
  25. return 0;
  26. }

我们来看一下运行结果:

我们来看下实例化为三个参数的参数包编译器推演生成的过程。

  1. // 实例化以后,推演生成的过程
  2. void ShowList(int val1, char ch, std::string s)
  3. {
  4. _ShowList(val1, ch, s);
  5. }
  6. void _ShowList(const int& val, char ch, std::string s)
  7. {
  8. cout << val << " ";
  9. _ShowList(ch, s);
  10. }
  11. void _ShowList(const char& val, std::string s)
  12. {
  13. cout << val << " ";
  14. _ShowList(s);
  15. }
  16. void _ShowList(const std::string& val)
  17. {
  18. cout << val << " ";
  19. _ShowList();
  20. }
  21. // 编译时递归返回条件
  22. void _ShowList()
  23. {
  24. cout << endl;
  25. }

 模板参数的推演还有另外一种写法:逗号表达式展开参数包

  1. template <class T>
  2. int PrintArg(T t)
  3. {
  4. cout << t << " ";
  5. return 0;
  6. }
  7. //展开函数
  8. template <class ...Args>
  9. void ShowList(Args... args)
  10. {
  11. int arr[] = { (PrintArg(args), 0)... };
  12. cout << endl;
  13. }

这种展开参数包的方式,不需要通过递归终止函数,是直接在expand函数体中展开的, printarg 不是一个递归终止函数,只是一个处理参数包中每一个参数的函数。这种就地展开参数包的方式 实现的关键是逗号表达式。我们知道逗号表达式会按顺序执行逗号前面的表达式。 expand函数中的逗号表达式:(printarg(args), 0),也是按照这个执行顺序,先执行 printarg(args),再得到逗号表达式的结果0。同时还用到了C++11的另外一个特性——初始化列表,通过初始化列表来初始化一个变长数组, {(printarg(args), 0)...}将会展开成((printarg(arg1),0), (printarg(arg2),0), (printarg(arg3),0), etc... ),最终会创建一个元素值都为0的数组int arr[sizeof...(Args)]。由于是逗号表达式,在创建数组的过程中会先执行逗号表达式前面的部分printarg(args) 打印出参数,也就是说在构造int数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在 数组构造的过程展开参数包。

STL容器中的empalce相关接口函数:

  1. template <class... Args>
  2. void emplace_back (Args&&... args);

首先我们看到的emplace系列的接口,支持模板的可变参数,并且万能引用。那么相对insert和 emplace系列接口的优势到底在哪里呢?

emplace_back支持可变参数,拿到构建pair对象的参数后自己去创建对象 ,那么在这里我们可以看到除了用法上,和push_back没什么太大的区别。

我们会发现其实差别也不到,emplace_back是直接构造了,push_back // 是先构造,再移动构造,其实也还好。

但是对我们的浅拷贝的效率是很明显的,因为浅拷贝的类是没有移动构造的,那么此时就构造 + 拷贝构造,此时消耗还是比较大的。

  1. class Date
  2. {
  3. public:
  4. Date(int year, int month, int day)
  5. :_year(year)
  6. , _month(month)
  7. , _day(day)
  8. {
  9. cout << "Date(int year, int month, int day) --- 构造" << endl;
  10. }
  11. Date(const Date& d)
  12. :_year(d._year)
  13. , _month(d._month)
  14. , _day(d._day)
  15. {
  16. cout << "Date(const Date& d) --- 拷贝构造" << endl;
  17. }
  18. private:
  19. int _year = 1;
  20. int _month = 1;
  21. int _day = 1;
  22. };
  23. int main()
  24. {
  25. std::list<Date> lt1;
  26. lt1.push_back({ 2024,3,30 });
  27. return 0;
  28. }

我们来看一下运行结果:

使用emplace呢?

我们再来看一下有名对象和匿名对象的区别

所以在使用emplace的时候,我们尽量不要传对象,否则效率上没有提升。我们可以直接传入参数包进行匹配,并不用逐步解析出参数包才能使用。

  1. class Date
  2. {
  3. public:
  4. Date(int year = 1, int month = 1, int day = 1)
  5. :_year(year)
  6. , _month(month)
  7. , _day(day)
  8. {
  9. cout << "Date(int year, int month, int day) --- 构造" << endl;
  10. }
  11. Date(const Date& d)
  12. :_year(d._year)
  13. , _month(d._month)
  14. , _day(d._day)
  15. {
  16. cout << "Date(const Date& d) --- 拷贝构造" << endl;
  17. }
  18. private:
  19. int _year = 1;
  20. int _month = 1;
  21. int _day = 1;
  22. };
  23. template<class ...Args>
  24. void CreateDate(Args&&... args)
  25. {
  26. Date d(args...);
  27. }
  28. int main()
  29. {
  30. CreateDate(1, 1, 1);
  31. CreateDate(2, 2);
  32. CreateDate(3);
  33. CreateDate();
  34. return 0;
  35. }

运行结果:

现在我们来模拟实现一下emplace_back

  1. template<class ...Args>
  2. ListNode(Args&&... args)
  3. : _next(nullptr)
  4. , _prev(nullptr)
  5. , _data(forward<Args>(args)...)
  6. // _data可能是Date、string、vector
  7. // 对于Date是拷贝构造
  8. // 对于string是移动拷贝
  9. {}
  10. template<class ...Args>
  11. void emplace_back(Args&&... args)
  12. {
  13. // 参数包不进行解析,而是传入插入函数
  14. // 尝试直接去进行匹配构造
  15. emplace(end(), forward<Args>(args)...);
  16. }
  17. template<class ...Args>
  18. iterator emplace(iterator pos, Args&&... args)
  19. {
  20. Node* cur = pos._node;
  21. Node* prev = cur->_prev;
  22. Node* newnode = new Node(forward<Args>(args)...);
  23. prev->_next = newnode;
  24. newnode->_prev = prev;
  25. newnode->_next = cur;
  26. cur->_prev = newnode;
  27. return newnode;
  28. }

我们看一下结果:

10 lambda表达式

10.1 C++98中的一个例子

在C++98中,如果想要对一个数据集合中的元素进行排序,可以使用std::sort方法。

  1. #include <algorithm>
  2. #include <functional>
  3. int main()
  4. {
  5. int array[] = { 4,1,8,5,3,7,0,9,2,6 };
  6. // 默认按照小于比较,排出来结果是升序
  7. std::sort(array, array + sizeof(array) / sizeof(array[0]));
  8. // 如果需要降序,需要改变元素的比较规则
  9. std::sort(array, array + sizeof(array) / sizeof(array[0]), greater<int>());
  10. return 0;
  11. }

如果待排序元素为自定义类型,需要用户定义排序时的比较规则:

  1. struct Goods
  2. {
  3. string _name; // 名字
  4. double _price; // 价格
  5. int _evaluate; // 评价
  6. Goods(const char* str, double price, int evaluate)
  7. :_name(str)
  8. , _price(price)
  9. , _evaluate(evaluate)
  10. {}
  11. };
  12. struct ComparePriceLess
  13. {
  14. bool operator()(const Goods& gl, const Goods& gr)
  15. {
  16. return gl._price < gr._price;
  17. }
  18. };
  19. struct ComparePriceGreater
  20. {
  21. bool operator()(const Goods& gl, const Goods& gr)
  22. {
  23. return gl._price > gr._price;
  24. }
  25. };
  26. int main()
  27. {
  28. vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2,
  29. 3 }, { "菠萝", 1.5, 4 } };
  30. sort(v.begin(), v.end(), ComparePriceLess());
  31. sort(v.begin(), v.end(), ComparePriceGreater());
  32. }

随着C++语法的发展,人们开始觉得上面的写法太复杂了,每次为了实现一个algorithm算法, 都要重新去写一个类,如果每次比较的逻辑不一样,还要去实现多个类,特别是相同类的命名, 这些都给编程者带来了极大的不便。因此,在C++11语法中出现了Lambda表达式。

10.2 lambda表达式语法

lambda表达式书写格式:[capture-list] (parameters) mutable -> return-type { statement }

10.3 lambda表达式

  1. int main()
  2. {
  3. // 局部的匿名函数对象
  4. //auto add = [](int a, int b)->int {return a + b; };
  5. auto add = [](int a, int b) {return a + b; };
  6. cout << add(1, 2) << endl;
  7. auto swap1 = [](int& a, int& b)->void {
  8. int tmp = a;
  9. a = b;
  10. b = tmp;
  11. };
  12. int x = 1, y = 2;
  13. swap1(x, y);
  14. // 参数列表可以省略
  15. // 返回值类型可以省略
  16. auto func1 = [] {
  17. cout << "hello world" << endl;
  18. };
  19. func1();
  20. return 0;
  21. }

注意: 在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为 空。因此C++11中最简单的lambda函数为:[]{}; 该lambda函数不能做任何事情。通过上述例子可以看出,lambda表达式实际上可以理解为无名函数,该函数无法直接调 用,如果想要直接调用,可借助auto将其赋值给一个变量。我们现在就可以用一下lambda进行排序Goods。

  1. int main()
  2. {
  3. vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2,
  4. 3 }, { "菠萝", 1.5, 4 } };
  5. sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
  6. return g1._price < g2._price; });
  7. sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
  8. return g1._price > g2._price; });
  9. sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
  10. return g1._evaluate < g2._evaluate; });
  11. sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
  12. return g1._evaluate > g2._evaluate; });
  13. }

上述代码就是使用C++11中的lambda表达式来解决,可以看出lambda表达式实际是一个匿名函 数。

我们先来看一下值传递方式的捕捉

此时就要使用我们的传引用捕捉

我们再来看一下值传递方式捕获所有

那怎么能少了传引用捕捉

还可以进行混合捕捉来玩

10.4 函数对象与lambda表达式

函数对象,又称为仿函数,即可以想函数一样使用的对象,就是在类中重载了operator()运算符的 类对象。

  1. class Rate
  2. {
  3. public:
  4. Rate(double rate) : _rate(rate)
  5. {}
  6. double operator()(double money, int year)
  7. {
  8. return money * _rate * year;
  9. }
  10. private:
  11. double _rate;
  12. };
  13. int main()
  14. {
  15. // 函数对象
  16. double rate = 0.49;
  17. Rate r1(rate);
  18. r1(10000, 2);
  19. // lambda
  20. auto r2 = [=](double monty, int year)->double {return monty * rate * year;
  21. };
  22. r2(10000, 2);
  23. return 0;
  24. }

从使用方式上来看,函数对象与lambda表达式完全一样。

函数对象将rate作为其成员变量,在定义对象时给出初始值即可,lambda表达式通过捕获列表可 以直接将该变量捕获到。

实际在底层编译器对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的,即:如 果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载了operator()。

  1. void (*PF)();
  2. int main()
  3. {
  4. auto f1 = [] {cout << "hello world" << endl; };
  5. auto f2 = [] {cout << "hello world" << endl; };
  6. // 此处先不解释原因,等lambda表达式底层实现原理看完后,大家就清楚了
  7. //f1 = f2; // 编译失败--->提示找不到operator=()
  8. // 允许使用一个lambda表达式拷贝构造一个新的副本
  9. auto f3(f2);
  10. f3();
  11. // 可以将lambda表达式赋值给相同类型的函数指针
  12. PF = f2;
  13. PF();
  14. return 0;
  15. }

上面f2不能赋值给f1的原因就是类型不一样,即使实现一样,f1和f2属于两个不同的类,底层各自是一个类的仿函数。

这里就可以将我们学的decltype和这里进行使用

  1. #include<queue>
  2. int main()
  3. {
  4. // lambda没有类型
  5. auto DateLess = [](const Date* p1, const Date* p2)
  6. {
  7. return *p1 < *p2;//需要重载operator<()
  8. };
  9. cout << typeid(DateLess).name() << endl;
  10. // 可以拷贝构造
  11. auto copy(DateLess);
  12. // lambda对象禁掉默认构造
  13. // decltype(DateLess) xx;
  14. //priority_queue<Date*, vector<Date*>, decltype(DateLess)> p1; error
  15. // 但是支持拷贝构造
  16. priority_queue<Date*, vector<Date*>, decltype(DateLess)> p1(DateLess);
  17. return 0;
  18. }

11 包装器

function包装器

function包装器 也叫作适配器。C++中的function本质是一个类模板,也是一个包装器。 那么我们来看看,我们为什么需要function呢?ret = func(x); // 上面func可能是什么呢?那么func可能是函数名?函数指针?函数对象(仿函数对象)?也有可能 是lamber表达式对象?所以这些都是可调用的类型!如此丰富的类型,可能会导致模板的效率低下! 为什么呢?我们继续往下看

  1. // 函数模板
  2. template<class F, class T>
  3. T useF(F f, T x)
  4. {
  5. static int count = 0;
  6. cout << "count:" << ++count << endl;
  7. cout << "count:" << &count << endl;
  8. return f(x);
  9. }
  10. double f(double i)
  11. {
  12. return i / 2;
  13. }
  14. struct Functor
  15. {
  16. double operator()(double d)
  17. {
  18. return d / 3;
  19. }
  20. };
  21. int main()
  22. {
  23. // 函数名
  24. cout << useF(f, 11.11) << endl;
  25. // 函数对象
  26. cout << useF(Functor(), 11.11) << endl;
  27. // lamber表达式
  28. cout << useF([](double d)->double { return d / 4; }, 11.11) << endl;
  29. return 0;
  30. }

我们来看一下运行结果:

通过上面的程序验证,我们会发现useF函数模板实例化了三份。 包装器可以很好的解决上面的问题

  1. std::function在头文件<functional>
  2. // 类模板原型如下
  3. template <class T> function; // undefined
  4. template <class Ret, class... Args>
  5. class function<Ret(Args...)>;
  6. 模板参数说明:
  7. Ret : 被调用函数的返回类型
  8. Args…:被调用函数的形参
  1. #include<functional>
  2. int main()
  3. {
  4. // 函数指针
  5. function<double(double)> fc1 = f;
  6. fc1(11.11);
  7. cout << useF(fc1, 11.11) << endl;
  8. // 函数对象
  9. function<double(double)> fc2 = Functor();
  10. fc2(11.11);
  11. cout << useF(fc2, 11.11) << endl;
  12. // lambda表达式
  13. function<double(double)> fc3 = [](double d)->double { return d / 4; };
  14. fc3(11.11);
  15. cout << useF(fc3, 11.11) << endl;
  16. return 0;
  17. }

此时就只实例化了一份

有了包装器,就可以解决模板的效率低下,实例化多份的问题。包装器的其他一些场景:. - 力扣(LeetCode)

我们再来看一下包装成员函数的情况。

  1. int f(int a, int b)
  2. {
  3. return a + b;
  4. }
  5. class Plus
  6. {
  7. public:
  8. static int plusi(int a, int b)
  9. {
  10. return a + b;
  11. }
  12. double plusd(double a, double b)
  13. {
  14. return a + b;
  15. }
  16. };
  17. int main()
  18. {
  19. // 普通函数
  20. function<int(int, int)> fc1 = f;
  21. cout << fc1(1, 1) << endl;
  22. // 静态成员函数,&可加可不加
  23. function<int(int, int)> fc2 = &Plus::plusi;
  24. cout << fc2(1, 1) << endl;
  25. // 非静态成员函数
  26. // 非静态成员函数需要对象的指针或者对象去进行调用
  27. /*Plus plus;
  28. function<double(Plus*, double, double)> fc3 = &Plus::plusd;
  29. cout << fc3(&plus, 1, 1) << endl;*/
  30. // 非静态成员函数还有一个隐含的参数 -> this指针
  31. function<double(Plus, double, double)> fc3 = &Plus::plusd;
  32. cout << fc3(Plus(), 1, 1) << endl;
  33. return 0;
  34. }

bind

std::bind函数定义在头文件中,是一个函数模板,它就像一个函数包装器(适配器),接受一个可 调用对象(callable object),生成一个新的可调用对象来“适应”原对象的参数列表。一般而 言,我们用它可以把一个原本接收N个参数的函数fn,通过绑定一些参数,返回一个接收M个(M 可以大于N,但这么做没什么意义)参数的新函数。同时,使用std::bind函数还可以实现参数顺 序调整等操作。

可以将bind函数看作是一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对 象来“适应”原对象的参数列表。

调用bind的一般形式:auto newCallable = bind(callable,arg_list);

其中,newCallable本身是一个可调用对象,arg_list是一个逗号分隔的参数列表,对应给定的 callable的参数。当我们调用newCallable时,newCallable会调用callable,并传给它arg_list中 的参数。

arg_list中的参数可能包含形如_n的名字,其中n是一个整数,这些参数是“占位符”,表示 newCallable的参数,它们占据了传递给newCallable的参数的“位置”。数值n表示生成的可调用对 象中参数的位置:_1为newCallable的第一个参数,_2为第二个参数,以此类推。

  1. // 使用举例
  2. #include <functional>
  3. int Plus(int a, int b)
  4. {
  5. return a + b;
  6. }
  7. class Sub
  8. {
  9. public:
  10. int sub(int a, int b)
  11. {
  12. return a - b;
  13. }
  14. };
  15. int main()
  16. {
  17. // 调整参数顺序,了解一下,意义不大
  18. int x = 10, y = 20;
  19. cout << Sub(x, y) << endl;
  20. auto f1 = bind(Sub, placeholders::_2, placeholders::_1);
  21. cout << f1(x, y) << endl;
  22. function<double(Plus, double, double)> fc3 = &Plus::plusd;
  23. cout << fc3(Plus(), 1, 1) << endl;
  24. // 调整参数的个数
  25. // 某些参数绑死
  26. function<double(double, double)> fc4 = bind(&Plus::plusd, Plus(), placeholders::_1, placeholders::_2);
  27. cout << fc4(2, 3) << endl;
  28. function<double(double)> fc5 = bind(&Plus::plusd, Plus(), placeholders::_1, 20);
  29. cout << fc5(2) << endl;
  30. return 0;
  31. }

总结:

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/人工智能uu/article/detail/765361
推荐阅读
相关标签
  

闽ICP备14008679号