【问题标题】:How are references implemented internally?引用是如何在内部实现的?
【发布时间】:2011-04-26 16:01:44
【问题描述】:

我只是想知道如何在不同的编译器和调试/发布配置中实际实现引用。该标准是否就其实施提供了建议?实现有什么不同吗?

我尝试运行一个简单的程序,在该程序中我从函数返回非常量引用和指向局部变量的指针,但它们的工作方式相同。这是否意味着引用在内部只是一个指针?

【问题讨论】:

  • 完全实现定义。该标准规定引用是否使用存储是未指定的。可能,指针是最简单的,但很多时候可以完全删除引用。

标签: c++ pointers reference


【解决方案1】:

只是重复一些大家一直在说的东西,让我们看看一些编译器输出:

#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
}

两个函数的主体是相同的

【讨论】:

    【解决方案2】:

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

        #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 的地址。它在堆栈上分配。但是你永远无法从你的代码中得到这个地址,因为你不需要知道这个地址。

    所以;引用确实占用内存。在这种情况下,它是堆栈内存,因为我们已将其分配为局部变量。 它占用多少内存? 一个指针占用多少空间。

    现在让我们看看我们如何访问引用和指针。为简单起见,我只展示了组件 sn-p 的一部分

    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
    

    我想你能够发现这里发生了什么。 如果查询&amp;refToI,则返回-0xc(%ebp)地址位置的内容,-0xc(%ebp)refToi所在的位置,其内容就是i的地址。

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

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

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

    【讨论】:

      【解决方案3】:

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

      【讨论】:

      • @Martin York,可能取决于 sizeof(T*)==sizeof(T&)
      • @Peter G:这不会像你期望的那样工作。由于引用是别名,RHS 实际上得到了 T 的大小(因为 T& 是 T 的别名)。因此它仅在 sizeof(T) == sizeof(void*) 时成立。尝试打印sizeof(char&amp;),它将返回 1。别名不会引入新变量,它们会为变量引入新名称(因此它们甚至可能不需要实现指针(如果变量和引用在同一范围内) .
      • 错过了一个不是来自原始评论:-> 因为你不能对参考做任何事情,因为你知道它的实现不会受到伤害。您可以NOT 获取引用的地址,您可以NOT 修改引用本身(仅它所引用的内容)。无法与引用进行交互。
      • 没有符合标准的方式直接依赖它。这对我来说也很清楚。我在上面的评论中所说的意思是引用存储的大小是可观察的并且会产生影响。当您有一个包含引用成员 a 的结构和另一个具有指针成员 a 的结构并且没有填充或对齐问题时,如果引用具有不同的大小,则结构的大小将会改变。 ...
      • 标准规定引用是否使用存储是未指定的。有时,引用只是存在于编译时的东西!
      【解决方案4】:

      用 Bjarne 的话来说:

      和指针一样,引用是对象的别名,通常实现保存对象的机器地址,与指针,但它与指针的不同之处在于:

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

      • 引用总是指向它被初始化的对象。

      • 没有“空引用”,我们可以假设引用指向一个对象


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

      【讨论】:

        【解决方案5】:

        没有必要将引用作为指针。 在许多情况下,它是,但在其他情况下,它只是一个别名,不需要为指针单独分配内存。 汇编样本并不总是正确的,因为它们在很大程度上取决于优化以及编译器的“智能”程度。

        例如: 诠释我; int&j = i;

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

        【讨论】:

          【解决方案6】:

          我不能肯定地说这是正确的,但我做了一些谷歌搜索并找到了以下声明:

          语言标准不要求 任何特定的机制。每个 实施是免费的 方式,只要行为是 合规。

          来源:Bytes.com

          【讨论】:

            【解决方案7】:

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

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

            【讨论】:

              【解决方案8】:

              一般:

              引用,在内部被实现为一个自动取消引用的常量指针。

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2011-08-06
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多