• C++ c++11(上)


    目录

    1.C++11简介

    2. 统一的列表初始化

    2.1 {}初始化

    2.2 std::initializer_list

    3. 声明

    3.1 auto

    3.2 decltype

    3.3 nullptr

    4.STL中的一些变化

    5.右值引用和移动语义

    5.1 左值引用和右值引用

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

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

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

    5.5 完美转发


    1.C++11简介

    百度:C++11 - cppreference.com

    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 增加的语法特性非常篇幅非常多,我们这里没办法一 一讲解,主要讲解实际中比较实用的语法。

    2. 统一的列表初始化

    2.1 {}初始化

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

    1. struct Point
    2. {
    3. int _x;
    4. int _y;
    5. };
    6. int main()
    7. {
    8. int array1[] = { 1, 2, 3, 4, 5 };
    9. int array2[5] = { 0 };
    10. Point p = { 1, 2 };
    11. return 0;
    12. }
    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 array1[]{ 1, 2, 3, 4, 5 };
    11. int array2[5]{ 0 };
    12. Point p{ 1, 2 };
    13. // C++11中列表初始化也可以适用于new表达式中
    14. int* pa = new int[4]{ 0 };
    15. return 0;
    16. }

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

    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. private:
    12. int _year;
    13. int _month;
    14. int _day;
    15. };
    16. int main()
    17. {
    18. Date d1(2022, 1, 1); // old style
    19. // C++11支持的列表初始化,这里会调用构造函数初始化
    20. Date d2{ 2022, 1, 2 };
    21. Date d3 = { 2022, 1, 3 };
    22. return 0;
    23. }

    2.2 std::initializer_list

    std::initializer_list的介绍文档:initializer_list - C++ Reference

    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. }

     std::initializer_list使用场景:

    std::initializer_list一般是作为构造函数的参数,C++11STL中的不少容器就增加std::initializer_list作为参数的构造函数,这样初始化容器对象就更方便了。也可以作为operator= 的参数,这样就以用大括号赋值。

    list::list - C++ Reference

    vector::vector - C++ Reference

    map::map - C++ Reference

    vector::operator= - C++ Reference

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

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

    1. namespace bit
    2. {
    3. template<class T>
    4. class vector {
    5. public:
    6. typedef T* iterator;
    7. vector(initializer_list l)
    8. {
    9. _start = new T[l.size()];
    10. _finish = _start + l.size();
    11. _endofstorage = _start + l.size();
    12. iterator vit = _start;
    13. typename initializer_list::iterator lit = l.begin();
    14. while (lit != l.end())
    15. {
    16. *vit++ = *lit++;
    17. }
    18. //for (auto e : l)
    19. //   *vit++ = e;
    20. }
    21. vector& operator=(initializer_list l) {
    22. vector tmp(l);
    23. std::swap(_start, tmp._start);
    24. std::swap(_finish, tmp._finish);
    25. std::swap(_endofstorage, tmp._endofstorage);
    26. return *this;
    27. }
    28. private:
    29. iterator _start;
    30. iterator _finish;
    31. iterator _endofstorage;
    32. };
    33. }

    3. 声明

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

    3.1 auto

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

    1. int main()
    2. {
    3. int i = 10;
    4. auto p = &i;
    5. auto pf = strcpy;
    6. cout << typeid(p).name() << endl;
    7. cout << typeid(pf).name() << endl;
    8. map dict = { {"sort", "排序"}, {"insert", "插入"} };
    9. //map::iterator it = dict.begin();
    10. auto it = dict.begin();
    11. return 0;
    12. }

    3.2 decltype

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

    1. // decltype的一些使用使用场景
    2. template<class T1, class T2>
    3. void F(T1 t1, T2 t2) {
    4. decltype(t1 * t2) ret;
    5. cout << typeid(ret).name() << endl;
    6. }
    7. int main()
    8. {
    9. const int x = 1;
    10. double y = 2.2;
    11. decltype(x * y) ret; // ret的类型是double
    12. decltype(&x) p;      // p的类型是int*
    13. cout << typeid(ret).name() << endl;
    14. cout << typeid(p).name() << endl; F(1, 'a');
    15. return 0;
    16. }

    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.STL中的一些变化

     新容器

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

     容器中的一些新方法

    如果我们再细细去看会发现基本每个容器中都增加了一些C++11的方法,但是其实很多都是用得比较少的。比如提供了cbegin和cend方法返回const迭代器等等,但是实际意义不大,因为begin和end也是可以返回const迭代器的,这些都是属于锦上添花的操作。实际上C++11更新后,容器中增加的新方法最后用的插入接口函数的右值引用版本:

    5.右值引用和移动语义

    5.1 左值引用和右值引用

    左值是一个表示数据的表达式 ( 如变量名或解引用的指针 ) 我们可以获取它的地址 + 可以对它赋
    值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边 。定义时 const 修饰符后的左
    值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。
    1. int main()
    2. {
    3. // 以下的p、b、c、*p都是左值
    4. int* p = new int(0);
    5. int b = 1;
    6. const int c = 2;
    7. // 以下几个是对上面左值的左值引用
    8. int*& rp = p;
    9. int& rb = b;
    10. const int& rc = c;
    11. int& pvalue = *p;
    12. return 0;
    13. }
    右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值 ( 这个不能是左值引
    用返回 ) 等等, 右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能
    取地址 。右值引用就是对右值的引用,给右值取别名。
    1. int main()
    2. {
    3. double x = 1.1, y = 2.2;
    4. // 以下几个都是常见的右值
    5. 10; x + y;
    6. fmin(x, y);
    7. // 以下几个都是对右值的右值引用
    8. int&& rr1 = 10;
    9. double&& rr2 = x + y;
    10. double&& rr3 = fmin(x, y);
    11. // 这里编译会报错:error C2106: “=”: 左操作数必须为左值
    12. 10 = 1; x + y = 1;
    13. fmin(x, y) = 1;
    14. return 0;
    15. }
    需要注意的是右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可
    以取到该位置的地址,也就是说例如:不能取字面量 10 的地址,但是 rr1 引用后,可以对 rr1 取地
    址,也可以修改 rr1 。如果不想 rr1 被修改,可以用 const int&& rr1 去引用,是不是感觉很神奇,
    这个了解一下实际中右值引用的使用场景并不在于此,这个特性也不重要。
    1. int main()
    2. {
    3. double x = 1.1, y = 2.2;
    4. int&& rr1 = 10;
    5. const double&& rr2 = x + y;
    6. rr1 = 20;
    7. rr2 = 5.5;  // 报错
    8. return 0;
    9. }

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

    左值引用总结:

    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. }

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

    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. // 移动构造
    47. string(string&& s)
    48. :_str(nullptr)
    49. , _size(0)
    50. , _capacity(0)
    51. {
    52. cout << "string(string&& s) -- 移动语义" << endl;
    53. swap(s);
    54. }
    55. // 移动赋值
    56. string& operator=(string&& s)
    57. {
    58. cout << "string& operator=(string&& s) -- 移动语义" << endl;
    59. swap(s);
    60. return *this;
    61. }
    62. ~string()
    63. {
    64. delete[] _str;
    65. _str = nullptr;
    66. }
    67. char& operator[](size_t pos)
    68. {
    69. assert(pos < _size);
    70. return _str[pos];
    71. }
    72. void reserve(size_t n)
    73. {
    74. if (n > _capacity)
    75. {
    76. char* tmp = new char[n + 1];
    77. strcpy(tmp, _str);
    78. delete[] _str;
    79. _str = tmp;
    80. _capacity = n;
    81. }
    82. }
    83. void push_back(char ch)
    84. {
    85. if (_size >= _capacity)
    86. {
    87. size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
    88. reserve(newcapacity);
    89. }
    90. _str[_size] = ch;
    91. ++_size;
    92. _str[_size] = '\0';
    93. }
    94. //string operator+=(char ch)
    95. string& operator+=(char ch)
    96. {
    97. push_back(ch);
    98. return *this;
    99. }
    100. const char* c_str() const
    101. {
    102. return _str;
    103. }
    104. private:
    105. char* _str;
    106. size_t _size;
    107. size_t _capacity; // 不包含最后做标识的\0
    108. };
    109. }
    左值引用的使用场景:做参数和做返回值都可以提高效率。
    1. void func1(bit::string s)
    2. {}
    3. void func2(const bit::string& s)
    4. {}
    5. int main()
    6. {
    7. bit::string s1("hello world");
    8. // func1和func2的调用我们可以看到左值引用做参数减少了拷贝,提高效率的使用场景和价值
    9. func1(s1);
    10. func2(s1);
    11. // string operator+=(char ch) 传值返回存在深拷贝
    12. // string& operator+=(char ch) 传左值引用没有拷贝提高了效率
    13. s1 += '!';
    14. return 0;
    15. }
    左值引用的短板:

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

    1. namespace bit
    2. {
    3. bit::string to_string(int value)
    4. {
    5. bool flag = true;
    6. if (value < 0)
    7. {
    8. flag = false;
    9. value = 0 - value;
    10. }
    11. bit::string str;
    12. while (value > 0)
    13. {
    14. int x = value % 10;
    15. value /= 10;
    16. str += ('0' + x);
    17. }
    18. if (flag == false)
    19. {
    20. str += '-';
    21. }
    22. std::reverse(str.begin(), str.end());
    23. return str;
    24. }
    25. }
    26. int main()
    27. {
    28. // 在bit::string to_string(int value)函数中可以看到,这里
    29. // 只能使用传值返回,传值返回会导致至少1次拷贝构造(如果是一些旧一点的编译器可能是两次拷
    30. 贝构造)。
    31. bit::string ret1 = bit::to_string(1234);
    32. bit::string ret2 = bit::to_string(-1234);
    33. return 0;
    34. }

    右值引用和移动语义解决上述问题:

    bit::string 中增加移动构造, 移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不 用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己
    1. // 移动构造
    2. string(string&& s)
    3. :_str(nullptr)
    4. , _size(0)
    5. , _capacity(0) {
    6. cout << "string(string&& s) -- 移动语义" << endl;
    7. swap(s);
    8. }
    9. int main()
    10. {
    11. bit::string ret2 = bit::to_string(-1234);
    12. return 0;
    13. }

     再运行上面bit::to_string的两个调用,我们会发现,这里没有调用深拷贝的拷贝构造,而是调用了移动构造,移动构造中没有新开空间,拷贝数据,所以效率提高了。

     

    不仅仅有移动构造,还有移动赋值:
    bit::string 类中增加移动赋值函数,再去调用 bit::to_string(1234) ,不过这次是将
    bit::to_string(1234) 返回的右值对象赋值给 ret1 对象,这时调用的是移动构造。
    1. // 移动赋值
    2. string& operator=(string&& s) {
    3. cout << "string& operator=(string&& s) -- 移动语义" << endl;
    4. swap(s);
    5. return *this;
    6. }
    7. int main()
    8. {
    9. bit::string ret1;
    10. ret1 = bit::to_string(1234);
    11. return 0;
    12. }
    13. // 运行结果:
    14. // string(string&& s) -- 移动语义
    15. // string& operator=(string&& s) -- 移动语义

     这里运行后,我们看到调用了一次移动构造和一次移动赋值。因为如果是用一个已经存在的对象接收,编译器就没办法优化了。bit::to_string函数中会先用str生成构造生成一个临时对象,但是我们可以看到,编译器很聪明的在这里把str识别成了右值,调用了移动构造。然后在把这个临时对象做为bit::to_string函数调用的返回值赋值给ret1,这里调用的移动赋值。

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

    string::string - C++ Reference

    vector::vector - C++ Reference

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

    按照语法,右值引用只能引用右值,但右值引用一定不能引用左值吗?因为:有些场景下,可能
    真的需要用右值去引用左值实现移动语义。当需要用右值引用引用一个左值时,可以通过 move 函数将左值转化为右值 C++11 中, std::move() 函数 位于 头文件中,该函数名字具有迷惑性,它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,然后实现移动语义。
    1. template<class _Ty>
    2. inline typename remove_reference<_Ty>::type&& move(_Ty&& _Arg) _NOEXCEPT
    3. {
    4. // forward _Arg as movable
    5. return ((typename remove_reference<_Ty>::type&&)_Arg);
    6. }
    7. int main()
    8. {
    9. bit::string s1("hello world");
    10. // 这里s1是左值,调用的是拷贝构造
    11. bit::string s2(s1);
    12. // 这里我们把s1 move处理以后, 会被当成右值,调用移动构造
    13. // 但是这里要注意,一般是不要这样用的,因为我们会发现s1的
    14. // 资源被转移给了s3,s1被置空了。
    15. bit::string s3(std::move(s1));
    16. return 0;
    17. }
    STL 容器插入接口函数也增加了右值引用版本:
    1. void push_back(value_type&& val);
    2. int main()
    3. {
    4. list lt;
    5. bit::string s1("1111");
    6. // 这里调用的是拷贝构造
    7. lt.push_back(s1);
    8. // 下面调用都是移动构造
    9. lt.push_back("2222");
    10. lt.push_back(std::move(s1));
    11. return 0;
    12. }
    13. 运行结果:
    14. // string(const string& s) -- 深拷贝
    15. // string(string&& s) -- 移动语义
    16. // string(string&& s) -- 移动语义

    5.5 完美转发

    模板中的&& 万能引用

    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. Fun(t);
    12. }
    13. int main()
    14. {
    15. PerfectForward(10);           // 右值
    16. int a;
    17. PerfectForward(a);            // 左值
    18. PerfectForward(std::move(a)); // 右值
    19. const int b = 8;
    20. PerfectForward(b);      // const 左值
    21. PerfectForward(std::move(b)); // const 右值
    22. return 0;
    23. }
    std::forward 完美转发在传参的过程中保留对象原生类型属性
    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. // std::forward(t)在传参的过程中保持了t的原生类型属性。
    6. template<typename T>
    7. void PerfectForward(T&& t) {
    8. Fun(std::forward(t));
    9. }
    10. int main()
    11. {
    12. PerfectForward(10);           // 右值
    13. int a;
    14. PerfectForward(a);            // 左值
    15. PerfectForward(std::move(a)); // 右值
    16. const int b = 8;
    17. PerfectForward(b);      // const 左值
    18. PerfectForward(std::move(b)); // const 右值
    19. return 0;
    20. }
    完美转发实际中的使用场景:
    1. template<class T>
    2. struct ListNode
    3. {
    4. ListNode* _next = nullptr;
    5. ListNode* _prev = nullptr;
    6. T _data;
    7. };
    8. template<class T>
    9. class List
    10. {
    11. typedef ListNode Node;
    12. public:
    13. List()
    14. {
    15. _head = new Node;
    16. _head->_next = _head;
    17. _head->_prev = _head;
    18. }
    19. void PushBack(T&& x)
    20. {
    21. //Insert(_head, x);
    22. Insert(_head, std::forward(x));
    23. }
    24. void PushFront(T&& x)
    25. {
    26. //Insert(_head->_next, x);
    27. Insert(_head->_next, std::forward(x));
    28. }
    29. void Insert(Node* pos, T&& x)
    30. {
    31. Node* prev = pos->_prev;
    32. Node* newnode = new Node;
    33. newnode->_data = std::forward(x); // 关键位置
    34. // prev newnode pos
    35. prev->_next = newnode;
    36. newnode->_prev = prev;
    37. newnode->_next = pos;
    38. pos->_prev = newnode;
    39. }
    40. void Insert(Node* pos, const T& x)
    41. {
    42. Node* prev = pos->_prev;
    43. Node* newnode = new Node;
    44. newnode->_data = x; // 关键位置
    45. // prev newnode pos
    46. prev->_next = newnode;
    47. newnode->_prev = prev;
    48. newnode->_next = pos;
    49. pos->_prev = newnode;
    50. }
    51. private:
    52. Node* _head;
    53. };
    54. int main()
    55. {
    56. List lt;
    57. lt.PushBack("1111");
    58. lt.PushFront("2222");
    59. return 0;
    60. }

  • 相关阅读:
    字节跳动大裁员,测试工程师差点遭团灭:大厂招人背后的套路,有多可怕?
    蛋白质相互作用
    一篇文章讲明白Java中的线程池(含源码分析)
    Linux系统学习
    【数据结构与算法】十大经典排序算法
    Pyton conway生存游戏--代码参考
    MySQL8.0 安装卸载validate_password插件 和 validate_password组件
    SQLServer下载与安装
    (游戏:三个数的加法)编写程序,随机产生三个一位整数,并提示用户输入这三个整数的和,判断用户输入的和是否正确。
    探索性能驱动的模拟 IC 布局的机器学习方法
  • 原文地址:https://blog.csdn.net/m0_61548909/article/details/126556222