一,列表初始化
1.1C++98中传统的{}
C++98中一样平常数组和结构体可以使用{}进行初始化:
- struct Date
- {
- int _year;
- int _month;
- int _day;
- };
- int main()
- {
- int a[] = { 1,2,3,4,5 };
- Date _date = { 2025,2,27 };
- return 0;
- }
复制代码 1.2C++11中的{}
- C++11以后想统一初始化方式,试图实现一切对象皆可用{}初始化,{}初始化也叫做列表初始化。
- 内置类型支持,自界说类型也支持,自界说类型本质是类型转换,中间会产生临时对象,最后优化了以后变成直接构造。
- {}初始化的过程中,可以省略掉=。
- C++11列表初始化的本意是想实现一个大统一的初始化方式,其次他在有些场景下带来的不少便利,如容器push/inset多参数构造的对象时,{}初始化会方便许多。
- class Date
- {
- public:
- Date(int year = 1, int month = 1, int day = 1)
- :_year(year)
- , _month(month)
- , _day(day)
- {
- cout << "Date(int year, int month, int day)" << endl;
- }
- Date(const Date& d)
- :_year(d._year)
- , _month(d._month)
- , _day(d._day)
- {
- cout << "Date(const Date& d)" << endl;
- }
- private:
- int _year;
- int _month;
- int _day;
- };
- int main()
- {
- int a[] = { 1,2,3,4,5 };
- Date _date = { 2025,2,27 };
- const Date& d2 = { 2027,2,29 };//右边{2027,2,29}是一个临时对象,需要加上const
- //也可以不加括号
- Date d3{ 2028,2,27 };
- //非{}初始化必须要加上等号
- Date d4 2025;//编译器报错
- Date d5 = 2025;
- return 0;
- }
复制代码 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的方法也很简朴,{}传入的值个数是固定的,由须要初始化的对象类型决定,里面所有数据的类型可能不同。后者可写入的值不固定,但类型一定雷同。
- map<int, string> m{ {1,string("hallo")} };
- 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,指的是那些可以提供
数据值,但是不可以寻址,比方:临时变量,字面量常量,存储于寄存器中的变量等,也就是说左
值和右值的核心区别就是能否取地址。
- //常见的左值比如:
- int p;
- const int p1;
- int a[10];
- char c;
- a[0] = 1;
- string s("111111111");
- s[0] = 'b';//....
- //它们都有一个共同的特性:可以取地址
- //常见的右值比如:
- 10;
- s[0] + a[0];
- string("111111111111111");
- min(s[0],a[0]);
- //它们均是临时对象,无法取地址
复制代码 2.2左值引用与右值引用
- Type& x = y;//左值引用
- Type&& x = y;//右值引用
复制代码 同样,右值引用其实就是给右值取别名。
1.对于左值引用,不可以直接引用右值,引用右值时须要加上const。
- int& x = 10;//error
- const int& x = 10;//true
复制代码 2.对于右值引用,固然也不可以直接引用左值,但是可以引用move移动之后的左值。
- int x = 10;
- int&& y = x;//error
- int&& y = std::move(x);//true
- //move可以将左值强制类型转化为右值,是库里面的一个函数模板,本质内部是进行强制类型转换,当然他还涉
- //及一些引用折叠的知识,我们后面会详细介绍。
复制代码 3.须要注意的是变量表达式都是左值属性,也就意味着一个右值被右值引用绑定后,右值引用变量变量表达式的属性是左值。
4.语法层面看,左值引用和右值引用都是取别名,不开空间。从汇编底层的角度看无论左值引用还是右值引用,底层都是用指针实现的,没有本质区别。
2.3引用改变生命周期
右值引用和带有const的左值引用都可以延伸临时变量的声明周期,但后者无法被修改。
- std::string s1 = "1111111111";
- std::string&& s2 = s1 + s1;
- const std::string& s3 = s1 + s1;
- s2 += "Text";//true
- s3 += "Text";//error
复制代码 2.4左右值引用的参数匹配
在C++98中,实现一个const左值引用作为参数的函数,传入左值还是右值都可以匹配
- void f(int& x)
- {
- std::cout << "f函数的左值引用重载(" << x << ")" << std::endl;
- }
- void f(const int& x)
- {
- std::cout << "f函数的const左值引用重载(" << x << ")" << std::endl;
- }
- int main()
- {
- int x = 10;
- f(x);
- f(10 + 20);
- return 0;
- }
复制代码
C++11中,对右值进行了明白界说,此时便可以分担const左值引用对于右值的引用任务给右值引用。即实参是左值会匹配f(左值引用),实参是const左值会匹配f(const 左值引用),实参是右值会匹配f(右值引用)。 (固然没有右值引用的重载下还可以通过const左值引用来引用右值)。
- void f(int& x)
- {
- std::cout << "f函数的左值引用重载(" << x << ")" << std::endl;
- }
- void f(const int& x)
- {
- std::cout << "f函数的const左值引用重载(" << x << ")" << std::endl;
- }
- void f(int&& x)
- {
- std::cout << "f函数的右值引用重载(" << x << ")" << std::endl;
- }
- int main()
- {
- int x = 10;
- const int y = 20;
- f(x);
- f(y);
- f(10 + 20);
- return 0;
- }
复制代码
2.5右值引用和移动语义的使用场景
2.5.1移动构造与移动赋值
- 1.移动构造函数是一种构造函数,类似拷贝构造函数,移动构造函数要求第一个参数是该类类型的引用,但是不同的是要求这个参数是右值引用,如果还有其他参数,额外的参数必须有缺省值。
- 2.移动赋值是一个赋值运算符的重载,他跟拷贝赋值构成函数重载,类似拷贝赋值函数,移动赋值函数要求第一个参数是该类类型的引用,但是不同的是要求这个参数是右值引用。
- 3.对于像string/vector如许的深拷贝的类或者包含深拷贝的成员变量的类,移动构造和移动赋值故意义,因为移动构造和移动赋值的第一个参数都是右值引用的类型,他的本质是要“窃取”引用的右值对象的资源,而不是像拷贝构造和拷贝赋值那样去拷贝资源,从提高服从。下面ELY::string例实现了移动构造和移动赋值,我们须要结合场景理解。
之前文章中我们自己实现的string类:
2.5.2右值引用和移动语义办理传值返回题目
我们知道,在函数传参的时候使用左值引用可以或许减少拷贝,比如下面通过字符串实现高精度盘算的函数:
- string AddStrings(string& num1, string& num2) {//原本不加引用时需要拷贝代价巨大
- string str;
- int end1 = num1.size() - 1, end2 = num2.size() - 1;
- // 进位
- int next = 0;
- while (end1 >= 0 || end2 >= 0)
- {
- int val1 = end1 >= 0 ? num1[end1--] - '0' : 0;
- int val2 = end2 >= 0 ? num2[end2--] - '0' : 0;
- int ret = val1 + val2 + next;
- next = ret / 10;
- ret = ret % 10;
- str += ('0' + ret);
- }
- if (next == 1)
- str += '1';
- reverse(str.begin(), str.end());
- return str;
- }
- int main()
- {
- std::string s1 = "11111";
- std::string s2 = "22222";
- std::string s3 = AddStrings(s1, s2);
- std::cout << s3 << std::endl;
- return 0;
- }
复制代码 但是此时返回str时,如果str过大,那么s3在吸收的时候会进行两次拷贝构造(在string没有实现移动构造以及编译器没有进行编译优化的前提下):
如许子代价特别巨大,但是有了上面的右值引用之后。因为右值是一个临时对象,我们完全可以走移动构造来完成上面的拷贝构造过程。直接把临时对象中的资源给抢过来。但是我们不能说将上面AddString的返回值改为string&&,因为str中存储的资源生命周期在函数的作用域内。无法到达预期效果,我们只有对string类实现了移动构造才能实现抢过来的那一步:
当我们把移动构造屏蔽后,s3吸收str会经历以下过程,走了两次拷贝构造,便是说进行两次新的资源构建:
但是我们实现移动构造之后,全程只有移动构造,没有任何新资源的构建,极大节省了资源消耗:
2.5.3右值引用与移动语义在传参中的提效
- 查看STL文档我们发现C++11以后容器的push和insert系列的接口否增加的右值引用版本。
- 当实参是一个左值时,容器内部继续调用拷贝构造进行拷贝,将对象拷贝到容器空间中的对象。
- 当实参是一个右值,容器内部则调用移动构造,将实参的资源直接掠夺过来构造当前对象
测试代码:
- int main()
- {
- std::list<bit::string> lt;
- bit::string s1("111111111111111111111");
- lt.push_back(s1);
- cout << "*************************" << endl;
- lt.push_back(bit::string("22222222222222222222222222222"));
- cout << "*************************" << endl;
- lt.push_back("3333333333333333333333333333");
- cout << "*************************" << endl;
- lt.push_back(move(s1));
- cout << "*************************" << endl;
- return 0;
- }
复制代码 运行结果:
2.6类型分类(相识)
- C++11以后,进一步对类型进行了划分,右值被划分纯右值(pure value,简称prvalue)和将亡值(expiring value,简称xvalue)。
- 纯右值是指那些字面值常量或求值结果相当于字面值或是一个不具名的临时对象。如: 42、true、nullptr 或者类似str.substr(1, 2)、str1 + str2 传值返回函数调用,或者整形a、b、a++,a+b 等。纯右值和将亡值C++11中提出的,C++11中的纯右值概念划分等价于C++98中的右值。
- 将亡值是指返回右值引用的函数的调用表达式和转换为右值引用的转换函数的调用表达,如move(x)、static_cast<X&&>(x)。
- 泛左值(generalized value,简称glvalue),泛左值包含将亡值和左值。
- 著名字,就是glvalue;著名字,且不能被move,就是lvalue;著名字,且可以被move,就是xvalu;没著名字,且可以被移动,则是prvalue。
官方文档:
值种别 - cppreference.com ,https://en.cppreference.com/w/cpp/language/value_category
2.7引用折叠
1.C++中不能直接界说引用的引用如int& && r = i; 如许写会直接报错,通过模板或 typedef
中的类型操作可以构成引用的引用。
- using lref = int&;
- using rref = int&&;
- int main()
- {
- int y = 10;
- int& &&r = y;//error
- lref&& z = y;//true
- return 0;
- }
复制代码 2.对于两种引用的四种组合,只有&& 与 && 组合时才是右值引用:
- lref& x = y;//x类型是int&
- lref&& x = y;//x类型是int&
- rref& x = y;//x类型是int&
- rref&& x = 10;//x类型是int&&
复制代码- template<class T>
- void f1(T& x)
- {}
- template<class T>
- void f2(T&& x)
- {}
- int main()
- {
- int n = 0;
- //无折叠->类型实例化为int&
- f1<int>(n);
- f1<int>(0);//报错
- //折叠->类型实例化为int&
- f1<int&>(n);
- f1<int&>(0);//报错
- //折叠->类型实例化为int&
- f1<int&&>(n);
- f1<int&&>(0);//报错
- //折叠->示例化为const int&
- f1<const int&>(n);
- f1<const int&>(0);
- //折叠->实例化为const int&
- f1<const int&&>(n);
- f1<const int&&>(0);
- //无折叠->类型实例化为int&&
- f2<int>(n);//报错
- f2<int>(0);
- //折叠->类型实例化为int&
- f2<int&>(n);
- f2<int&>(0);//报错
- //折叠->类型实例化为int&&
- f2<int&&>(n);//报错
- f2<int&&>(0);
- //折叠->示例化为const int&
- f2<const int&>(n);
- f2<const int&>(0);
- //折叠->实例化为const int&&
- f2<const int&&>(n);//报错
- f2<const int&&>(0);
- return 0;
- }
复制代码- template<class T>
- void Function(T&& t)
- {
- int a = 0;
- T x = a;
- x++;
- cout << &a << endl;
- cout << &x << endl << endl;
- }
- int main()
- {
- //无折叠-模板类型实例化为int&&
- Function(10);//右值
- int a = 0;
- //折叠,因为a为左值,模板类型实例化为int&
- Function(a);//左值
- // std::move(a)是右值,推导出T为int,模板实例化为void Function(int&& t)
- Function(std::move(a)); // 右值
- const int b = 8;
- // b是左值,推导出T为const int&,引用折叠,模板实例化为void Function(const int&
- // 所以Function内部会编译报错,x不能++
- Function(b); // const 左值
- // std::move(b)右值,推导出T为const int,模板实例化为void Function(const int&&t)
- // 所以Function内部会编译报错,x不能++
- Function(std::move(b)); // const 右值
- return 0;
- }
复制代码 我们也称Function(T&& t)这种为万能引用,传左值为左值,而传右值则为右值。
2.8完美转发
我们在实际过程中,可能在函数中再去调用别的函数,比如如下这种情况:
- void f1(int& x)
- {}
- void f1(int&& x)
- {}
- template<class T>
- void fc(T&& t)
- {
- f1(t);
- }
- int main()
- {
- int n = 0;
- fc(n);
- fc(0);
- return 0;
- }
复制代码 我们上面也说过,对于int&& x = y;此时的x为左值属性。我们发现对于上图中的两次fc调用,n是左值直到传到f1时也可以保持其左值属性不变。但0在传入fc之后,0是一个右值,一个右值被右值引用绑定后,右值引用变量表达式的属性是左值,也就是说fc函数中t的属性是左值。此时便会调用f1的左值引用版本,换言之0在函数传递中失去了其自己的右值属性。如果我们想要保持0自身的右值属性在传递中不丢失,就须要使用完美转发。
完美转发:
- template <class _Ty>
- _Ty&& forward(remove_reference_t<_Ty>& _Arg) noexcept
- { // forward an lvalue as either an lvalue or an rvalue
- return static_cast<_Ty&&>(_Arg);
- }
复制代码 完美转发forward本质是一个函数模板,他主要还是通过引用折叠的方式实现。此时我们只须要将上面的代码改为如下格式:
- void f1(int& x)
- {}
- void f1(int&& x)
- {}
- template<class T>
- void fc(T&& t)
- {
- f1(forward<T>(t));
- }
- int main()
- {
- int n = 0;
- fc(n);
- fc(0);
- return 0;
- }
复制代码 如许0在经fc传入f1时便会调用右值引用版本的重载。保留了其自己的右值属性。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |