左值和右值的概念偶尔就会听到,经常性碰到的是 “表达式是不可修改的左值”,这个问题对于初学者来说会经常遇到。那么左值和右值究竟是什么呢?
左值:从字面意思理解就是 ,出现在赋值语句左边的内容,它要代表一个地址
int i=10;
我们就但看这行代码,我们定义了一个int型变量,系统为它在内存中分配了一块空间(地址),i就是一个左值,再来看看右值,右值是为了更好的解释左值而引入的,常量就是一个右值,比如上面的10;
i=i+1;
再来看这行代码,根据上面的说法,i出现在赋值语句的左边,所以i一定是左值,但是它又出现了赋值语句的右边,这又该怎么去理解呢,这里我们能不能说i也是右值呢,显然是不可以的。注意:这里的i出现在等号的右侧,可以称i具有右值属性(右值属性不是右值),当它出现在等号的左边时,我们又称它具有左值属性,此时我们可以称左值既可以有左值属性,也可以有右值属性。
我们来进一步了解一下自增运算符(前置、后置)哪个是左值,哪个是右值,或者还有其他的情况,我们都知道前置自增是先进行自增,然后再进行别的运算,它的这个自增是对变量本身进行操作的,所以前置加加是左值,后置自增是先进行别的运算,再自增,后置自增实际上是先生成一个临时变量,对这个临时变量进行加一的操作,最后返回的是这个临时变量,因此后置自增是一个右值,每个左值在内存中都有自己对应地址,我们可以通过取地址运算发来判断是否是左值。函数的返回值(因为函数的返回值是一个局部变量,一旦离开函数,函数体内的临时变量就会被自动销毁,因为返回的值也一样)也是一个右值
int reference()
{
int a = 10;
return a;
}
void test5()
{
int b = reference();//函数的返回值(右值)
}
另外还有左值表达式和右值表达式,不要被其字面意思搞混了,左值表达式就是左值,右值表达式就是右值。
左值引用就是绑定到左值的引用,用一个&符号
int a = 10;
int& ref = a;//把ref和a绑定到一起
ref = 10;//可以通过ref来修改a的值
右值引用就是绑定到右值的引用,一般来说,就是要和那些临时变量/即将要销毁的变量绑定,通过&&实现
int&& vai = 10;//右值引用,两个&&符号
string &&str{"I love China."};//将str和字符串绑
再看下面的一行代码
int &&r1=10;
int &&r2=r1;//错误
此处应该注意,虽然&&r1和10绑定了,但是r1本身是左值,编译器会报错,无法将右值绑定到左值
move是c++11标准库里面的一个新函数,move翻译成移动,在这里就指的是把一个左值强制转换成右值,后面的移动构造函数中也会用到。
void func(int&& x)//定义一个需要传右值的函数
{
}
void test5()
{
int r1 = 10;
int&& r2 = 20;
func(r1);//报错
func(move(r1));//
func(move(r2));
}
看上面的代码,我们先定义一个空的需要传入右值的函数,然后调用它,这时候就需要用std::move将r1或r2转换成右值传入,否则就会报错。
下面让我们来看看std::move对string类能否实现移动呢
void test6()
{
string str1 = "I love China";
const char* p = str1.c_str();//转换成c类型的指针
string str2 = move(str1);
const char* q = str2.c_str();
cout << p << endl;
cout << q << endl;
}
观察运行结果,我们发现了str1变成了空串,str1的内容移动到str2里面去了,实际上这里的移动操作是触发了string类的移动构造函数,而不是move的功劳,我们继续看范例
string s1 = "I love China";
move(s1);//此处不会触发移动构造
cout << s1 << endl;
string &&s2 = move(s1);//此处不会触发移动构造函数
cout << s1 << endl;
cout << s2 << endl;
上面又列举了两种不会触发移动构造函数的写法,同时也说明move不具有移动的作用。
移动构造函数是把一块内存中的数据从原来的所有者标记为新的所有者,如果原来的这块所有者是A,那么移动后这块数据的所有者就变成B了,此时对象A变得残缺了,原则上不要再去使用A对象。
在移动构造函数中,函数的形参传入的是右值引用。移动构造函数的写法:在完成移动构造函数时要完成资源的移动,一块对象中的内容移动到另一块中后,原来块中的数据将不在使用,下面通过代码来实现:
class Human
{
public:
int m_age;
string m_name;
Human() :m_name("Tom"), m_age(10)
{
cout << "Human构造函数调用" << endl;
}
Human(const Human& h):m_age(h.m_age),m_name(h.m_name)
{
cout << "human的拷贝构造函数调用" << endl;
}
virtual~Human()
{
cout << "human析构函数调用" << endl;
}
};
class Person:public Human
{
public:
Person():hum(new Human())
{
cout << "Person的构造函数执行" << endl;
}
Person(Person&& p)noexcept :hum(p.hum)//移动构造函数
{
p.hum = nullptr;
cout << "Person的移动构造函数"<
在这里打个断点观察一下,发现运行到此处时,p的内容已经变为空了,可以说p处于一种被释放的状态,不能再使用p,
这里说明一下,person类以public方式继承human类,human类的析构函数最好每次都写成虚函数,不管有没有父类指针指向子类对象的情况,万一有时候自己没写成虚函数而有父类指针指向子类对象的情况,此时就无法调用子类的析构函数。
通常我们会在移动构造函数中加入关键字 noexcept 需要注意它放置的位置,第三块那里的析构函数是因为在调用重载=时,delete了hum,导致调用了Human的析构函数
在有必要的情况下尽量添加移动构造函数和移动构造赋值运算符,达到减少拷贝构造和赋值运算的目的,当然,一般只有在new分配了大量内存的这种类才需要移动构造函数和移动赋值运算符。不抛出异常的移动构造函数、移动赋值运算符都应该加上关键字noexcept。一个本该调用移动构造函数和移动赋值运算符的地方,如果类中没有提供,系统会调用拷贝构造函数和拷贝赋值运算符代替。