ToB企服应用市场:ToB评测及商务社交产业平台

标题: C++11相较于C++98的新特性先容:列表初始化,右值引用与移动语义 [打印本页]

作者: 海哥    时间: 8 小时前
标题: C++11相较于C++98的新特性先容:列表初始化,右值引用与移动语义
一,列表初始化

1.1C++98中传统的{}

C++98中一样平常数组和结构体可以使用{}进行初始化:
  1. struct Date
  2. {
  3.         int _year;
  4.         int _month;
  5.         int _day;
  6. };
  7. int main()
  8. {
  9.         int a[] = { 1,2,3,4,5 };
  10.         Date _date = { 2025,2,27 };
  11.         return 0;
  12. }
复制代码
 1.2C++11中的{}

  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;
  20.         int _month;
  21.         int _day;
  22. };
  23. int main()
  24. {
  25.         int a[] = { 1,2,3,4,5 };
  26.         Date _date = { 2025,2,27 };
  27.         const Date& d2 = { 2027,2,29 };//右边{2027,2,29}是一个临时对象,需要加上const
  28.         //也可以不加括号
  29.         Date d3{ 2028,2,27 };
  30.         //非{}初始化必须要加上等号
  31.         Date d4 2025;//编译器报错
  32.         Date d5 = 2025;
  33.         return 0;
  34. }
复制代码
1.3C++11中的std::initializer_list 

        上面的{}初始化已然很方便,但是如果说我们要初始化像vector如许的对像时,他的参数个数是会发生变化的,显然仅仅只有上面的{}远远无法满足这种需求。所以c++中就提供了std::initializer_list的类, auto il = { 10, 20, 30 }; // thetype of il is an initializer_list ,这个类的本质是底层开一个数组,将数据拷贝过来,std::initializer_list内部有两个指针分别指向数组的开始和竣事。

这是他的文档:initializer_list - C++ Reference,std::initializer_list支持迭代器遍历。 
区别是{}还是initializer_list的方法也很简朴,{}传入的值个数是固定的,由须要初始化的对象类型决定,里面所有数据的类型可能不同。后者可写入的值不固定,但类型一定雷同。
  1. map<int, string> m{ {1,string("hallo")} };
  2. vector<string> v{ "a","b","c" };
复制代码
二,右值引用与移动语义 

我们之前学习的是C++98中的引用,比如原本有一个int类型的对象x,那么int& b = x,此时b其实就是x的别名。C++11之后,我们之前学过的这种引用方式被叫做左值引用。须要注意的是,无论是左值引用还是右值引用,本质上都是取别名。 
2.1左值与右值 

左值是一个表示数据的表达式(如变量名或解引用的指针),一样平常是有持久状态,存储在内存中,我
们可以获取它的地址
,左值可以出现赋值符号的左边,也可以出现在赋值符号右边。界说时const
修饰符后的左值,不能给他赋值,但是可以取它的地址。
右值也是一个表示数据的表达式,要么是字面值常量、要么是表达式求值过程中创建的临时对象
等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。 
值得一提的是,左值的英文简写为lvalue,右值的英文简写为rvalue。传统认为它们分别是left
value、right value 的缩写。现代C++中,lvalue 被解释为loactor value的缩写,可意为存储在内
存中、有明白存储地址可以取地址的对象,而 rvalue 被解释为 read value,指的是那些可以提供
数据值,但是不可以寻址,比方:临时变量,字面量常量,存储于寄存器中的变量等,也就是说左
值和右值的核心区别就是能否取地址。

  1. //常见的左值比如:
  2. int p;
  3. const int p1;
  4. int a[10];
  5. char c;
  6. a[0] = 1;
  7. string s("111111111");
  8. s[0] = 'b';//....
  9. //它们都有一个共同的特性:可以取地址
  10. //常见的右值比如:
  11. 10;
  12. s[0] + a[0];
  13. string("111111111111111");
  14. min(s[0],a[0]);
  15. //它们均是临时对象,无法取地址
复制代码
2.2左值引用与右值引用 

  1. Type& x = y;//左值引用
  2. Type&& x = y;//右值引用
复制代码
同样,右值引用其实就是给右值取别名。
1.对于左值引用,不可以直接引用右值,引用右值时须要加上const。
  1. int& x = 10;//error
  2. const int& x = 10;//true
复制代码
2.对于右值引用,固然也不可以直接引用左值,但是可以引用move移动之后的左值。
  1. int x = 10;
  2. int&& y = x;//error
  3. int&& y = std::move(x);//true
  4. //move可以将左值强制类型转化为右值,是库里面的一个函数模板,本质内部是进行强制类型转换,当然他还涉
  5. //及一些引用折叠的知识,我们后面会详细介绍。
复制代码
3.须要注意的是变量表达式都是左值属性,也就意味着一个右值被右值引用绑定后,右值引用变量变量表达式的属性是左值。
  1. int&& y = 10;//y此时是一个左值
复制代码
4.语法层面看,左值引用和右值引用都是取别名,不开空间。从汇编底层的角度看无论左值引用还是右值引用,底层都是用指针实现的,没有本质区别。
2.3引用改变生命周期 

右值引用和带有const的左值引用都可以延伸临时变量的声明周期,但后者无法被修改。
  1. std::string s1 = "1111111111";
  2. std::string&& s2 = s1 + s1;
  3. const std::string& s3 = s1 + s1;
  4. s2 += "Text";//true
  5. s3 += "Text";//error
复制代码
 2.4左右值引用的参数匹配

在C++98中,实现一个const左值引用作为参数的函数,传入左值还是右值都可以匹配 
  1. void f(int& x)
  2. {
  3.         std::cout << "f函数的左值引用重载(" << x << ")" << std::endl;
  4. }
  5. void f(const int& x)
  6. {
  7.         std::cout << "f函数的const左值引用重载(" << x << ")" << std::endl;
  8. }
  9. int main()
  10. {
  11.         int x = 10;
  12.         f(x);
  13.         f(10 + 20);
  14.         return 0;
  15. }
复制代码

C++11中,对右值进行了明白界说,此时便可以分担const左值引用对于右值的引用任务给右值引用。即实参是左值会匹配f(左值引用),实参是const左值会匹配f(const 左值引用),实参是右值会匹配f(右值引用)。 (固然没有右值引用的重载下还可以通过const左值引用来引用右值)。

  1. void f(int& x)
  2. {
  3.         std::cout << "f函数的左值引用重载(" << x << ")" << std::endl;
  4. }
  5. void f(const int& x)
  6. {
  7.         std::cout << "f函数的const左值引用重载(" << x << ")" << std::endl;
  8. }
  9. void f(int&& x)
  10. {
  11.         std::cout << "f函数的右值引用重载(" << x << ")" << std::endl;
  12. }
  13. int main()
  14. {
  15.         int x = 10;
  16.         const int y = 20;
  17.         f(x);
  18.         f(y);
  19.         f(10 + 20);
  20.         return 0;
  21. }
复制代码


2.5右值引用和移动语义的使用场景 

2.5.1移动构造与移动赋值 

之前文章中我们自己实现的string类:
  1. namespace ELY
  2. {
  3.         class string
  4.         {
  5.         public:
  6.                 typedef char* iterator;
  7.                 typedef const char* const_iterator;
  8.                 iterator begin()
  9.                 {
  10.                         return _str;
  11.                 }
  12.                 iterator end()
  13.                 {
  14.                         return _str + _size;
  15.                 }
  16.                 const_iterator begin() const
  17.                 {
  18.                         return _str;
  19.                 }
  20.                 const_iterator end() const
  21.                 {
  22.                         return _str + _size;
  23.                 }
  24.                 string(const char* str = "")
  25.                         :_size(strlen(str))
  26.                         , _capacity(_size)
  27.                 {
  28.                         cout << "string(char* str)-构造" << endl;
  29.                         _str = new char[_capacity + 1];
  30.                         strcpy(_str, str);
  31.                 }
  32.                 void swap(string& s)
  33.                 {
  34.                         ::swap(_str, s._str);
  35.                         ::swap(_size, s._size);
  36.                         ::swap(_capacity, s._capacity);
  37.                 }
  38.                 string(const string& s)
  39.                         :_str(nullptr)
  40.                 {
  41.                         cout << "string(const string& s) -- 拷贝构造" << endl; reserve(s._capacity);
  42.                         for (auto ch : s)
  43.                         {
  44.                                 push_back(ch);
  45.                         }
  46.                 }
  47.                 //移动构造
  48.                 string(string&& s)
  49.                 {
  50.                         cout << "string(string&& s) -- 移动构造" << endl;
  51.                         swap(s);//这样写的原因是因为移动构造底层实现原理与直接和临时对象交换资源类似
  52.                 }
  53.                 string& operator=(const string& s)
  54.                 {
  55.                         cout << "string& operator=(const string& s) -- 拷贝赋值" <<
  56.                                 endl;
  57.                         if (this != &s)
  58.                         {
  59.                                 _str[0] = '\0';
  60.                                 _size = 0;
  61.                                 reserve(s._capacity);
  62.                                 for (auto ch : s)
  63.                                 {
  64.                                         push_back(ch);
  65.                                 }
  66.                         }
  67.                         return *this;
  68.                 }
  69.                 ~string()
  70.                 {
  71.                         cout << "~string() -- 析构" << endl;
  72.                         delete[] _str;
  73.                         _str = nullptr;
  74.                 }
  75.                 char& operator[](size_t pos)
  76.                 {
  77.                         assert(pos < _size);
  78.                         return _str[pos];
  79.                 }
  80.                 void reserve(size_t n)
  81.                 {
  82.                         if (n > _capacity)
  83.                         {
  84.                                 char* tmp = new char[n + 1];
  85.                                 if (_str)
  86.                                 {
  87.                                         strcpy(tmp, _str);
  88.                                         delete[] _str;
  89.                                 }
  90.                                 _str = tmp;
  91.                                 _capacity = n;
  92.                         }
  93.                 }
  94.                 void push_back(char ch)
  95.                 {
  96.                         if (_size >= _capacity)
  97.                         {
  98.                                 size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
  99.                                 reserve(newcapacity);
  100.                         }
  101.                         _str[_size] = ch;
  102.                         ++_size;
  103.                         _str[_size] = '\0';
  104.                 }
  105.                 string& operator+=(char ch)
  106.                 {
  107.                         push_back(ch);
  108.                         return *this;
  109.                 }
  110.                 const char* c_str() const
  111.                 {
  112.                         return _str;
  113.                 }
  114.                 size_t size() const
  115.                 {
  116.                         return _size;
  117.                 }
  118.         private:
  119.                 char* _str = nullptr;
  120.                 size_t _size = 0;
  121.                 size_t _capacity = 0;
  122.         };
  123. }
复制代码
2.5.2右值引用和移动语义办理传值返回题目

我们知道,在函数传参的时候使用左值引用可以或许减少拷贝,比如下面通过字符串实现高精度盘算的函数:
  1. string AddStrings(string& num1, string& num2) {//原本不加引用时需要拷贝代价巨大
  2.         string str;
  3.         int end1 = num1.size() - 1, end2 = num2.size() - 1;
  4.         // 进位
  5.         int next = 0;
  6.         while (end1 >= 0 || end2 >= 0)
  7.         {
  8.                 int val1 = end1 >= 0 ? num1[end1--] - '0' : 0;
  9.                 int val2 = end2 >= 0 ? num2[end2--] - '0' : 0;
  10.                 int ret = val1 + val2 + next;
  11.                 next = ret / 10;
  12.                 ret = ret % 10;
  13.                 str += ('0' + ret);
  14.         }
  15.         if (next == 1)
  16.                 str += '1';
  17.         reverse(str.begin(), str.end());
  18.         return str;
  19. }
  20. int main()
  21. {
  22.         std::string s1 = "11111";
  23.         std::string s2 = "22222";
  24.         std::string s3 = AddStrings(s1, s2);
  25.         std::cout << s3 << std::endl;
  26.         return 0;
  27. }
复制代码
但是此时返回str时,如果str过大,那么s3在吸收的时候会进行两次拷贝构造(在string没有实现移动构造以及编译器没有进行编译优化的前提下):

如许子代价特别巨大,但是有了上面的右值引用之后。因为右值是一个临时对象,我们完全可以走移动构造来完成上面的拷贝构造过程。直接把临时对象中的资源给抢过来。但是我们不能说将上面AddString的返回值改为string&&,因为str中存储的资源生命周期在函数的作用域内。无法到达预期效果,我们只有对string类实现了移动构造才能实现抢过来的那一步:
当我们把移动构造屏蔽后,s3吸收str会经历以下过程,走了两次拷贝构造,便是说进行两次新的资源构建:

但是我们实现移动构造之后,全程只有移动构造,没有任何新资源的构建,极大节省了资源消耗:

2.5.3右值引用与移动语义在传参中的提效

测试代码:
  1. int main()
  2. {
  3.     std::list<bit::string> lt;
  4.     bit::string s1("111111111111111111111");
  5.     lt.push_back(s1);
  6.     cout << "*************************" << endl;
  7.     lt.push_back(bit::string("22222222222222222222222222222"));
  8.     cout << "*************************" << endl;
  9.     lt.push_back("3333333333333333333333333333");
  10.     cout << "*************************" << endl;
  11.     lt.push_back(move(s1));
  12.     cout << "*************************" << endl;
  13.     return 0;
  14. }
复制代码
运行结果:

2.6类型分类(相识)


官方文档:
值种别 - cppreference.com ,​​​​https://en.cppreference.com/w/cpp/language/value_category
2.7引用折叠

1.C++中不能直接界说引用的引用如int& && r = i; 如许写会直接报错,通过模板或 typedef
中的类型操作可以构成引用的引用。

  1. using lref = int&;
  2. using rref = int&&;
  3. int main()
  4. {
  5.         int y = 10;
  6.         int& &&r = y;//error
  7.         lref&& z = y;//true
  8.         return 0;
  9. }
复制代码
2.对于两种引用的四种组合,只有&& 与 && 组合时才是右值引用:
  1. lref& x = y;//x类型是int&
  2. lref&& x = y;//x类型是int&
  3. rref& x = y;//x类型是int&
  4. rref&& x = 10;//x类型是int&&
复制代码
  1. template<class T>
  2. void f1(T& x)
  3. {}
  4. template<class T>
  5. void f2(T&& x)
  6. {}
  7. int main()
  8. {
  9.         int n = 0;
  10.         //无折叠->类型实例化为int&
  11.         f1<int>(n);
  12.         f1<int>(0);//报错
  13.         //折叠->类型实例化为int&
  14.         f1<int&>(n);
  15.         f1<int&>(0);//报错
  16.         //折叠->类型实例化为int&
  17.         f1<int&&>(n);
  18.         f1<int&&>(0);//报错
  19.         //折叠->示例化为const int&
  20.         f1<const int&>(n);
  21.         f1<const int&>(0);
  22.         //折叠->实例化为const int&
  23.         f1<const int&&>(n);
  24.         f1<const int&&>(0);
  25.         //无折叠->类型实例化为int&&
  26.         f2<int>(n);//报错
  27.         f2<int>(0);
  28.         //折叠->类型实例化为int&
  29.         f2<int&>(n);
  30.         f2<int&>(0);//报错
  31.         //折叠->类型实例化为int&&
  32.         f2<int&&>(n);//报错
  33.         f2<int&&>(0);
  34.         //折叠->示例化为const int&
  35.         f2<const int&>(n);
  36.         f2<const int&>(0);
  37.         //折叠->实例化为const int&&
  38.         f2<const int&&>(n);//报错
  39.         f2<const int&&>(0);
  40.         return 0;
  41. }
复制代码
  1. template<class T>
  2. void Function(T&& t)
  3. {
  4.         int a = 0;
  5.         T x = a;
  6.         x++;
  7.         cout << &a << endl;
  8.         cout << &x << endl << endl;
  9. }
  10. int main()
  11. {
  12.         //无折叠-模板类型实例化为int&&
  13.         Function(10);//右值
  14.         int a = 0;
  15.         //折叠,因为a为左值,模板类型实例化为int&
  16.         Function(a);//左值
  17.         // std::move(a)是右值,推导出T为int,模板实例化为void Function(int&& t)
  18.         Function(std::move(a)); // 右值
  19.         const int b = 8;
  20.         // b是左值,推导出T为const int&,引用折叠,模板实例化为void Function(const int&
  21.         // 所以Function内部会编译报错,x不能++
  22.         Function(b); // const 左值
  23.         // std::move(b)右值,推导出T为const int,模板实例化为void Function(const int&&t)
  24.         // 所以Function内部会编译报错,x不能++
  25.         Function(std::move(b)); // const 右值
  26.         return 0;
  27. }
复制代码
我们也称Function(T&& t)这种为万能引用,传左值为左值,而传右值则为右值。 
2.8完美转发

我们在实际过程中,可能在函数中再去调用别的函数,比如如下这种情况:
  1. void f1(int& x)
  2. {}
  3. void f1(int&& x)
  4. {}
  5. template<class T>
  6. void fc(T&& t)
  7. {
  8.         f1(t);
  9. }
  10. int main()
  11. {
  12.         int n = 0;
  13.         fc(n);
  14.         fc(0);
  15.         return 0;
  16. }
复制代码
我们上面也说过,对于int&& x = y;此时的x为左值属性。我们发现对于上图中的两次fc调用,n是左值直到传到f1时也可以保持其左值属性不变。但0在传入fc之后,0是一个右值,一个右值被右值引用绑定后,右值引用变量表达式的属性是左值,也就是说fc函数中t的属性是左值。此时便会调用f1的左值引用版本,换言之0在函数传递中失去了其自己的右值属性。如果我们想要保持0自身的右值属性在传递中不丢失,就须要使用完美转发。
完美转发:
  1. template <class _Ty>
  2. _Ty&& forward(remove_reference_t<_Ty>& _Arg) noexcept
  3. { // forward an lvalue as either an lvalue or an rvalue
  4.     return static_cast<_Ty&&>(_Arg);
  5. }
复制代码
完美转发forward本质是一个函数模板,他主要还是通过引用折叠的方式实现。此时我们只须要将上面的代码改为如下格式:
  1. void f1(int& x)
  2. {}
  3. void f1(int&& x)
  4. {}
  5. template<class T>
  6. void fc(T&& t)
  7. {
  8.         f1(forward<T>(t));
  9. }
  10. int main()
  11. {
  12.         int n = 0;
  13.         fc(n);
  14.         fc(0);
  15.         return 0;
  16. }
复制代码
如许0在经fc传入f1时便会调用右值引用版本的重载。保留了其自己的右值属性。 






免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4