【问题标题】:C++ references and pointers at the compiler level编译器级别的 C++ 引用和指针
【发布时间】:2014-01-15 18:13:32
【问题描述】:

我正在努力学习 C++ 编译器如何处理引用和指针,为我下学期要学习的编译器课程做准备。我对编译器如何处理 C++ 中的引用特别感兴趣。

标准规定引用是“别名”,但我不知道这在编译器级别意味着什么。我有两个理论:

  1. 非引用变量在符号表中有一个条目。当创建对该变量的引用时,编译器只需创建另一个“指向”符号表中完全相同的条目(而不是指向非引用变量在内存中的位置)的词位。

  2. 创建对该变量的引用时,编译器会创建一个指向该变量在内存中的位置的指针。解析语言的上下文时会处理引用的限​​制(无空值等)。换句话说,引用是解引用指针的“语法糖”。

据我所知,这两种解决方案都会创建一个“别名”。编译器使用一种而不使用另一种吗?还是依赖于编译器?

顺便说一句,我知道在机器语言级别,两者都是“指针”(除整数之外的几乎所有内容都是机器级别的“指针”)。我对编译器在生成机器代码之前做了什么感兴趣。

编辑:我很好奇的部分原因是PHP uses method #1,我想知道 C++ 编译器是否以同样的方式工作。 Java 当然不使用方法#1,它们的“引用”实际上是取消引用的指针;请参阅 Scott Stanchfield 的 this article

【问题讨论】:

  • 为什么你认为编译器不能同时使用取决于上下文?
  • 该标准不需要引用甚至需要存储,因此您的两种理论中的任何一种都可能是正确的。真正了解的唯一方法是打开编译器的源代码并检查(或询问实现编译器的人。)
  • @Slava:我想它可以,我只是假设以一种或另一种方式处理它会使编译器的代码“更好”(更一致,更容易维护等)。另一方面,我不知道一种方法比另一种方法有什么优势,或者即使有什么优势。
  • @KarlGiesing 我会说理论 N2 是通用的,可以在任何地方使用,尽管 N1 可以在特殊情况下用于优化。但编译器开发人员应该更清楚。

标签: c++ pointers compiler-construction reference


【解决方案1】:

我将尝试解释 g++ 编译器是如何实现引用的。

    #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 是不允许的,它会给你一个编译时错误。

【讨论】:

  • 谢谢,正是我想要的:)
【解决方案2】:

理解指针和引用与为它们实现代码完全不同。

我建议您学习如何正确使用它们并专注于编译器理论的核心。如果没有指针、引用和继承的概念,基本的编译器理论课就够难了。指针和引用留给更高级的类。

简单地说:尽可能使用引用,必要时使用指针。

编辑 1:
编译器可以以任何他们想要的方式实现引用和指针,只要它们的语法和语义行为符合语言规范。

一个简单的实现是将引用视为具有附加属性的指针。

内存中的所有东西都有一个位置,即地址。编译器可能必须使用内部指针从内存加载到寄存器并将寄存器内容存储到内存中。因此,要引用内存中的变量,无论是通过指针、引用还是别名,编译器都需要变量的地址。 (这不包括被区别对待的寄存器变量。)因此使用指针作为引用或别名可以节省一些编码。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2010-12-12
    • 2013-10-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-09-28
    • 2015-02-13
    相关资源
    最近更新 更多