• 左值和左值引用、右值和右值引用


    1. 左值和右值

    • 左值(L-value):能用“取地址&”运算符获得对象的内存地址,表达式结束后依然存在的持久化对象。左值可以出现在等号左边也能够出现在等号右边。
    • 右值(R-value):不能用“取地址&”运算符获得对象的内存地址,表达式结束后就不再存在的临时对象。只能出现在等号右边。

       - 可以做出以下三点理解:

         1)当一个对象被用作右值的时候,用的是对象的值(内容);而被用作左值的时候,用的是对象的身份(在内存中的位置)。总之:左值看地址,右值看内容。

         2)所有的具名变量或者对象都是左值,而右值不具名,如常见的右值有非引用返回的临时变量、运算表达式产生的临时变量、原始字面量和lambda表达式等。

            很难得到左值和右值的真正定义,但是有一个可以区分左值和右值的便捷方法:看能不能对表达式取地址,如果能,则为左值,否则为右值。

         3)右值要么是字面常量,要么是在表达式求值过程中创建的对象。

         特例:因为可以用&取得字符串字面值常量的地址,虽然它不能被赋值,但它是一个左值。

    int main()
    {
        char *p = "1234";
        printf("%d
    ", p);
        printf("%d
    ", &"1234");
    }
    

       - 为什么右值不能用&取地址呢?

         1)对于临时对象,它可以存储于寄存器中,所以没办法用“取地址&”运算符;

         2)对于(非字符串)常量,它可能被编码到机器指令的“立即数”中,所以没办法用“取地址&”运算符。

    2. 左值引用和右值引用

       使用引用的目的就在于减少不必要的拷贝。

    • 左值引用:对左值的引用,就是给左值取别名。其基本语法如下:
    Type &引用名 = 左值表达式;

       - 变量名实质上是一段连续存储空间的别名,是一个标号(门牌号),通过变量的名字可以使用存储空间。

       - 对一段连续的内存空间只能取一个别名吗?

         在C++中新增加了引用的概念,引用可以看作一个已定义变量的别名,于是我们就可以通过引用为一个内存空间取多个别名。

    int main()
    {
        int a = 0;
        int &b = a;
        b = 11;
        return 0;
    }

       - 普通引用在声明时必须用其它的变量进行初始化,引用作为函数参数声明时不进行初始化。

    struct Teacher
    {
        char name[64];
        int age;
    };
    
    void printfT(Teacher *pT) { cout << pT->age << endl; }
    
    /*
     * pT是t1的别名, 相当于修改了t1
     */
    void printfT2(Teacher &pT) { pT.age = 33; }
    
    /*
     * pT和t1的是两个不同的变量,t1 copy一份数据给pT, 只会修改pT变量 ,不会修改t1变量
     */
    void printfT3(Teacher pT) { pT.age = 45; }
    
    int main()
    {
        Teacher t1;
        t1.age = 35;
        printfT(&t1);
        printfT2(t1);
        printf("t1.age:%d
    ", t1.age)   // 33
        printfT3(t1);
        printf("t1.age:%d
    ", t1.age);  //35
        return 0;
    }
    

       - 对于引用语法,C++编译器背后做了什么工作呢?

         首先我们知道引用单独定义时,必须初始化,说明它很像一个常量。又因为引用是一个内存空间的别名所以它可以取地址。

         故我们可以得到引用的本质:

         1)引用在C++中的内部实现是一个常指针Type& name <=> Type* const name

         2) C++编译器在编译过程中使用常指针作为引用的内部实现,因此引用所占用的空间大小与指针相同。

         3) 从使用的角度,引用会让人误会其只是一个别名,没有自己的存储空间。这是C++为了实用性而做出的细节隐藏。

       - 函数返回值是引用(引用当左值)

         当函数返回值为引用时,若返回栈变量,不能成为其它引用的初始值,不能作为左值使用。若返回静态变量或全局变量,

         可以成为其他引用的初始值,即可作为右值使用,也可作为左值使用。

         对于引用的理解可以直接看成指针,因为栈变量在函数结束后,内存空间就被释放了,所以这个指针指向的内容就不对了。

       - 对指针的引用

    struct Teacher
    {
        char name[64];
        int age;
    };
    
    // 指针的引用
    int getTe(Teacher* &myp)
    {
        myp = (Teacher *)malloc(sizeof(Teacher));
        myp->age = 34;
        return 0;
    }
    
    int main()
    {
        Teacher *p = NULL;
        getTe(p);
        printf("age:%d
    ", p->age);
        return 0;
    }

       - 常引用(const T &)

    int main()
    {
        int a = 10;
        int &b = a;        //普通引用
        const int &c = a;  //常量引用:只能通过c读取a的内存空间
    
        // 常量引用初始化分为两种
        // 1. 变量 初始化 常量引用
        int x = 20;
        const int& y = x;
        printf("y:%d
    ", y);
    
        // 2. 常量 初始化 常量引用
        // int &m = 10; // 引用是内存空间的别名 字面量10没有内存空间 没有方法做引用
        const int &m = 10; 
    
        return 0;
    }
    

      const引用结论

        1)Const & int e  相当于 const int * const e

        2)普通引用相当于 int *const e

        3)当使用常量(字面量)这类右值对const引用进行初始化时,C++编译器会为常量值分配空间,并将引用名作为这段空间的别名

           初始化后,将生成一个只读变量。只有常引用才可以用右值表达式初始化,这一点很重要,因为如果不加const,那么这个

           临时的对象是无法进行传递给左值引用的,比如

    MyString s = MyString("hello")    // 这个临时对象本身就存在于内存空间,所以无需为这个右值分配空间

         因为MyString("hello")是一个临时对象,即右值,所以MyString实现的拷贝构造函数参数不加const就会报错。

    • 右值引用:对右值的引用,就是给右值取别名。其基本语法如下:
    Type &&引用名 = 右值表达式;   // 如果是左值表达式,绑定就会出错。这里虽然是个右值引用,但左侧的具名变量本身是个左值

        - 开始介绍右值引用之前,先得了解到底啥是临时对象?

          在C++中创建对象是一个费时、废空间的一个操作,有些固然必不可少,但还有一些对象却在我们不知道的情况下创建了。

          1)以值的方式给函数传参

             给函数传参有两种方式----按值传递和按引用传递。按值传递时,首先将需要传给函数的参数,调用拷贝构造函数创建

             一个副本,所有在函数里的操作都是针对这个副本的,也正是因为这个原因,在函数体里对该副本进行任何操作,都不会影响原参数。

    class Test
    {
    public:
        int a, b;
    
    public:
        Test(Test& t) : a(t.a), b(t.b) { printf("Copy function!
    "); }
        Test(int m = 0,int n = 0) : a(m), b(n) { printf("Construct function!
    "); }
        virtual ~Test() {}
    
    public:
        int GetSum(Test ts)
        {
            int tmp = ts.a + ts.b;
            ts.a = 1000;           //此时修改的是tm的一个副本
            return tmp;
        }
    };
    
    int main()
    {
        Test tm(10,20);
        printf("Sum = %d 
    ",tm.GetSum(tm));
        printf("tm.a = %d 
    ",tm.a);
        return 0;
    }

             当函数执行结束后,这个临时的对象就会被销毁了。可以将 int GetSum(Test ts)改成 int GetSum(Test &ts) 来避免产生这个拷贝了。

          2)类型转换生成的临时对象

    int main()
    {
        Test tm(10,20), sum;
        sum = 1000;  // 调用 Test(int m = 0,int n = 0) 构造函数,还会调用一次赋值运算符
        printf("Sum = %d 
    ",tm.GetSum(sum));
    }
    

          3)函数返回一个对象

             当函数需要返回一个对象,他会在栈中创建一个临时对象或也叫匿名对象(如果是类对象,则会调用拷贝构造函数),存储函数的返回值。

             这个临时对象在表达式 sum = Double(tm) 结束后就自动销毁了,这个临时对象就是右值。

             按理说下面这个例子中Double函数返回时会触发拷贝构造函数,但实际运行后却没有,猜想是被编译器优化了,可以在编译时设置编译

             选项-fno-elide-constructors用来关闭返回值优化效果。

    class Test
    {
    public:
        int a;
    
    public:
        Test(Test& t) : a(t.a) { printf("Copy Construct!
    "); }
        Test(int m = 0) : a(m) { printf("Construct!
    "); }
        virtual ~Test() {};
    
    public:
        Test& operator=(const Test& t)
        {
            a = t.a;
            printf("Assignment Operator!
    ");
            return *this;
        }
    };
    
    Test Double(Test& ts)
    {
        Test tmp;
        tmp.a = ts.a * 2;
        return tmp;
    }
    
    int main()
    {
        Test tm(10), sum;
        sum = Double(tm);
        printf("sum.a = %d
    ",sum.a);
        return 0; 
    }
    

        - 引入右值引用的目的:右值引用是C++11中新增加的一个很重要的特性,它主要用来解决以下问题。

          1)函数返回临时对象造成不必要的拷贝操作通过使用右值引用,右值不会在表达式结束之后就销毁了,而是会被“续命”,

             的生命周期将会通过右值引用得以延续,和变量的声明周期一样长。

    int g_constructCount = 0;
    int g_copyConstructCount = 0;
    int g_destructCount = 0;
     
    class Test
    {
    public:
        Test() { cout << "construct: " << ++g_constructCount << endl; }
        Test(const Test& a) { cout << "copy construct: " << ++g_copyConstructCount << endl; }
        ~Test() { cout << "destruct: " << ++g_destructCount << endl; }
    };
     
    Test GetTestObj() { return Test(); }
     
    int main() 
    {
        Test a = GetTestObj();
        return 0;
    }
    
    // 上面代码关掉返回值优化后输出:
    construct: 1          // return Test()
    copy construct: 1     // 临时对象构造
    destruct: 1           // return Test()对象销毁
    copy construct: 2     // a对象构造
    destruct: 2           // 临时对象销毁
    destruct: 3           // a对象销毁
    
    //-------------------------------------------------------------------------------------------------
    
    // 但是如果使用右值引用来接收返回值呢?
    int main() 
    {
        Test &&a = GetTestObj();
        return 0;
    }
    
    // 输出如下
    construct: 1          // return Test()
    copy construct: 1     // 临时对象构造
    destruct: 1           // return Test()对象销毁
    destruct: 2           // a这个对象其实就是那个临时对象了,main结束后才销毁

             通过右值引用,比之前少了一次拷贝构造和一次析构,原因在于右值引用绑定了右值,让临时右值的生命周期延长了。

             我们可以利用这个特点做一些性能优化,即避免临时对象的拷贝构造和析构。

          2)通过右值引用传递临时参数:使用字面值(如1、3.15f、true),或者表达式等临时变量作为函数实参传递时,按左值引用传递参数会被编译器阻止。

             而进行值传递时,将产生一个和参数同等大小的副本。C++11提供了右值引用传递参数,不申请局部变量,也不会产生参数副本。

    static float  global = 1.111f;
    
    void offset(float &&f) { global += f; }   // 通过右值引用传递参数
    void offset(float& f)  { global -= f; }   // 重载了offset函数,而且是左值传递
    float getFloat() { return 4.444f; }
    
    int main()
    {
        float u = 10.000f;
        cout << "global:" << global << endl;
    
        offset(3.333f);   // 这里会调用右值引用参数的函数
        cout << "global:" << global << endl;
    
        offset(getFloat() + 2.222);
        cout << "global:" << global << endl;
    
        offset(u);        // 执行的是按左值引用的offset函数,右值引用无法初始化为左值.
        cout << "global:" << global << endl;
        return 0;
    }
    

           对于非模板函数,函数参数有确定的类型,右值引用只能与右值绑定,只接收右值实参,可以将它看作是临时变量的别名,不会将临时

             变量再复制1次,和按值传递相比提高了效率。这一点同3)进行区别。

          3)模板函数中如何按照参数的实际类型进行转发:当右值引用和模板结合的时候,就复杂了。T&&并不一定表示右值引用,它可能是个左值

             引用又可能是个右值引用。如果函数模板表示的是右值引用的话,肯定是不能传递左值的,但是事实却是可以。这里的&&是一个未定义的引用类型,

             称为universal references,它必须被初始化,它是左值引用还是右值引用却决于它的初始化,如果它被一个左值初始化,它就是一个左值引用;

             如果被一个右值初始化,它就是一个右值引用。

             注意:只有当发生自动类型推断时(如函数模板的类型自动推导,或auto关键字),&&才是一个universal references

    // Test是一个特定的类型,不需要类型推导,所以&&表示右值引用  
    template<typename T>
    class Test 
    {
      Test(Test&& rhs);
    };
    
    // 右值引用
    void f1(Test&& param);
    
    // 在调用这个f之前,这个vector<T>中的推断类型已经确定了,所以调用f函数的时候没有类型推断了,所以是右值引用
    template<typename T>
    void f2(std::vector<T>&& param); 
    
    // universal references仅仅发生在 T&& 下面,任何一点附加条件都会使之失效, 所以是右值引用
    template<typename T>
    void f3(const T&& param);
    
    // 这里T的类型需要推导,所以 && 是一个 universal references
    template<typename T>
    void f(T&& param);
    
    int main()
    {
        int x = 1;
        int && a = 2;
        string str = "hello";
        f(1);               // 参数是右值 T 推导成了int, 所以是int&& param, 右值引用
        f(x);               // 参数是左值 T 推导成了int&, 所以是int&&& param, 折叠成 int&, 左值引用
        f(a);               // 虽然 a 是右值引用,但它还是一个左值,T推导成了int&
        f(str);             // 参数是左值, T 推导成了string&
        f(string("hello")); // 参数是右值, T 推导成了string
        f(std::move(str));  // 参数是右值, T 推导成了string
    }
    

              所以最终还是要看T被推导成什么类型,如果T被推导成了string,那么T&&就是string&&,是个右值引用,如果T被推导为string&

              就会发生类似string& &&的情况,对于这种情况,c++11增加了引用折叠的规则,本质如下:

                  所有的引用折叠最终都代表一个引用,要么是左值引用,要么是右值引用。规则就是:

                  如果任一引用为左值引用,则结果为左值引用。否则(即两个都是右值引用),结果为右值引用。

              引用折叠存在四种情形,根据上面的规则我们可以知道:

                  1)左值-左值 T& &     <=>   int &

                  2)左值-右值 T& &&    <=>   int &

                  3)右值-左值 T&& &    <=>   int &

                  4)右值-右值 T&& &&   <=>   int &&

              因为1,2,3中都存在一个左值引用。

      

  • 相关阅读:
    Oracle DBLink 使用情况
    asp.net里AjaxPro简单入门教程
    AjaxPro异步调用的超时设置
    ORA-01552: 非系统表空间 'USERS' 不能使用系统回退段的处理
    andoid 监听返回键退出
    WinForm调用user32.dll实现全屏
    C# Panel 打开 Form 窗口的方法
    Android获取日期及星期的方法
    WinForm 自定义对话框 获取返回值
    52. (待补) 实现对 无头单链表 的基本操作
  • 原文地址:https://www.cnblogs.com/yanghh/p/12976780.html
Copyright © 2020-2023  润新知