再谈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);
}

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

再谈c++中的引用

出现这种错误,是因为要调用函数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++代码可以通过编译。

原文链接: https://www.cnblogs.com/chaoguo1234/p/3330013.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/105050

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年2月10日 上午7:55
下一篇 2023年2月10日 上午8:02

相关推荐