本博客将记录:新经典课程知识点的第12节的笔记!
本小节的知识点分别是左值、右值、左值引用、右值引用、move函数。
今天总结的知识分为以下5个点:
一、左值和右值
二、引用分类
三、左值引用
四、右值引用及引入其的目的
五、std::move函数(C++11引入的新的标准库函数)
一、左值和右值:
所谓左值(左值表达式):“能用在赋值语句等号左侧的东西”,就称之为左值。它能够代表一个地址(也即能代表一块内存区域)
所谓右值(右值表达式):“不能用在赋值语句等号左侧的东西”,就称之为右值。它能代表一个值(任何类型),但不代表内存地址。
左值和右值这种东西其实我们之前一直在使用,只不过可能没有很明确的概念而已。就比如一般的变量or对象就是个左值,常量就是个右值。
注意事项:
①不能修改的左值也是右值!(就比如常量,包括常数以及const修饰的变量)
②不能作为左值的值就是右值!
③左值有时候也可当做是右值来使用!
④C++中的任何一条表达式,要么是左值,要么就是右值,不可能是其他值了!
(一个左值,可能同时具有左值属性和右值属性。也即左值是有右值属性的,但本质上还是左值)
(一个右值,只具有右值属性。不可能同时具有左值属性和右值属性。)
请看以下代码:
int a = 1; const int b = 2; 1 = a;//×!能作为左值表达式得必须是可修改的左值! b = a;//×!能作为左值表达式得必须是可修改的左值! int c = a;//左值有时候也可当做是右值来使用 int i = -1; i = i + 1;//i是左值!
能用左值得运算符有哪些呢?下面举几个例子:
1)=赋值运算符
int a = 1; (a = 4) = 8;//a = 8;
2)&取地址运算符
int a = 5; &a;//✓!左值可以取其地址(因为左值就代表了一块内存空间) &8;//×!右值不可以取地址
3)string、vector等容器所重载的[ ]号运算符
string str = "lzf"; str[0] = 'c';
4)容器的迭代器也是左值
auto x = str.begin(); x++; x--; cout << *x << endl;//c x = x + 2; cout << *x << endl;//f
总结:其实,通过看一个运算符在字面上能否进行操作,我们就可以判断出该运算符是否是左值/右值了。能do算术运算的基本就是左值,不能do算术运算的基本就是右值了!
二、引用分类:
我在之前总结过,引用的作用:给一个对象/变量取别名,且引用类型的变量必须要初始化(因为引用的本质是指针常量,指针是个常量,也即该指针指向的对象的地址是个const的,如果不初始化会导致该指针常量乱指向)
(指针变量是有空指针的,但是引用变量是没有空引用的说法的!)
下面我将总结引用变量的三种类型:
①左值引用(绑定到左值)
请看以下代码:
int value = 10; int& refval = value; refval = 13;//==> value =13;
②常量引用(const referenceType& refName):本质上常量引用也是左值引用,只不过常量引用时不可改变我要do初始化的那个对象/变量的值而已!也即:const常量引用既可以绑定到左值上,又可以绑定到右值上,也即const的通吃的,绑谁都行。
请看以下代码:
int value = 10;
int& refval = value;
refval = 13;//==> value = 13;
const int& refval2 = value;//==> refval2 = 13;常量引用可以绑定到左值上!
refval2 = 11;//错误!表达式必须是可修改的左值!
refval2 = refval;//错误!表达式必须是可修改的左值!
string str{ "I Love China!" };
const string& s{ "I Love China!" };//常量引用可以绑定到右值上!
③右值引用(绑定到右值):是C++11的新标准中引入的概念!用两个引用符号&&来do!
请看以下代码:
//这是C98写的左值引用和常量引用的常见用法: int a = 1; int& b = a;//√正确!可将左值引用绑定到左值上!b == 1 int& b = 1;//错误!非常量引用的表达式必须为左值! //也即无法将左值引用变量b绑定到右值1上! //这是C++11新引入的右值引用的用法: int aa = 11; int&& bb = aa;//×错误!无法将右值引用绑定到左值! //也即无法将右值引用变量bb绑定到aa这个左值上! int&& bb = 11;//√正确!可将右值引用绑定到右值上! bb == 11
三、左值引用(1个地址符&):
左值引用:必须绑定到左值的引用类型。下面用代码详细讲解怎么使用左值引用。
(左值引用一般是不能绑定右值,除非你用const的左值引用,其实就算const常量引用了)
int a = 1;
int& aa;//❌错误!引用必须在定义时就初始化!
int& b{ a };//将左值引用b绑定到左值a上!
const int& c = 1;//将const常量引用c绑定到1上!
//在编译器的内部const int& c = 1;这行代码是这样工作的:==>
//int tempVal = 1; const int& c = tempVal;
四、右值引用(2个地址符&&)及引入其的目的
右值引用:必须绑定到右值的引用类型。
(右值引用一般是不能绑定到左值上的!)
下面用代码详细讲解怎么使用右值引用。
string str{ "I Love China!" };
string& r1{ str };
string& r2{ "I Love China!" };//错误!左值引用不能绑定到一个常量/临时对象(右值)上!
//此时编译器会生成一个临时的string对象,该对象存储的值是"I Love China!"
//而临时对象在Cpp中被认为是右值!
//并尝试初始化给r2这个左值引用。
//这是错误的!
string&& r3{ "I Love China!" };//√!右值引用可以绑定到一个临时对象(右值)上!
//其实就相当于给string类的临时对象"I Love China!" 取了个名字r3
//当我们后续要用到该临时对象时可以直接用r3这个对象对其进行读写操作即可!
string&& r4{ str };//×错误!右值引用无法绑定到左值上!
当然,右值引用还具有const的形式,也即:const常量右值引用
const int && rr = 1;//对!右值引用可以绑定到右值上!且是const不可变的! rr = 1;//❌!const限定了该右值引用是const的,不可变了的!
总结:
返回左值引用的函数(比如拷贝构造函数)、赋值、取下标[]、解引用、和前置递增递减运算符(--i),都是返回左值(左值表达式)的例子。
返回非引用类型的函数、算术运算符、关系、位和后置递增递减运算符(i--),都是返回右值(右值表达式)的例子。
int i = 10; int& leftRi = (--i);//--i为左值 int&& rightRi = (i--);//i--为右值 --i:编译器先让i=i-1;然后再用i去do事情 i--:编译器先生成要给临时的变量_tempi=i;然后用i去do事情,最好再让i = i-1;并返回_tempi 所以我们会发现i--时我们输出的i是原来没减1的值! int i = 10; int a = --i; cout << a << endl;// a == 9! i = 10; int b = i--; cout << b << endl;// b == 10! //++i和i++是同理的! //学习了左值和右值的概念之后,是不是对于++--运算符的理解更为深入了呢?我想应该是这样的來
注意:
1-左值引用绑定到左值上后,这2个值是共命运的,因为是取别名嘛!(左值痴情好男人!)
int i = 10; int& r1 = ++i;//注意!r1这个左值绑定为i的值后,r1的值与i的值是共命运的关系!因为是起了别名嘛! r1 += 10; cout << "r1 = " << r1 << endl;//r1 = 21; cout << "i = " << i << endl;//i = 21;
2-右值引用绑定到右值上后,这2个值是不再有任何关系了的,互相独立了!(右值渣男!)
int i = 10; int&& r2 = i++;//注意!r1这个右值绑定为i的值后,r1的值与i的值就没有任何关系了! r2 += 10; cout << "r2 = " << r2 << endl;//r2 = 20; cout << "i = " << i << endl;//i = 11;
3-右值引用虽然绑定到了右值,但其本身还是个左值
int i = 110; int&& r1 = i++; int& rr1 = r1; int&& r2 = r1;//错误!右值引用r1也是左值!而右值引用r2不可以绑定到左值r1上! cout << rr1 << endl;//110 cout << r1 << endl;//110
4-all的变量/对象都是左值,因为每个变量/对象名都代表了一个内存地址。
5-任何函数都形参都是左值。
比如:
void func(int a,int&& b); //形参a是左值,因为形参也是变量嘛。 //b是右值引用类型,但也是一个左值
6-临时对象/匿名对象都是右值。
引入右值引用的目的:
a)C++11引入的右值引用,用&&符号表示,代表了一种新的数据类型,引入了这种新的数据类型必然是有目的的有用处的!
b)右值引用可以提高程序的运行效率!(把拷贝对象变成移动对象来提高程序运行的效率!)其中,拷贝对象是类的拷贝构造函数do的事情,移动对象上类的移动构造函数do的事情。(拷贝构造函数相信你已经很熟悉了,那什么是移动构造函数呢?不用慌,这里看不懂没关系,在下一节的课程我将学习并总结这个新的移动构造函数以及移动赋值operator=号运算符函数。)
因为你拷贝对象时可能会涉及new一块内存的操作,而移动对象就可以直接让对象a的内存空间到主人直接转手(移动)给对象b,然后再释放掉对象a。这样就可以大大地提高代码的运行效率了!
c)右值引用是用在移动构造函数以及移动赋值运算符函数中的!
拷贝构造函数与移动构造函数
赋值运算符重载函数与移动赋值运算符重载函数
这2种类型的函数其实差别不大,前者都是用const varType& var 来传参的,而后者都是用const varType&& var来传参数的!
五、std::move函数(C++11引入的新的标准库函数):
std::move() :从名字上看是一个移动的函数。但实际上,这个函数根本就没有做移动的操作!std::move()的作用只有一个:将一个左值 强制转换为一个右值!且书上建议我们,当该左值使用std::move()函数强制类型转换为右值后就不能够再使用该左值去do事情了!这是规范!if你偏不这么干,那编译器也拿你没招儿!
解释:右值引用只可以绑定到一个右值上,但如果一个左值通过std::move()函数强制转换为一个右值,那么就可以让右值引用绑定到该强制类型转换后的值身上了!
请看以下代码:
int i = 10; //int&& ri1 = i;//无法将右值引用变量ri1绑定到左值i上! int&& ri1 = i++; cout << "ri1 = " << ri1 << endl;//10 cout << "i = " << i << endl;//11 i += 20;//i = 31 cout << "ri1 = " << ri1 << endl;//10 cout << "i = " << i << endl;//31 i = 10; int&& ri2 = std::move(i);//用std::move()函数将左值强制转换为右值 cout << "ri2 = " << ri1 << endl;//10 cout << "i = " << i << endl;//10 i += 20; cout << "ri2 = " << ri2 << endl;//30 cout << "i = " << i << endl;//30
注意:通过上面的代码例子,你可以发现,本来右值引用绑定了右值后,这2个值应该是相互独立了的,互不相干的了!(右值本来是个渣男!)但是一旦右值引用绑定了 通过std::move()函数强制转换为的 右值时,这2个值就相关了!共命运了!穿一条裤子了!(此时右值就不再是渣男了!转身一变变成痴情好男人了!)
再比如:(穿一条裤子的例子)
int&& ri1 = 100; int&& ri2 = std::move(ri1);//此时ri1和ri2就共命运了!穿一条裤子了! cout << "ri1 = " << ri1 << endl;//100 cout << "ri2 = " << ri2 << endl;//100 ri1 = 68; cout << "ri1 = " << ri1 << endl;//68 cout << "ri2 = " << ri2 << endl;//68 ri2 = 88; cout << "ri1 = " << ri1 << endl;//88 cout << "ri2 = " << ri2 << endl;//88
运行结果:
再比如:(穿一条裤子的例子)
string str = "I Love China!"; string&& rstr = std::move(str); //不会触发string类容器的移动构造函数的执行!因为这是右值引用 //而不是创建一个新的string类的对象! cout << "str = " << str << endl;//str = I Love China! cout << "rstr = " << rstr << endl;//rstr = I Love China! str = "abc"; cout << "str = " << str << endl;//str = abc cout << "rstr = " << rstr << endl;//rstr = abc rstr = "defghijk"; cout << "str = " << str << endl;//str = defghijk cout << "rstr = " << rstr << endl;//rstr = defghijk
运行结果:
注意:如果你用容器类的对象do std::move()的话,就会造成不但没有利用该容器类的移动构造函数将一个对象的某段内存的权限直接给到另外一个对象,还会额外开辟了内存。 再将该内存中的内容拷贝到这个新的内存中
string str = "I Love China!"; const char* p = str.c_str(); cout << "str = " << str << endl; cout << "&p = " << &p << endl; string def = std::move(str);//会触发string类容器的移动构造函数 const char* q = def.c_str(); cout << "def = " << def << endl; cout << "&q = " << &q << endl;
运行结果:
本节课中,除了右值引用这个知识点,其他的知识点大家伙应该都不会有啥疑问。不要着急。在下一节中,我将深入总结右值引用和std::move()函数以及移动构造函数、移动赋值运算符重载函数。届时,大家便可阔然开朗!
好,那么以上就是这一3.11小节我所回顾的内容的学习笔记,希望你能读懂并且消化完,也希望自己能牢记这些小小的细节知识点,加油吧,我们都在coding的路上~



