文章目录

  • 一、左值和右值
    • 1.左值
    • 2.右值
    • 3.总结
  • 二、左值引用和右值引用
    • 1.左值引用
    • 2.右值引用
    • 3.对比与总结
  • 三、左值引用的使用场景及实际意义
    • 1.使用场景
    • 2.实际意义
    • 3.短板
  • 四、右值引用
    • 1.移动语义(Move semantics)
      • (1)移动构造
        • ① 概念
        • ② 移动构造有无的比较
      • (2)移动赋值
        • ① 概念
        • ② 移动赋值有无的比较
    • 2.右值引用的使用场景
    • 3.完美转发(Perfect forwarding)
      • (1)引入原因
      • (2)概念
      • (3)使用场景
    • 4.重大意义

一、左值和右值

1.左值

左值是一个表示数据的表达式,比如:变量名、解引用的指针变量。一般地,我们可以获取它的地址对它赋值,但被 const 修饰后的左值,不能给它赋值,但是仍然可以取它的地址。

总体而言,可以取地址的对象就是左值。

// 以下的a、p、*p、b都是左值
int a = 3;
int* p = &a;
*p;
const int b = 2;

2.右值

右值也是一个表示数据的表达式,比如:字面常量、表达式返回值,传值返回函数的返回值(是传值返回,而非传引用返回)右值不能出现在赋值符号的左边且不能取地址

总体而言,不可以取地址的对象就是右值。

double x = 1.3, y = 3.8;
// 以下几个都是常见的右值
10;                 // 字面常量
x + y;             // 表达式返回值
fmin(x, y);        // 传值返回函数的返回值

以下写法均不能通过编译:

  1. 10 = 4;x + y = 4;fmin(x, y) = 4;,VS2015 编译报错:error C2106: “=”: 左操作数必须为左值。原因:右值不能出现在赋值符号的左边。
  2. &10;&(x + y);&fmin(x, y);,VS2015 编译报错:error C2102: “&” 要求左值。原因:右值不能取地址。

3.总结

区分左值和右值,终究还是要看能否取地址。

二、左值引用和右值引用

传统的 C++ 语法中就存在引用语法,而 C++11标准中新增了右值引用的语法特性,因此为了区分两者,将C++11标准出现之前的引用称为左值引用。

无论左值引用还是右值引用,都是给对象取别名。

1.左值引用

左值引用就是对左值的引用,给左值取别名。

// 以下几个是对上面左值的左值引用
int& ra = a;
int*& rp = p;
int& r = *p;
const int& rb = b;

2.右值引用

右值引用就是对右值的引用,给右值取别名。

右值引用的表示是在具体的变量类型名称后加两个 &,比如:int&& rr = 4;

// 以下几个是对上面右值的右值引用
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);

注意:
右值引用引用右值,会使右值被存储到特定的位置。
也就是说,右值引用变量其实是左值,可以对它取地址和赋值(const右值引用变量可以取地址但不可以赋值,因为 const 在起作用)。
当然,取地址是指取变量空间的地址(右值是不能取地址的)。

比如:

  1. double&& rr2 = x + y;
    &rr2;
    rr2 = 9.4;
    右值引用 rr2 引用右值 x + y 后,该表达式的返回值被存储到特定的位置,不能取表达式返回值 x + y 的地址,但是可以取 rr2 的地址,也可以修改 rr2 。
  2. const double&& rr4 = x + y;
    &rr4;
    可以对 rr4 取地址,但不能修改 rr4,即写成rr4 = 5.3;会编译报错。

现在我们知道左值引用可以引用左值,右值引用可以引用右值。
那么左值引用是否可以引用右值?右值引用是否可以引用左值呢?
下面的对比与总结给出了答案。

3.对比与总结

左值引用总结:

  1. 左值引用只能引用左值,不能直接引用右值。
  2. 但是const左值引用既可以引用左值,也可以引用右值。
// 1.左值引用只能引用左值
int t = 8;
int& rt1 = t;//int& rt2 = 8;  // 编译报错,因为10是右值,不能直接引用右值// 2.但是const左值引用既可以引用左值
const int& rt3 = t;const int& rt4 = 8;  // 也可以引用右值
const double& r1 = x + y;
const double& r2 = fmin(x, y);

问:为什么const左值引用也可以引用右值?
答:在 C++11标准产生之前,是没有右值引用这个概念的,当时如果想要一个类型既能接收左值也能接收右值的话,需要用const左值引用,比如标准容器的 push_back 接口:void push_back (const T& val)
也就是说,如果const左值引用不能引用右值的话,有些接口就不好支持了。

下面就是 C++98标准中相关接口const左值引用引用右值的例子:

vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);

右值引用总结:

  1. 右值引用只能引用右值,不能直接引用左值。
  2. 但是右值引用可以引用被move的左值。

move,本文指std::move(C++11),作用是将一个左值强制转化为右值,以实现移动语义。
左值被 move 后变为右值,于是右值引用可以引用。

// 1.右值引用只能引用右值
int&& rr1 = 10;
double&& rr2 = x + y;
const double&& rr3 = x + y;int t = 10;
//int&& rrt = t;  // 编译报错,不能直接引用左值// 2.但是右值引用可以引用被move的左值
int&& rrt = std::move(t);
int*&& rr4 = std::move(p);
int&& rr5 = std::move(*p);
const int&& rr6 = std::move(b);

三、左值引用的使用场景及实际意义

1.使用场景

// 1.左值引用做参数
void func1(string s)
{...}void func2(const string& s)
{...}int main()
{string s1("Hello World!");func1(s1);  // 由于是传值传参且做的是深拷贝,代价较大func2(s1);  // 左值引用做参数减少了拷贝,提高了效率return 0;
}
// 2.左值引用做返回值(仅限于对象出了函数作用域以后还存在的情况)
string s2("hello");
// string operator+=(char ch)  传值返回存在拷贝且是深拷贝
// string& operator+=(char ch)  左值引用做返回值没有拷贝,提高了效率
s2 += '!';

2.实际意义

传值传参和传值返回都会产生拷贝,有的甚至是深拷贝,代价很大。而左值引用的实际意义在于做参数和做返回值都可以减少拷贝,从而提高效率。

3.短板

左值引用虽然较完美地解决了大部分问题,但对于有些问题仍然不能很好地解决。

当对象出了函数作用域以后仍然存在时,可以使用左值引用返回,这是没问题的。

string& operator+=(char ch)
{push_back(ch);return *this;
}

但当对象(对象是函数内的局部对象)出了函数作用域以后不存在时,就不可以使用左值引用返回了。

string operator+(const string& s, char ch)
{string ret(s);ret.push_back(ch);return ret;
}// 拿现在这个函数来举例:ret是函数内的局部对象,出了函数作用域后会被析构,即被销毁了
// 若此时再返回它的别名(左值引用),也就是再拿这个对象来用,就会出问题

于是,对于第二种情形,左值引用也无能为力,只能传值返回。

四、右值引用

于是,为了解决上述传值返回的拷贝问题,C++11标准就增加了右值引用移动语义

1.移动语义(Move semantics)

将一个对象中的资源移动到另一个对象(资源控制权的转移)。

(1)移动构造

① 概念

转移参数右值的资源来构造自己。

// 这是一个模拟string类的实现的移动构造
string(string&& s):_str(nullptr), _size(0), _capacity(0)
{swap(s);
}

拷贝构造函数和移动构造函数都是构造函数的重载函数,所不同的是:

  1. 拷贝构造函数的参数是 const左值引用,接收左值或右值;
  2. 移动构造函数的参数是右值引用,接收右值或被 move 的左值。

注:当传来的参数是右值时,虽然拷贝构造函数可以接收,但是编译器会认为移动构造函数更加匹配,就会调用移动构造函数。

总的来说,如果这两个函数都有在类内定义的话,在构造对象时:

  1. 若是左值做参数,那么就会调用拷贝构造函数,做一次拷贝(如果是像 string 这样的在堆空间上存在资源的类,那么每调用一次拷贝构造就会做一次深拷贝)。
  2. 若是右值做参数,那么就会调用移动构造,而调用移动构造就会减少拷贝(如果是像 string 这样的在堆空间上存在资源的类,那么每调用一次移动构造就会少做一次深拷贝)。

比如执行下面这几行代码:

string s("Hello World11111111111111111");
string s1 = s;  // s是左值,所以调用拷贝构造函数
string s2 = move(s);  // s被move后变为右值,所以调用移动构造函数,s的资源会被转移用来构造s2
// 要注意的是,move一般是不这样用的,因为s的资源被转走了

执行string s1 = s;前:
执行string s1 = s;后(也是执行string s2 = move(s);前):

执行string s2 = move(s);后:

② 移动构造有无的比较

比如执行语句cout << MyLib::to_string(1234) << endl;

只有拷贝构造没有移动构造:

在 to_string 函数栈帧销毁前,用局部对象 str 拷贝构造出临时对象返回到函数调用处。

既有拷贝构造也有移动构造:

在 to_string 函数栈帧销毁前,用局部对象 str (反正 str 要销毁,将 str 视为右值,直接转移 str 的资源 )移动构造出临时对象返回到函数调用处。

比如执行语句MyLib::string ret = MyLib::to_string(1234);

只有拷贝构造没有移动构造:
在 to_string 函数栈帧销毁前,先用局部对象 str 拷贝构造出临时对象返回到函数调用处,to_string 函数栈帧销毁后,再用临时对象拷贝构造出 ret 。
但现在的编译器一般都会进行优化:因为临时对象有 ret 来接收,这样的话临时对象的创建和销毁就显得多余了,不如省略掉这一步,直接用 str 拷贝构造出 ret 。

既有拷贝构造也有移动构造:
在 to_string 函数栈帧销毁前,由于局部对象 str 是左值(可以对它取地址),所以用 str 拷贝构造出临时对象返回到函数调用处,to_string 函数栈帧销毁后,由于临时对象是右值,所以用临时对象移动构造出 ret 。
但现在的编译器一般都会进行优化:因为临时对象有 ret 来接收,先拷贝构造出临时对象再用它移动构造出 ret ,临时对象好像没必要产生一样,不如省略掉。既然 str 是 to_string 函数栈帧的局部对象,最后还是要销毁,不如将 str 视为右值,直接转移 str 的资源用来构造 ret ,也就是直接用 str 移动构造出 ret 。

再比如执行下面的代码:


调用该函数后,需要传值返回这种占用很多资源的自定义类型,
在 C++98 中,没有移动构造,拷贝构造做深拷贝,花费的代价很大;
在 C++11 中,直接移动构造,转移 m 的资源给 ret ,提高了效率。

(2)移动赋值

① 概念

转移参数右值的资源来赋给自己。

// 这是一个模拟string类的实现的移动赋值
string& operator=(string&& s)
{swap(s);return *this;
}

拷贝赋值函数和移动赋值函数都是赋值运算符重载函数的重载函数,所不同的是:

  1. 拷贝赋值函数的参数是 const左值引用,接收左值或右值;
  2. 移动赋值函数的参数是右值引用,接收右值或被 move 的左值。

注:当传来的参数是右值时,虽然拷贝赋值函数可以接收,但是编译器会认为移动赋值函数更加匹配,就会调用移动赋值函数。

总的来说,如果这两个函数都有在类内定义的话,在进行对象的赋值时:

  1. 若是左值做参数,那么就会调用拷贝赋值,做一次拷贝(如果是像 string 这样的在堆空间上存在资源的类,那么每调用一次拷贝赋值就会做一次深拷贝)。
  2. 若是右值做参数,那么就会调用移动赋值,而调用移动赋值就会减少拷贝(如果是像 string 这样的在堆空间上存在资源的类,那么每调用一次移动赋值就会少做一次深拷贝)。

比如下面这几行代码:

string s("11111111111111111");
string s1("22222222222222222");
s1 = s;  // s是左值,所以调用拷贝赋值函数string s2("333333333333333333");
s2 = std::move(s);  // s被move后变为右值,所以调用移动赋值函数,s的资源会被转移用来赋给s2
// 要注意的是,move一般是不这样用的,因为s的资源被转走了

② 移动赋值有无的比较

比如执行下面的语句:
MyLib::string ret("111111111111111111111111");
ret = MyLib::to_string(12345);

没有移动赋值(有移动构造和拷贝赋值):

用 str(编译器视 str 为右值)移动构造出临时对象作为返回值,再用临时对象拷贝赋值给 ret 。

有移动赋值:

用 str(编译器视 str 为右值)移动构造出临时对象作为返回值,由于临时对象是右值,再用临时对象移动赋值给 ret 。

2.右值引用的使用场景

除了上面的使用场景之外,C++11标准的STL 容器的相关接口函数也增加了右值引用版本。

比如:

3.完美转发(Perfect forwarding)

(1)引入原因

在此之前我们需要知道什么是万能引用:

确定类型的 && 表示右值引用(比如:int&& ,string&&),
但函数模板中的 && 不表示右值引用,而是万能引用,模板类型必须通过推断才能确定,其接收左值后会被推导为左值引用,接收右值后会被推导为右值引用

注意区分右值引用和万能引用:下面的函数的 T&& 并不是万能引用,因为 T 的类型在模板实例化时已经确定。

template<typename T>
class A
{void func(T&& t);  // 模板实例化时T的类型已经确定,调用函数时T是一个确定类型,所以这里是右值引用
};

让我们通过下面的程序来认识万能引用:

template<typename T>
void f(T&& t)  // 万能引用
{//...
}int main()
{int a = 5;  // 左值f(a);  // 传参后万能引用被推导为左值引用const string s("hello");  // const左值f(s);  // 传参后万能引用被推导为const左值引用f(to_string(1234));  // to_string函数会返回一个string临时对象,是右值,传参后万能引用被推导为右值引用const double d = 1.1;f(std::move(d));  // const左值被move后变成const右值,传参后万能引用被推导为const右值引用return 0;
}

在调试下开监视窗口可看到传参后参数 t 的类型:

于是我们会用万能引用去做一些有意义的事,比如下面的代码:

void Func(int& x) {  cout << "左值引用" << endl; }void Func(const int& x) { cout << "const左值引用" << endl; }void Func(int&& x) { cout << "右值引用" << endl; }void Func(const int&& x) { cout << "const右值引用" << endl; }template<typename T>
void f(T&& t)  // 万能引用
{Func(t);  // 根据参数t的类型去匹配合适的重载函数
}int main()
{int a = 4;  // 左值f(a);const int b = 8;  // const左值f(b);f(10); // 10是右值const int c = 13;f(std::move(c));  // const左值被move后变成const右值return 0;
}

运行程序后,我们本以为打印的结果是:
左值引用
const左值引用
右值引用
const右值引用

但实际的结果却是:

后两行的运行结果跟我们预想的不一样。

那么这是怎么一回事呢?
其实在本文的前面已经讲过了,右值引用变量其实是左值,所以就有了上面的运行结果。

具体解释:

  1. f(10);
    10是右值,传参后万能引用被推导为右值引用,但该右值引用变量其实是左值,因此实际调用的函数是void Func(int& x)
  2. f(std::move(c));
    const左值被move后变成const右值,传参后万能引用被推导为const右值引用,但该const右值引用变量其实是const左值,因此实际调用的函数是void Func(const int& x)

也就是说,右值引用失去了右值的属性

但我们希望的是,在传递过程中能够保持住它的原有的左值或右值属性,于是 C++11标准提出完美转发。

(2)概念

完美转发是指在函数模板中,完全依照模板的参数类型,将参数传递给当前函数模板中的另外一个函数。

因此,为了实现完美转发,除了使用万能引用之外,我们还要用到std::forward(C++11),它在传参的过程中保留对象的原生类型属性

这样右值引用在传递过程中就能够保持右值的属性。

void Func(int& x) { cout << "左值引用" << endl; }void Func(const int& x) { cout << "const左值引用" << endl; }void Func(int&& x) { cout << "右值引用" << endl; }void Func(const int&& x) { cout << "const右值引用" << endl; }template<typename T>
void PerfectForward(T&& t)  // 万能引用
{Func(std::forward<T>(t));  // 根据参数t的类型去匹配合适的重载函数
}int main()
{int a = 4;  // 左值PerfectForward(a);const int b = 8;  // const左值PerfectForward(b);PerfectForward(10); // 10是右值const int c = 13;PerfectForward(std::move(c));  // const左值被move后变成const右值return 0;
}

运行结果如下:

实现完美转发需要用到万能引用和 std::forward 。

(3)使用场景

除了上面的使用场景之外,C++11标准的 STL 容器的相关接口函数也实现了完美转发,这样就能够真正实现右值引用的价值。

比如 STL 库中的容器 list :

上面四个接口函数都调用 _Insert 函数,_Insert 函数模板实现了完美转发。

再比如自己模拟实现的 list(这里只写出主要部分):

template<class T>
struct ListNode
{ListNode* _next = nullptr;ListNode* _prev = nullptr;T _data;
};template<class T>
class List
{typedef ListNode<T> Node;
public:List(){_head = new Node;_head->_next = _head;_head->_prev = _head;}void PushBack(const T& x)  // 左值引用{Insert(_head, x);}void PushFront(const T& x)  // 左值引用{Insert(_head->_next, x);}void PushBack(T&& x)  // 右值引用{Insert(_head, std::forward<T>(x));  // 关键位置:保留对象的原生类型属性}void PushFront(T&& x)  // 右值引用{Insert(_head->_next, std::forward<T>(x));  // 关键位置:保留对象的原生类型属性}template<class TPL>    // 该函数模板实现了完美转发void Insert(Node* pos, TPL&& x)  // 万能引用{Node* prev = pos->_prev;Node* newnode = new Node;newnode->_data = std::forward<TPL>(x);  // 关键位置:保留对象的原生类型属性// prev newnode posprev->_next = newnode;newnode->_prev = prev;newnode->_next = pos;pos->_prev = newnode;}private:Node* _head;
};

只要是右值引用,由当前函数再传递给其它函数调用,要保持右值属性,必须实现完美转发。

4.重大意义

右值引用(及其支持的移动语义和完美转发)是 C++11 中加入的最重要的新特性之一,它使得 C++ 程序的运行更加高效。

详解 C++ 左值、右值、左值引用以及右值引用相关推荐

  1. R语言生存分析详解:KM曲线、COX比例风险模型、HR值解读、模型比较、残差分析、是否比例风险验证:基于survival包lung数据集

    R语言生存分析详解:KM曲线.COX比例风险模型.HR值解读.模型比较.残差分析.是否比例风险验证:基于survival包lung数据集 目录

  2. smali语言详解之一般/构造方法(函数)的声明与返回值关键字

    smali语言详解之一般/构造方法(函数)的声明与返回值关键字 一. smali语言的方法声明格式 .method与.end method成对出现,类似于java中的花括号 1.1.非静态的一般方法 ...

  3. 详解SQL的四种连接-左外连接、右外连接、内连接、全连接

    1.内联接(典型的联接运算,使用像 =  或 <> 之类的比较运算符).包括相等联接和自然联接.      内联接使用比较运算符根据每个表共有的列的值匹配两个表中的行.例如,检索 stud ...

  4. 详解自监督发展趋势! 何恺明连获三年CVPR最高引用的秘诀是?

    点击文末公众号卡片,不错过计算机会议投稿信息 0 引言 许多加了我好友的读者知道尼谟之前的研究方向是"自监督学习",而最近我无意中发现,CVPR最近三年引用量最高的论文竟然都是来自 ...

  5. C++左值、右值、左值引用、右值引用的详解

    目录 一.概述 1.左值和右值 2.引用 3.左值引用和右值引用 3.1 左值引用 3.2 右值引用 二.左值引用与右值引用详解 1.左值引用详解 2.右值引用详解 一.概述 1.左值和右值 左值是可 ...

  6. windows批处理(cmd/bat脚本)编程详解

    cmd文件和bat文件的区别:二者本质上没有区别,都是简单的文本编码方式,都可以用记事本创建.编辑和查看.两者所用的命令行代码也是共用的,只是cmd文件中允许使用的命令要比bat文件多.cmd文件只有 ...

  7. Python基础语法day02字符串详解和列表

    今天是python基础语法入门第二天,大概总共会有四天左右.四天后就是对于python的numpy库的详细文章.那话不多说,我们开始. 目录 字符串详解 字符串独有功能 检测头尾字符 判断输入结果是否 ...

  8. 常见字典用法集锦及代码详解

    目录 前言 字典的简介 1. 字典对象 1.1 Add 方法 1.2 Exists 方法 1.3 Keys 方法 1.4 Items 方法 1.5 Remove 方法 1.6 RemoveAll 方法 ...

  9. Java集合详解之Map

    一.首先看看集合框架体系图 从图中可以看到,Map接口扩展了Iterator接口,关于Iterator接口详解请移步:Iterator接口详解 二.Map是什么? Map<k,v>使用键值 ...

  10. Java类的加载过程详解 面试高频!!!值得收藏!!!

    受多种情况的影响,又开始看JVM 方面的知识. 1.Java 实在过于内卷,没法不往深了学. 2.面试题问的多,被迫学习. 3.纯粹的好奇. 很喜欢一句话: 八小时内谋生活,八小时外谋发展. 望别日与 ...

最新文章

  1. CUDA Samples: 获取设备属性信息
  2. 滴滴自动驾驶获得江苏省苏州公安局颁发的路测牌照
  3. 英伟达再出GAN神作!多层次特征的风格迁移人脸生成器
  4. vagrant安装及使用方法
  5. HTML中的meta(转载)
  6. 【笔记】mybatis的sqlSession和Mapper详解
  7. 如何使用Visual Studio创建SQL Server数据库项目
  8. servlet,listener,filter,interceptor的关系
  9. 通讯录标准化输入fread c语言,C语言实现通讯录系统
  10. MCGS组态屏CRC16(Modbus)校验计算脚本
  11. 【华为机试真题 JAVA】最大股票收益-100
  12. python基础教程虎课-想配一台PS修图的电脑,请大家推荐配置? | ps修图基础
  13. GDI+ 中图片的绘制
  14. kvm介绍、kvm存储池、kvm快照和克隆、kvm虚拟机基本管理和网络管理
  15. 磁盘阵列(RAID)级别的简单介绍
  16. 手把手带你搞定OPENSTACK
  17. 仿新浪微博2014之登陆界面四(总体功能)
  18. VGGNet网络详解与模型搭建
  19. 【编译原理】中间代码优化(二) 局部优化
  20. excel组合汇总_Excel汇总20160222

热门文章

  1. win7中IIS配置添加网站,局域网访问不到··
  2. 毕业设计-基于Springboot实现图书管理系统
  3. 小白用c++画出手枪,写代码时实属不易,请点赞加关注吧!
  4. 快手发现页视频采集问题
  5. A Unified Objective for Novel Class Discovery
  6. 一只青蛙一次可以跳上1级台阶,也可以跳上2级,也可以跳上3级 请问跳100级有多少种方法?
  7. YYCache 设计分析
  8. 【面向对象项目】基于【Java】实战的飞机大战(一)
  9. Day 21-30 :Web前端概述
  10. Python Flask,Flask-WTF,表单验证,CSRF验证