参考如何在内部实施?

时间:2010-10-17 19:02:11

标签: c++ pointers reference

只是想知道它是如何在不同的编译器和调试/发布配置中实现的。标准是否以某种方式提供有关其实施的建议?它在哪里有所不同吗?

我尝试运行一个简单的程序,我已经从函数返回非const引用和指向局部变量的指针,但它的工作方式相同。那么内部引用只是一个指针吗?

8 个答案:

答案 0 :(得分:40)

重复一些大家都在说的内容,让我们看看一些编译器输出:

#include <stdio.h>
#include <stdlib.h>

int byref(int & foo)
{
  printf("%d\n", foo);
}
int byptr(int * foo)
{
  printf("%d\n", *foo);
}

int main(int argc, char **argv) {
  int aFoo = 5; 
  byref(aFoo);
  byptr(&aFoo);
}

我们可以使用LLVM编译它(关闭优化),我们得到以下结果:

define i32 @_Z5byrefRi(i32* %foo) {
entry:
  %foo_addr = alloca i32*                         ; <i32**> [#uses=2]
  %retval = alloca i32                            ; <i32*> [#uses=1]
  %"alloca point" = bitcast i32 0 to i32          ; <i32> [#uses=0]
  store i32* %foo, i32** %foo_addr
  %0 = load i32** %foo_addr, align 8              ; <i32*> [#uses=1]
  %1 = load i32* %0, align 4                      ; <i32> [#uses=1]
  %2 = call i32 (i8*, ...)* @printf(i8* noalias getelementptr inbounds ([4 x i8]* @.str, i64 0, i64 0), i32 %1) ; <i32> [#uses=0]
  br label %return

return:                                           ; preds = %entry
  %retval1 = load i32* %retval                    ; <i32> [#uses=1]
  ret i32 %retval1
}

define i32 @_Z5byptrPi(i32* %foo) {
entry:
  %foo_addr = alloca i32*                         ; <i32**> [#uses=2]
  %retval = alloca i32                            ; <i32*> [#uses=1]
  %"alloca point" = bitcast i32 0 to i32          ; <i32> [#uses=0]
  store i32* %foo, i32** %foo_addr
  %0 = load i32** %foo_addr, align 8              ; <i32*> [#uses=1]
  %1 = load i32* %0, align 4                      ; <i32> [#uses=1]
  %2 = call i32 (i8*, ...)* @printf(i8* noalias getelementptr inbounds ([4 x i8]* @.str, i64 0, i64 0), i32 %1) ; <i32> [#uses=0]
  br label %return

return:                                           ; preds = %entry
  %retval1 = load i32* %retval                    ; <i32> [#uses=1]
  ret i32 %retval1
}

两个功能的主体是相同的

答案 1 :(得分:14)

引用的自然实现确实是一个指针。但是,请不要在代码中依赖于此。

答案 2 :(得分:13)

很抱歉使用程序集来解释这一点,但我认为这是了解编译器如何实现引用的最佳方法。

    #include <iostream>

    using namespace std;

    int main()
    {
        int i = 10;
        int *ptrToI = &i;
        int &refToI = i;

        cout << "i = " << i << "\n";
        cout << "&i = " << &i << "\n";

        cout << "ptrToI = " << ptrToI << "\n";
        cout << "*ptrToI = " << *ptrToI << "\n";
        cout << "&ptrToI = " << &ptrToI << "\n";

        cout << "refToNum = " << refToI << "\n";
        //cout << "*refToNum = " << *refToI << "\n";
        cout << "&refToNum = " << &refToI << "\n";

        return 0;
    }

此代码的输出就像这样

    i = 10
    &i = 0xbf9e52f8
    ptrToI = 0xbf9e52f8
    *ptrToI = 10
    &ptrToI = 0xbf9e52f4
    refToNum = 10
    &refToNum = 0xbf9e52f8

让我们看一下反汇编(我为此使用了GDB。这里的8,9和10是代码行号)

8           int i = 10;
0x08048698 <main()+18>: movl   $0xa,-0x10(%ebp)

此处$0xa是我们分配给i的10(十进制)。 -0x10(%ebp)此处表示ebp register -16(十进制)的内容。 -0x10(%ebp)指向堆栈上i的地址。

9           int *ptrToI = &i;
0x0804869f <main()+25>: lea    -0x10(%ebp),%eax
0x080486a2 <main()+28>: mov    %eax,-0x14(%ebp)

i的地址指定给ptrToIptrToI位于地址-0x14(%ebp)的堆栈上,即ebp - 20(十进制)。

10          int &refToI = i;
0x080486a5 <main()+31>: lea    -0x10(%ebp),%eax
0x080486a8 <main()+34>: mov    %eax,-0xc(%ebp)

现在抓住了!比较第9行和第10行的反汇编,您将观察到,-0x14(%ebp)被第10行中的-0xc(%ebp)替换。-0xc(%ebp)refToNum的地址。它在堆栈上分配。但是,您永远无法从代码中获取此地址,因为您无需知道地址。

因此;引用确实占用了内存。在这种情况下,它是堆栈内存,因为我们已将其分配为局部变量。 它占用了多少内存?  指针占据了很多。

现在让我们看看我们如何访问引用和指针。为简单起见,我只展示了汇编代码段的一部分

16          cout << "*ptrToI = " << *ptrToI << "\n";
0x08048746 <main()+192>:        mov    -0x14(%ebp),%eax
0x08048749 <main()+195>:        mov    (%eax),%ebx
19          cout << "refToNum = " << refToI << "\n";
0x080487b0 <main()+298>:        mov    -0xc(%ebp),%eax
0x080487b3 <main()+301>:        mov    (%eax),%ebx

现在比较以上两行,你会看到惊人的相似性。 -0xc(%ebp)refToI的实际地址,您永远无法访问该地址。 简单来说,如果您将引用视为普通指针,那么访问引用就像获取引用指向的地址处的值一样。这意味着以下两行代码将为您提供相同的结果

cout << "Value if i = " << *ptrToI << "\n";
cout << " Value if i = " << refToI << "\n";

现在比较一下

15          cout << "ptrToI = " << ptrToI << "\n";
0x08048713 <main()+141>:        mov    -0x14(%ebp),%ebx
21          cout << "&refToNum = " << &refToI << "\n";
0x080487fb <main()+373>:        mov    -0xc(%ebp),%eax

我想你能发现这里发生了什么。 如果您要求&refToI,则会返回-0xc(%ebp)地址位置的内容,-0xc(%ebp)位于refToi所在的位置,其内容只是i的地址。

最后一件事,为什么这一行被评论了?

//cout << "*refToNum = " << *refToI << "\n";

因为不允许*refToI,它会给你一个编译时错误。

答案 3 :(得分:4)

在Bjarne的话中:

与指针一样,引用是对象的别名,通常用于保存对象的计算机地址,并且与之相比不会产生性能开销指针,但它与指针的不同之处在于:

•您使用与对象名称完全相同的语法访问引用。

•引用始终引用它初始化的对象。

•没有''空引用'',我们可以假设引用引用了一个对象

虽然 引用 实际上是指针,但它不应该像指针一样使用但作为别名

答案 4 :(得分:2)

不需要引用作为指针。 在许多情况下,它是,但在其他情况下,它只是一个别名,并且不需要为指针单独的内存分配。 汇编样本并不总是正确的,因为它们在很大程度上依赖于优化以及如何&#34; smart&#34;是编译器。

例如: int i; INT和放大器; j = i;

不需要生成任何额外的代码或分配任何额外的内存。

答案 5 :(得分:1)

我不能说这是肯定的,但我做了一些谷歌搜索并发现了这个声明:

  

语言标准不要求   任何特定的机制。每   任何实现都是免费的   方式,只要行为是   兼容。

来源:Bytes.com

答案 6 :(得分:1)

参考不是指针。这是事实。指针可以绑定到另一个对象,有自己的操作,如解除引用和递增/递减。

虽然在内部,但可以将引用实现为指针。但这是一个实现细节,它不会改变引用不能与指针互换的事实。人们不能编写代码,假设引用被实现为指针。

答案 7 :(得分:0)

一般:

  

参考,在内部实现为常量指针,该指针会自动取消引用。