1、重新理解左值与右值
  • 最初理解:
    • 左值:就是赋值语句=左边的内容(它代表一个地址),例如:int a = 10;a就是一个左值,它有一个地址
    • 右值:赋值语句右边的内容(一般来说没有地址),例如常数值1、2、3.14…很明显这个不能放在左侧,因为没有地址!左值不仅有左值属性还有右值属性
    • 但是左值不仅有左值属性还有右值属性,因为还可以把左值当右值用,例如:int b = a,此时其实是用了a里面的内容10(右值属性)给b进行赋值
void test1()
{
    int a = 1;
//    int 1 = a;                        //  右值无法当左值用
    int b = a;
    cout << &a << " " << &b << " " <<endl;
//    cout << &1 << endl;               //  报错, 右值无法获取地址
}
  • 其实可以发现一个值是左值还是右值其实还是很难界定的,左值可以当右值用,但是右值不能当左值用
  • 新的理解:判断一个值是左值还是右值,分析一下这个值能不能取地址,能取地址就是一个左值,反之就是右值。
    • 左值:一个有名字的房子(可以获取地址);其左值属性就是指房子的名字(变量名称),右值属性就是房子里住的人(变量的值)。
    • 右值:一个没有名字的危房(无法获取地址),一般临时的值是一个右值
      在这里插入图片描述
1.1、举例分析(一)
void test2()
{
    int a = 1;
//    a = 10 = 20;                  // 报错
//    a = (10 = 20);                // 报错
    (a = 10) = 20;
    cout << a << endl;
}
  • a = 10 = 20 和 a = (10 = 20):根据运算符优先级都是从右往左开始赋值的,先执行10 = 20,然后再执行 a = 20(如果10 = 20成功)。
    • 10是一个右值,所以无法在等号左边当左值属性使用进行赋值操作
  • (a = 10) = 20:先执行a = 10,在执行a = 20;因为a 是一个左值(有地址有名字),可以当左值也可以当右值使用。
1.2、举例分析(二)
  • a、b都是左值,可以当右值使用,根据运算符优先级分析即可
void test3()
{
    int a, b;
    a = 1; b = 2;
    a = (b = 20);
    cout << a << " " << b << endl;              // 20 20
    a = 1; b = 2;
    a = b = 20;
    cout << a << " " << b << endl;              // 20 20
    a = 1; b = 2;
    (a = b) = 30;
    cout << a << " " << b << endl;              // 30 2
}
1.3、举例(三)
  • 一般来说除了简单字面量是右值,字符串也是一个左值,它比较特殊准确来说它应该属于常量区的东西,同一个字符串常量到处取地址都是一样的
  • 可以通过地址可以看到字符串和全局变量所在的区域非常接近都是0x55f719开头,而全局变量在全局区,因此可以大胆推测字符串也是在全局区。
int global_a = 100;
void test4()
{
    cout << "global_a: " << &global_a << endl;          //  global_a: 0x55f719fe8010
    cout << &"123456" << endl;          //  0x55f719de6ed7
    cout << &"123456" << endl;          //  0x55f719de6ed7
}
int main()
{
    cout << "main: " << &"123456" << endl;          // main: 0x55f719de6ed7
    return 0;
}
2、左值引用
  • 引用:一般认为引用就是左值引用,但实际上还有一个右值引用(下面讨论)。

  • 引用的核心:引用的核心是一个类型 * const的指针(int *const)

  • 具体是指针常量还是常量指针感觉很难记住,我一般看是指针先还是const先,再决定组合叫法。const 在*前面是一种类型,const 在 * 后面又是一种类型。

  • 左值引用只能引用左值,无法引用右值(右值没有地址)

2.1、左值引用的本质
  • *类型& 实际上等价于 类型 const,例如下面的例子int *const c = &a ===> int&c = a;
  • c存的是a的地址,&c取得是c自己的地址,*c取得是a地址中的值。
  • 下面的例子中:a、b、c操作着同一个值,一个发生改变大家都发生改变
void test_reference1()
{
    int a = 10;
    int& b = a;
    int *const c = &a;					// c实际上等价int& c = a;
    cout << "&a = " << &a << ", a = " << a << endl;
    cout << "&b = " << &b << ", b = " << b << endl;
    cout << "c = " << c << ", &c = " << &c << ", *c = " << *c << endl;
    b = 100;
    cout << "&a = " << &a << ", a = " << a << endl;
    cout << "&b = " << &b << ", b = " << b << endl;
    cout << "c = " << c << ", &c = " << &c << ", *c = " << *c << endl;
    *c = 200;
    cout << "&a = " << &a << ", a = " << a << endl;
    cout << "&b = " << &b << ", b = " << b << endl;
    cout << "c = " << c << ", &c = " << &c << ", *c = " << *c << endl;
}
/*		输出
&a = 0x7fff8325d534, a = 10
&b = 0x7fff8325d534, b = 10
c = 0x7fff8325d534, &c = 0x7fff8325d538, *c = 10
&a = 0x7fff8325d534, a = 100
&b = 0x7fff8325d534, b = 100
c = 0x7fff8325d534, &c = 0x7fff8325d538, *c = 100
&a = 0x7fff8325d534, a = 200
&b = 0x7fff8325d534, b = 200
c = 0x7fff8325d534, &c = 0x7fff8325d538, *c = 200
*/

在这里插入图片描述

  • *int const c = a表示c这辈子就指向a,不能修改这个执行(红色虚线无法修改),但是可以修改指向地址的值。
  • *int&b = a也是一样,因为int&b 本质上和 int const 没有区别,指向不能修改但是值可以修改。
2.2、左引用的定义
  • 另外还有引用初始化的问题,引用只能在定义的时候初始化(定义的时候必须指定一个类型)。
  • 定义完毕无法初始化和修改指向,虽然无法修改指向但是可以修改值
  • b = y:实际上是将y的右值给了b当右值,不是把y的左值给了b当右值。
void test_reference2()
{
    int x = 1, y = 2;
    int&b = x;									
    cout << "&x = " << &x << ", x = " << x << endl;
    cout << "&y = " << &y << ", y = " << y << endl;
    cout << "&b = " << &b << ", b = " << b << endl;
    b = y;
    cout << "&x = " << &x << ", x = " << x << endl;
    cout << "&y = " << &y << ", y = " << y << endl;
    cout << "&b = " << &b << ", b = " << b << endl;
}
/*		输出
&x = 0x7fff5fe3f3c8, x = 1
&y = 0x7fff5fe3f3cc, y = 2
&b = 0x7fff5fe3f3c8, b = 1
&x = 0x7fff5fe3f3c8, x = 2
&y = 0x7fff5fe3f3cc, y = 2
&b = 0x7fff5fe3f3c8, b = 2
*/
2.3、左引用的使用
  • 左引用只能引用一个有地址的东西,无法引用一个没有地址或者地址不清的临时变量(无法引用右值)
void test_reference3()
{
    int a = 10;
    int b = a;
    int& c = a;
//    int& d = 1;               // 报错,因为引用只能引用一个有地址的东西
    cout << "&a = " << &a << ", a = " << a << endl;
    cout << "&b = " << &b << ", b = " << b << endl;
    cout << "&c = " << &c << ", c = " << c << endl;
}
2.4、左引用传递参数
  • 当使用左引用作为参数列表时,必须传入一个有实际地址的左值,无法直接使用右值
void test_reference4()
{
    auto f_val = [](int a){
        cout << "f_val(), a = " << a << ", &a = " << &a << endl;
    };
    int a = 3;
    cout << "test_reference4(), a = " << a << ", &a = " << &a << endl;
    f_val(a);
    f_val(1);

    auto f_ref = [](int& a){
        cout << "f_ref(), a = " << a << ", &a = " << &a << endl;
    };
    f_ref(a);
//    f_ref(1);                //报错,左引用必须指向一个有实际地址的东西
}
/*
test_reference4(), a = 3, &a = 0x7ffdad291404
f_val(), a = 3, &a = 0x7ffdad2913e4
f_val(), a = 1, &a = 0x7ffdad2913e4				// 这里地址与上面相同可能是地址的重复使用
f_ref(), a = 3, &a = 0x7ffdad291404
*/
2.5、常量左引用
  • 左引用的本质是一个指针常量,表示引用不能在修改其指向,但是可以修改值。
  • 而当左引用 + const之后,原先可以修改值的操作也被禁止掉了,当传入参数并且不希望函数在内部修改这个变量时会再引用前面在加一个const
  • 例如下面:const int * const <==> const int& y,此时指向和指向地址的值都不允许修改。
void test_reference5()
{
    int v_a = 10;
    int& b = v_a;
    int *const c = &v_a;
    
    int v_b = 22;
    const int *const x = &v_b;
    const int& y = v_b;
//    x = 100;    y = 200;            // 报错
    v_b = 100;              // 可以
}
3、右值引用
  • 到这里就要开始打破引用的一些固定化的思想了,引用开始分左值引用和右值引用。
  • 右值引用:必须指向右值(没有地址)的引用,很tmd的抽象。右值引用一般绑定到那些即将销毁或者临时的对象上。
3.1、右值引用的定义
  • 左值引用不是引用不了一个临时变量值吗,那右值引用来引用临时变量(东厂管不了的西厂管)
  • 右值引用却无法引用一个存在地址的左值(有实际地址)
void test_right_reference1()
{
    int&& x = 1;
//    int& y = 2;             // 报错

    int t = 10;
    int& a = t;           // 报错
//    int&& b = t;           // 报错
    cout << "&x = " << &x << ", x = " << x << endl;
    cout << "&a = " << &a << ", a = " << a << endl;
    cout << "&t = " << &t << ", t = " << t << endl;
}
/*		输出
&x = 0x7ffc2a1c8a64, x = 1
&a = 0x7ffc2a1c8a60, a = 10
&t = 0x7ffc2a1c8a60, t = 10
*/
3.2、右值引用的本质
  • 上面的例子可以看到右值引用实际上也是一个左值,它有变量名字也有地址(左值属性)也有值(右值属性)。

  • 那么左值引用就可以引用一个右值引用了,emmm…很抽象,很抽象

void test_right_reference2()
{
    int&& a = 1;
    int& b = a;
    int *p = &a;
    cout << "&a = " << &a << ", a = " << a << endl;
    cout << "&b = " << &b << ", b = " << b << endl;
    cout << " p = " <<  p << ",*p = " << *p << endl;

    cout << "------------b = 5;------------" << endl;
    b = 5;
    cout << "&a = " << &a << ", a = " << a << endl;
    cout << "&b = " << &b << ", b = " << b << endl;
    cout << " p = " <<  p << ",*p = " << *p << endl;
}
/*
&a = 0x7ffddb8f0a2c, a = 1
&b = 0x7ffddb8f0a2c, b = 1
 p = 0x7ffddb8f0a2c,*p = 1
------------b = 5;------------
&a = 0x7ffddb8f0a2c, a = 5
&b = 0x7ffddb8f0a2c, b = 5
 p = 0x7ffddb8f0a2c,*p = 5
*/

实际上这样我们是看不出来什么的,为什么呢?因为没有结合到实际情况来说,当把右值引用介入到完美转发和移动语义中去就能看出来了,这样看完全可以把

int &&a = 1; <==> int a = 1; 所展示出来的效果就是这个样子。

3.3、右值引用的参数传递
  • 可以看到f_right_ref和f_left_ref表达式对于传入左值和右值有着不同的强烈反应。
  • 右值引用可以传入右值(临时变量),但是不能传递一个左值(有地址)
  • 左值引用可以传入左值(有地址),但是却不能传入一个右值(临时变量)
void test_right_reference3()
{
    auto f_right_ref = [](int&& a) -> void{
        cout << "f_right_ref():: &a = " << &a << ", a = " << a << endl;
    };
    auto f_left_ref = [](int& a) -> void{
        cout << "f_right_ref():: &a = " << &a << ", a = " << a << endl;
    };
    int a = 10;
    cout << "test_right_reference3():: &a = " << &a << ", a = " << a << endl;
    f_left_ref(a);
//    f_right_ref(a);         // 报错
//    f_left_ref(1);          // 报错
    f_right_ref(1);
}
3.4、右值引用与const
  • 右值引用加const实际上就是一个普通变量加const,例如:const int&& a = 1 <==> const int a = 1; 很明显结论就是a值不能在修改了
  • 而如果同类型一个变量需要使用const右值引用变量那么也需要加const,如下代码:
    • const int& c = a; // 必须要const, 实际上引用带的那个const 多余了,它只是说值不能修改,没说指向不能改
    • const int *p = &a; // 必须const引住表示当前指针指向地址的值不能修改
    • *p = 100;报错,但是p = &b;不报错啊,指向可以继续修改。
void test_right_reference4() {
    const int &&a = 1;
//    a = 100;          // 报错
    int b = a;          // 使用a的右值1给b当右值
    cout << "&a = " << &a << ", a = " << a << endl;
    cout << "&b = " << &b << ", b = " << b << endl;

    b = 123;
    const int& c = a;   // 必须要const, 实际上引用带的那个const 多余了,它只是说值不能修改,没说指向不能改
    const int *p = &a;  // 必须const引住表示当前指针指向地址的值不能修改
//    *p = 100;           // 报错
    p = &b;
    cout << "&a = " << &a << ", a = " << a << endl;
    cout << "&b = " << &b << ", b = " << b << endl;
    cout << " p = " << p << ", *p = " << *p << endl;
}
/*
&a = 0x7ffcc9f7c7bc, a = 1
&b = 0x7ffcc9f7c7b8, b = 1
&a = 0x7ffcc9f7c7bc, a = 1
&b = 0x7ffcc9f7c7b8, b = 123
 p = 0x7ffcc9f7c7b8, *p = 123
*/
4、总结
  • 区分一个东西是左值还是右值最简单的方法就是查看当前这个值是否可以取地址,能取地址的意味着这个东西是一个左值,如果否则这个东西就是右值。
  • 对于左值引用需要注意引用的东西必须是一个存在地址的左值
  • 对于右值引用需要注意引用的东西必须是一个临时变量(不存在地址的东西),很奇葩就喜欢奇怪的。
  • 无论是左值引用还是右值引用,当与const关键字搭配使用时需要注意不可修改的是值还是地址
  • 当指针指向const的引用时也需要注意能修改的是值还是指向地址。

只有理解好了左值、右值、左值引用、右值引用、常引用才能帮助我们更加轻松容易的理解移动语义、完美转发等特性。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐