左值和右值 不是等于号的左边和右边 !!(一部分场景下是这样)
右值可以描述成一个临时值
c++ 左值、右值、左值引用、右值引用&&
- 左值
- 右值
- 左值引用
- 右值引用
- 结论
- 第二弹~ 你可以完全不看上面的解释
- 移动语义
- 移动构造和move
左值
英文简写为“lvalue”,是“locator value”
最初指的是可出现在赋值语句左边的实体(就是等于号左边的值)
本质上是是 可以取到它的地址的值的内存区域(一般可以通过&)
常见的左值有:
1、变量(对象);
int i = 0; i = 1; // i是变量(对象),左值
2、const 变量(对象);
const int ci = 5; //ci = 6; // ci是const变量(对象),是左值但不能出现在赋值号左边
3、对指针解引用,*(指针)
;*pi = 1;
4、数组元素,a[1]
;
5、结构体成员、类成员,s.m_a
、ps->ma
;
st.m_a = 1; // 结构体成员,左值pst->m_b = 2;
右值
英文简写为“rvalue”,是"read value"的缩写
指一种表达式,其结果是值而非值所在的位置。一般是没有明确的内存位置,无法使用&获取地址,值不可被修改的。
另外,很多时候左值可以当右值使用。
常见的右值有:
1、字面常量(立即数),引号括起的字符串除外(它们由其地址表示),42、‘a’;
2、算术运算符(+、-、*、/、%、正号、负号)的求值结果,1+2;
3、逻辑运算符(&&、||、!)的求值结果,a!=10;
4、关系运算符(>、>=、<、<=、==、!=)的求值结果,a>10 && a<100;
5、函数的非引用返回值。这种返回值位于临时内存单元中,运行到下一条语句时,它们可能不再存在;
i = get100(); // 函数的非引用返回值,右值
6、当条件运算符的两个表达式都是左值或者能转换成同一种左值类型时,运算的结果是左值;否则运算的结果是右值。
左值引用
为了与右值引用区分开,把C++11之前出现的引用成为左值引用。
左值引用就是C++中的引用类型,是己定义的变量的别名,主要用作函数形参或返回值。
// g++ 13_lvalue_reference.cpp
int get100()
{return 100;
}
int main()
{int i = 0;int &ri = i; // 左值引用
// int &*pri = &ri; // 不能定义引用的指针。报错:cannot declare pointer to ‘int&’
// int &&rri = ri; // 不能定义引用的引用。在C++11标准里,这是一个右值引用int *pi = &i;int *&rpi = pi; // pi是左值,rpi是指针的引用int &r_pi = *pi; // *pi是左值,可以初始化左值引用int arr[5] = {0};int &rarr = arr[0]; // arr[0]是左值,可以初始化左值引用// const 左值引用可以被初始化为一些左值const int &cri = i; // const 左值引用
// const int *&crpi = pi; // 报错:用 ‘int*’ 初始化 ‘const int*&’ 无效const int &cr_pi = *pi; // const 左值引用const int &crarr= arr[0];// const 左值引用可以被初始化为右值 去掉const不行const int &ri0 = 42;const int &ri1 = 'a';const int &ri2 = 1+2;const int &ri3 = (ri1 != 'a');const int &ri4 = (ri1 >= 'a');const int &ri5 = get100(); // 函数的非引用返回值,右值const int &ri6 = i>0?1:0;
// ri0 = 1; // 报错,const引用是只读的,其值不能修改return 0;
}
右值引用
右值引用:这是为了支持移动操作,C++11标准引入的一种新的引用类型
所谓右值引用就是必须绑定到右值的引用。
通过 &&
而不是 &
来获得右值引用。
怎样定义右值引用?右值引用使用&&
来定义,也是必须初始化,初始化后无法改变其绑定的对象。定义右值引用格式:类型 &&引用名称 = 右值;
。定义一个右值引用可以参考下面代码:
int &&rl = 13;
int *pi = &rl;
将右值关联到右值引用导致该右值被存储到特定的位置,且可以获取该位置的地址。也就是说,虽然不能将运算符&用于 13,但可将其用于 rl。通过将数据与特定的地址关联,使得可以通过右值引用来访问该数据。
右值引用只能绑定到一个右值,右值要么是字面常量,要么是在表达式求值过程中创建的临时对象;
而临时对象有两个特点:
1、该对象将要被销毁;
2、该对象没有其他用户再使用它。
这就意味着,右值引用的代码是最后使用这个对象的了,可以自由地接管所引用的对象的资源。
如上图,变量a、b相加之后会产生一个值,这个值就是临时量,但它在内存中肯定是存在某个地址的,没有右值引用之前,这个值使用完就会被销毁,我们也不会知道它的内存地址。现在,这块内存可以被右值引用关联,关联后,右值引用甚至可以改变内存的内容,等右值引用使用完再销毁。
// g++ 13_rvalue_reference.cpp -std=c++11
#include <iostream>
using namespace std;
int get100()
{return 100;
}void fun(int &&rri)// 右值引用作为函数形参
{rri = 0;
}
int main()
{// 1、右值引用必须被初始化为右值int &&ri0 = 42; // 将 42 存到一个临时量,然后引用这个临时量int &&ri1 = 'a'; // 将 'a' 存到一个临时量,然后引用这个临时量int &&ri2 = 1+2; // 将 1+2 存到一个临时量,然后引用这个临时量int &&ri3 = (ri1 != 'a');int &&ri4 = (ri1 >= 'a');int &&ri5 = get100(); // 函数的非引用返回值,右值int &&ri6 = ri0>0?1:0;// 2、右值引用的内容可以被修改ri0 = 1;cout << "ri0=" << ri0 << endl;// 3、虽然没办法获取右值的地址,但可以获取右值引用的地址,并改变该地址的值int *pi = &ri0;*pi = 2;cout << "pi=" << pi << ", *pi=" << *pi << ", ri0=" << ri0 << endl;// 4、传入右值fun(1+2);return 0;
}
gpt补充:
ri0 = 1;
:
- 这是直接使用右值引用
ri0
进行赋值操作。尽管ri0
引用了一个右值临时变量,但ri0
本身是一个左值,可以直接给它赋值。- 此操作将临时变量的值直接修改为
1
,且没有解引用操作,简单直接。
*pi = 1;
:
- 这里
pi
是一个指向ri0
的指针,*pi = 1;
解引用指针来访问临时变量的值。- 从结果上看,
*pi = 1;
和ri0 = 1;
会导致相同的效果,即右值临时变量的值被修改成1
。- 但使用指针间接访问对象相对稍微多了一层解引用操作。
结论
虽然这两种操作可以达到相同的效果,但直接使用
ri0
赋值(ri0 = 1;
)在语法上更直接,性能上可能稍微优于通过指针赋值。
第二弹~ 你可以完全不看上面的解释
#include <iostream>
#include <string>void printA(const int& s)
{//const的左值引用可以接受 左值和右值std::cout << s << std::endl;
}
void printB(int&& s)
{//右值引用只能接受右值std::cout << "右值" ;std::cout << s << std::endl;}
//如果printB 名字改成printA 也就是重载,代码printB(1); 也会先执行printA(int&& s) 优先级
int main(int argc, char const *argv[])
{// demo1 说明左值引用int i = 1;int &ii = i;ii = 2; // 这是修改 不是初始化赋值std::cout << i << std::endl; // 2std::cout << ii << std::endl; // 2// 补充: 左值引用指向的地址不会变化,但是值会变化!// demo2 说明左值引用是不能用右值的 除非.....// int &a=1; 报错,右值是不能初始化赋值給左值引用的// int const &a=1; // 编译报错'const int& a' previously declared hereconst int &b = 1; // 除非是常量引用() const// b=i; b=2 常量不可修改std::cout << b << std::endl; // 1//demo3 // 左值引用在调用方法时的使用printA(i);printA(1); //demo4 右值引用在调用方法时的使用//printB(i);//报错 printB(1); return 0;
}
移动语义
本质上允许我们移动对象,而不是复制对象到其他地方
为什么有移动语义
class Person {
public:string name;int age;Person(const Person& other) { // 拷贝构造函数name = other.name;age = other.age;}
};int main() {Person p1("Alice", 30); // 创建 Person 对象Person p3=p1;//!!!这样也会调用拷贝构造函数,这是全新的对象 cout << p2.name << ", " << p2.age << endl; // 输出:Alice, 30return 0;
}
那么为什么不能直接把p1给p3 假如p1是一个临时量 ,或者是当它是一个右值的时候
移动构造和move
// g++ 14_Copy_Date.cpp -std=c++11
#include <iostream>
#include <stdio.h>
#include <string.h>using namespace std;#define MAX_NEW_MEM (64 * 1000 * 1000)class CDate
{
public:CDate(int year, int mon, int day){m_year = year;m_mon = mon;m_day = day;str = new char[MAX_NEW_MEM];sprintf(str, "%4d.%02d.%02d", year, mon, day);cout << "Calling Constructor" << ", this=" << this << endl;}// 拷贝构造函数定义CDate(const CDate &date){m_year = date.m_year;m_mon = date.m_mon;m_day = date.m_day;str = new char[MAX_NEW_MEM];memcpy(str, date.str, MAX_NEW_MEM);cout << "Calling Copy Constructor" << ", this=" << this << ", Copy Data" << endl;}//移动构造CDate(CDate&& date) noexcept //加上noexcept,用于通知标准库不抛出异常。提高性能{m_year = date.m_year;m_mon = date.m_mon;m_day = date.m_day;str = date.str;//直接指向了 省去了copydate.str = NULL;//把原来指针的指向null 防止悬挂指针cout << "Calling Move Constructor" << ", this=" << this <<endl;}// 析构函数定义~CDate(){cout << "Calling Destructor" << ", this=" << this << endl;delete[] str;}CDate operator+(int day){CDate temp = *this;temp.m_day += day;cout << "Calling operator+" << ", this=" << &temp << endl;return temp;}void show(){cout << "Date: " << m_year << "." << m_mon << "." << m_day << ", this=" << this << endl;// cout << "Date: " << str << endl;}private:int m_year;int m_mon;int m_day;char *str;
};int main()
{CDate date(2024, 06, 07);cout << endl;CDate date1 = date;cout << endl;/* Calling Constructor, this=0x5ffe70Calling Copy Constructor, this=0x5ffe50, Copy DataCalling Destructor, this=0x5ffe50Calling Destructor, this=0x5ffe70*/return 0;
}int main2()
{CDate date(2024, 06, 07);cout << endl;CDate date2 = move(date);cout << endl;/* Calling Constructor, this=0x5ffe70Calling Move Constructor, this=0x5ffe50Calling Destructor, this=0x5ffe50Calling Destructor, this=0x5ffe70*/return 0;
}