网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
- [3.右值引用与移动构造](#3_229)
- [4.有了右值引用STL库的变化](#4STL_314)
- [5.模板中的&& 万能引用](#5__334)
- [6.完美转发](#6_419)
- [7.新的类功能](#7_456)
- * [1.默认成员函数](#1_457)
* [2.强制生成默认函数的关键字default:](#2default_473)
* [3.禁止生成默认函数的关键字delete:](#3delete_483)
文章简介
本篇文章续上篇文章C++11特性的讲解,这篇主要讲解C++11的新特性:右值引用与移动语义。
本篇文章会涵盖: 左值与右值的区别,怎么去区分左值与右值。右值引用的概念,为什么要增加右值引用,右值引用的作用,解决了什么问题, 右值引用与左值引用的比较,右值引用的使用场景,右值引用的注意事项,完美转发的使用,C++11后容器新增的两个默认函数等等
二.右值引用
1.什么是左值,什么是右值?什么是左值引用,什么是右值引用?
- 左值:左值就是那些可以出现在赋值符号左边的东西,它标识了一个可以存储结果值的地点。例如:一些变量名或解引用的指针 ,左值是可以取地址的,一般可以对它赋值,一般可以修改(除const 修饰),可以出现在赋值运算符的左边,右值不能出现在赋值运算符的左边。左值引用就是对左值的引用,给左值取别名。
程序在编译时,编译器会为每个变量分配一个地址(左值),这个地址在编译是即可知。
int& func(int& x) //func函数的返回值也是左值
{ //因为func函数返回的是x,x是其他变量的别名,当func函数结束时,x没有被销毁,是有地址的
x\*=3;
return x;
}
int main()
{
//一些左值的例子
int _a = 10;
int\* p = &_a;
const int _b = 10;
//\_b = 20; //不能修改
//他们都是可以取地址的。
//左值引用
int& pa = _a;
int\*& pp = p;
const int& pb = _b;
int& ret = func(_a);
return 0;
}
- 右值:右值就是那些可以出现在赋值符号右边的东西,它必须具有一个特定的值。
右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。右值引用就是对右值的引用,给右值取别名。
与左值相反,变量中存储的那个值(右值),只有在运行时才可知,且只有要用到变量中存储的值时,编译器才会发出指令从指定的地址读入变量的值,并将它存于寄存器中。也就是说,右值就是一个数字或一个字面值或一个常量或一个式子等,它并不标识任何位置。
int func() //传值返回,函数结束后,局部变量a就销毁了,就没有地址
{ //所以是右值
int a = 10;
return a;
}
int main()
{
int x = 10;
int y = 10;
10; //字面量
x + y; //式子
func(); //返回值是右值
//右值引用
int&& _a = 10;
int&& _x = func();
int&& add = x + y;
return 0;
}
注意:右值是不能取地址的,如字面量10是没有地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址,也就是说_a是可以取地址的。
总结:
左值可以取地址,右值不能取地址。不能以是否能修改判断是左值还是右值,因为左值加了const也不能被修改。
2.左值引用与右值引用比较
左值引用总结:
- 左值引用一般只能引用左值,不能引用右值。
- 但是const左值引用既可引用左值,也可引用右值。
int main()
{
// 左值引用只能引用左值,不能引用右值。
int a = 10;
int& ra1 = a;
// ra为a的别名
//int& ra2 = 10; // 编译失败,因为10是右值
// const左值引用既可引用左值,也可引用右值。
const int& ra3 = 10;
const int& ra4 = a;
return 0;
}
右值引用总结:
- 右值引用一般只能引用右值,不能引用左值。
- 但是右值引用可以move以后的左值。
关键字move
move可以将右值转为左值,但是不能将左值转为右值。move(x)不是将x变为右值,是move()后的返回值是左值,x本身属性没有改变。
int main()
{
// 右值引用只能右值,不能引用左值。
int&& r1 = 10;
// error C2440: “初始化”: 无法从“int”转换为“int &&”
// message : 无法将左值绑定到右值引用
int a = 10;
int&& r2 = a;
// 右值引用可以用move,move以后右值可以变为左值
int&& r3 = std::move(a);
return 0;
}
三.右值引用使用场景和意义
前面所学了左值引用,左值引用解决了传值传参:利用左值引用会减少在传值传参要进行拷贝的问题。函数返回值的问题(函数结束该变量(对象)没有别销毁可以用左值引用返回,也可以减少拷贝)
1.左值引用的使用场景:
做参数和做返回值都可以提高效率。
void func1(map<string, string> dict1)
{}
void func2(const map<string,string>& dict2)
{}
int main()
{
map<string, string> dict; //假设dict很大
func1(dict); //会调用map的拷贝构造,用dict构造dict1
func2(dict); //dict2直接是dict的别名,不用拷贝构造
// string operator+=(char ch) 传值返回存在深拷贝
// string& operator+=(char ch) 传左值引用没有拷贝提高了效率
s1 += '!';
return 0;
}
2.左值引用的短板:
但是当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回,只能传值返回。传值返回会导致2次拷贝构造,如果编译器优化了,就是一个拷贝构造。
例如:
namespace XX
{
class string
{
public:
string(const char\* str = "")
:\_size(strlen(str))
, \_capacity(_size)
{
cout << "构造" << endl;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
void swap(string& s)
{
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}
// 拷贝构造
string(const string& s)
:\_str(nullptr)
{
cout << "拷贝构造" << endl;
string tmp(s._str);
swap(tmp);
}
// 赋值重载
string& operator=(const string& s)
{
cout << "赋值拷贝" << endl;
string tmp(s);
swap(tmp);
return \*this;
}
~string()
{
delete[] _str;
_str = nullptr;
}
private:
char\* _str;
size_t _size;
size_t _capacity;
};
}
XX::string func(const XX::string& s)
{
XX::string str(s);
return str;
}
int main()
{
XX::string s1("hello");
XX::string ret1 = func(s1);
return 0;
}
如图:
解析:
上面示例代码中,func()函数的返回值是一个右值,会用这个值去构造ret1,因为XX::里面string类的拷贝构造的参数是const string& ,是一个const 左值引用,所以不仅可以引用左值,也可以引用右值。这里就会拷贝构造,是一个深拷贝。
3.右值引用与移动构造
上面的场景,有没有可以不用拷贝就能解决的办法呢?
可以用右值引用和移动语义解决上述问题:
在XX::string中增加移动构造,移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己。
// 拷贝构造
string(const string& s)
:\_str(nullptr)
{
cout << "拷贝构造" << endl;
string tmp(s._str);
swap(tmp);
}
//移动构造
string(string&& s)
:\_str(nullptr)
{
cout << "移动构造" << endl; //打印只是方便观察
swap(s);
}
移动构造原理:
先用str去拷贝构造一个临时对象(右值),用临时对象又去拷贝构造s1,拷贝完成后,临时对象再被销毁。
移动构造就是利用了临时对象反正就会被销毁的特点,直接将s1与临时对象交换,然后临时对象再把s1析构掉。
所以重载了一个构造函数(移动构造),移动构造的参数为右值引用,所以临时对象构造s1的时候,编译器会走更匹配的移动构造,而不会走拷贝构造。
如下图解:
编译器不优化前: 需要做一次拷贝。
编译器优化后: 不用拷贝构造
编译器优化后可以理解为:返回值str本身是一个左值,经过编译器处理后,可以猜测编译器可能将它move变成了右值,然后根据优先匹配更合适的原则,直接走移动构造,减少拷贝。
除了有移动拷贝,还有移动赋值。
// 赋值重载
string& operator=(const string& s)
{
cout << "赋值拷贝" << endl;
string tmp(s);
swap(tmp);
return \*this;
}
// 移动赋值
string& operator=(string&& s)
{
cout << " 移动赋值" << endl;
swap(s);
return \*this;
}
当我们将上面的代码改成两个语句时:
XX::string func(const XX::string& s)
{
XX::string str(s);
return str;
}
int main()
{
XX::string s1("hello");
XX::string ret1 ;
ret1 = func(s1);
return 0;
}
这里编译器就不会优化了,只有是同一条语句连续构造或赋值时,编译器才会优化处理。
这里运行后,我们看到调用了一次移动构造和一次移动赋值。因为这里是用一个已经存在的对象接收。
XX::func() 函数中会先用str构造生成一个临时对象,但是我们可以看到,编译器很聪明的在这里把str识别成了右值,调用了移动构造。然后在把这个临时对象做为XX::func()函数调用的返回值赋值给ret1,这里调用的移动赋值。
4.有了右值引用STL库的变化
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新
看到,编译器很聪明的在这里把str识别成了右值,调用了移动构造。然后在把这个临时对象做为XX::func()函数调用的返回值赋值给ret1,这里调用的移动赋值。
4.有了右值引用STL库的变化
[外链图片转存中…(img-8Y77rJUY-1715700449383)]
[外链图片转存中…(img-E9x7ZhEp-1715700449384)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新