zoukankan      html  css  js  c++  java
  • 再谈c++中的引用

    《从汇编看c++的引用和指针》一文中,虽然谈到了引用,但是只是为了将两者进行比较。这里将对引用做进一步的分析。

    1 引用的实现方式

    在介绍有关引用的c++书中,很多都说引用只是其引用变量的一个别名。我自己不是很喜欢这种解释,因为觉得这种解释会给人误解,好像引用和变量就是一回事,而且,书中也没有给出,为什么引用就是一个别名的原因,大都只是给出一个例子,证明引用可以改变传递给函数的实际参数的值,从而说明,用引用传参,和传值不是一回事。这里,我将介绍一下自己分析的引用实现方式。下面先来看c++源码:

    int main() {
        int i = 1;
        int j = 2;
        int& ri = i; //指向i的引用
        int* pj = &j; //指向j的指针
        
        //用指针和引用的方式分别对i和j加1
        ri++;  
        (*pj)++;
    }

    下面来看一下main函数的汇编码:

    int main() {
    
        push    ebp
        mov    ebp, esp
        sub    esp, 16                    ; 00000010H
    
    ; 2    :     int i = 1;
    
        mov    DWORD PTR _i$[ebp], 1 ;将1写入_i$[ebp]代表的内存,即给变量i赋值1
    
    ; 3    :     int j = 2;
    
        mov    DWORD PTR _j$[ebp], 2;将2写入_j$[ebp]代表的内存,即给变量j赋值2
    
    ; 4    :     int& ri = i;
    
        lea    eax, DWORD PTR _i$[ebp];获得_i$[ebp]所带表的内存地址,写入寄存器eax
        mov    DWORD PTR _ri$[ebp], eax;将eax写入_ri$[ebp]所带表的内存,这里,完成了引用的初始化
                                    ;可以看到,虽然我们使用变量i来初始化引用,但是,编译器仍然保存的是变量i的地址
    
    ; 5    :     int* pj = &j;
    
        lea    ecx, DWORD PTR _j$[ebp]; 将_j$[ebp]所代表的内存地址给寄存器ecx
        mov    DWORD PTR _pj$[ebp], ecx;将ecx里面的值写入_pj$[ebp]所代表的的内存,这里完成了指针变量的初试化,可以看到
                                    ;指针变量pj确实保存的是变量j的地址
    
    ; 6    :     
    ; 7    :     ri++;
    
        mov    edx, DWORD PTR _ri$[ebp];将_ri$[ebp]所代表的内存的内容(即变量i的地址)写入寄存器edx
        mov    eax, DWORD PTR [edx];将edx的值指向的内存(即存储i的内存)的内容(即i的值1)写入寄存器eax
        add    eax, 1 ;将eax里面的值加1,结果存入eax寄存器
        mov    ecx, DWORD PTR _ri$[ebp];将_ri$[ebp]所代表的内存的内容(即变量i的地址)写入寄存器ecx
        mov    DWORD PTR [ecx], eax;将eax的值写入ecx的值指向的内存(即变量i所在的内存)
    
    ; 8    ; 8    :     (*pj)++;
    
        mov    edx, DWORD PTR _pj$[ebp];将_pj$[ebp]所代表的内存的内容(即变量j所在内存的地址)写入寄存器edx
        mov    eax, DWORD PTR [edx];将edx的值指向的内存(即变量j所在的内存)的内容(即变量j的值2)写入寄存器eax
        add    eax, 1;将eax里面的值加1,结果存到寄存器eax里面
        mov    ecx, DWORD PTR _pj$[ebp];将_pj$[ebp]所代表的内存内容(即变量j所在的内存的地址)写入寄存器ecx
        mov    DWORD PTR [ecx], eax;将eax的值写入ecx的值指向的内存(即变量j所在的内存)
    
    ; 9    : }

    从上边初始化指针和引用的汇编码可以看出:两者都是保存的变量的地址,虽然初始化指针的时候明确使用了&运算符,但是初始化引用的时候,编译器仍然保存的是变量i的地址。

    而在用指针和引用操作各自所指的变量的时候,对于引用,虽然没有使用*运算符,通过保存的地址获取变量的对应值,但是,编译器已经帮我们做了,可以把对引用和指针的操作的汇编码进行比较,可以发现,二者是一样的。

    因此,对于指针,pj它保存的是变量j的地址,但是如果我们要通过指针变量pj获取j的指针,必须自己用*运算符做这种寻址操作;但是,但我们是用引用的时候,ri虽然也保存的是变量i,但是获取变量i的值的时候不必使用*运算符来寻址,因为编译器已经帮我们做了,正因为省略了用*运算符寻址的过程,使得引用使用起来更像普通变量一样,也就是说使用引用ri和使用变量i的效果是一样的。我觉得,这就是把引用称为变量的别名的原因。

    作为一种特殊的类型,引用也有它自己的特点:

    1 必须初始化

    2 不能指向NULL

    3 一旦初始化了引用,这个引用就一直引用某一变量,无法改变。比如,ri只能是i的引用,不能通过ri=j使其引用变量j,这条指令只是将ri引用的变量i赋值为j的值,ri仍然引用的是i,也就是说保存的仍然是变量i的地址,即给引用赋值,只能在初试化的时候。

    4 永远也无法获取引用的地址,即无法通过&ri获取ri的地址,这样获取的都是变量i的地址

    5 没有引用类型的数组,即,不能声明int& a[3];

    2 传引用与返回引用

    下面来看引用作为参数和返回值的情况:

    c++源码如下:

    //该函数传递引用也返回引用
    int& f(int& i) {
        i++;
        return i;
    }
    
    int main() {
        int i = 1;
        int j = f(i);
    }

    下面是main函数对应的汇编码:

     7    : int main() {
    
        push    ebp
        mov    ebp, esp
        sub    esp, 8
    
    ; 8    :     int i = 1;
    
        mov    DWORD PTR _i$[ebp], 1;将1写入_i$[ebp]所代表的内存,即为变量i赋值1
    
    ; 9    :     int j = f(i);
    
        lea    eax, DWORD PTR _i$[ebp];将_i$[ebp]所代表的内存(即变量i所在内存)给寄存器eax
        push    eax;将eax的值压栈,作为参数传递给函数f
        call    ?f@@YAAAHAAH@Z                ; 调用函数f
        add    esp, 4;这条指令的作用是将栈顶指针下移4byte,目的是为了释放传递参数所分配的栈空间
        mov    ecx, DWORD PTR [eax];将eax的值所指向的内存(即变量i所在的内存)的内容(即变量i的值)给寄存器ecx
        mov    DWORD PTR _j$[ebp], ecx;将ecx的值写入_j$[ebp]所代表的的内存(即变量j所在内存),初始化变量j
    
    ; 10   : }

    可以看到,传递给函数f的参数时实际参数变量i的地址,这和上面讲的ri=i一样,只不过这里使用实参初始化形参引用。

    下面是函数f的汇编码:

    ; 2    : int& f(int& i) {
    
        push    ebp
        mov    ebp, esp
    
    ; 3    :     i++;
    
        mov    eax, DWORD PTR _i$[ebp]; 将_i$[ebp]所代表的的内存内容(即传进来的参数,也就是变量i的地址)给寄存器eax
        mov    ecx, DWORD PTR [eax];将eax的值所指向的内存(即变量i所在内存)的内容(即变量i的值)给寄存器ecx
        add    ecx, 1;将ecx的值加1
        mov    edx, DWORD PTR _i$[ebp];将_i$[ebp]所代表的内存内容(即传进来的参数,也就是变量i的地址)给寄存器edx
        mov    DWORD PTR [edx], ecx;将ecx的值给edx的值所指向的内存(即变量i所在的内存)
    
    ; 4    :     return i;
    
        mov    eax, DWORD PTR _i$[ebp];将_i$[ebp]所代表的内存内容(即传进来的参数,也就是变量i的地址)给寄存器eax,作为返回值
    
    ; 5    : }

    可以看到,函数f返回的值也是一个地址,即实参i的地址。

    从上面可以总结,当引用作为参数和返回值的时候,实际上也是传递和返回的地址

    3 临时变量和引用

    当用引用作为参数时,可能产生临时变量,然后用临时变量的地址初始化引用参数,但是产生临时变量有条件:

    c++源码如下:

    void f(int& i) {}
    
    int main() {
        int i;
        f(i + 1);
    }

    编译之后,就会产生如下错误:

    出现这种错误,是因为要调用函数f,必须为其传递一个地址,但是(i + 1)并不是一个变量,也就是说,并没有为(i + 1)生成临时变量,无法获取地址,它只是一个非左值。下面就来看一下生成临时变量的条件:

    在引用参数为const的情况下:

    1 实参的类型正确,但是不是左值(上面的类型就是这种情况)

    2 实参的类型不正确,但是可以转换成正确的类型

    所谓左值,就是可以被引用的数据对象,常规变量(可以修改的左值)和const变量(不可以修改的左值)都可以看成左值,而非左值,包括字面常量(比如单纯的数字1,但用引号括起来的字符串除外,他们有地址表示)和包含多项的表达式,他们不能被取地址,也不允许赋值。

    因此,如果把上述函数f的参数类型改成const int&就可以通过编译,因为这时候会为(i + 1)生成临时变量。

    为什么作为函数参数的引用类型要是const才能生成临时变量?因为使用引用作为函数参数的目的,就是可以修改做为参数传递的变量,但是,创建临时变量会阻止这种意图,也就是说,如果作为参数的引用没有const关键字,而临时变量依然可以产生,那么,引用操作的将是这些临时变量,而不是传递进来的参数本身(这和传值差不多)。而有const关键字的时候,表示函数不能改变这些传进来的参数,因此,用生成的临时变量来初始化这些参数也就无所谓了。

    但是,上面的规则,只是对于内建类型成立,而对于复合类型(结构体和类,不包括数组,因为没有引用类型的数组,数组只能通过指针传递),总是会生成临时变量:

    class X {
    private:
       int i;
       int j;
    };
    void f(X& x) {}
    int main() {
        int i;
        f(X());
    }

    上面的c++代码可以通过编译。

  • 相关阅读:
    乔治·奥威尔的六条有效写作的规则
    读书:《个人形成论》 Carl R. Rogers
    想想体制性的生存法则
    每一个山峰都建立在同一座山上
    读书笔记:这些道理没有人告诉过你(二)
    举国的不仅仅是运动员
    参加了一个社会化营销策划比赛整理一下参考资料
    读书:《个人形成论》2 Carl R. Rogers
    前端避坑指南丨辛辛苦苦开发的 APP 竟然被判定为简单网页打包?
    Entify Framework 4.1[问题集] 一个实体的双向依赖引起的错误
  • 原文地址:https://www.cnblogs.com/chaoguo1234/p/3330013.html
Copyright © 2011-2022 走看看