内存方面的C ++引用如何?

时间:2009-07-24 20:15:39

标签: c++ memory-management reference

假设:

int i = 42;
int j = 43;
int k = 44;

通过查看变量地址,我们知道每个地址占用4个字节(在大多数平台上)。

然而,考虑:

int i = 42;
int& j = i;
int k = 44;

我们将看到变量i确实需要4个字节,但j需要 none k再次占用堆栈上的4个字节。

这里发生了什么?看起来j在运行时根本就不存在。那么我作为函数参数收到的引用呢? 必须在堆栈上占用一些空间......

虽然我们在这里 - 为什么我不能定义数组或引用?

int&[] arr = new int&[SIZE]; // compiler error! array of references is illegal

8 个答案:

答案 0 :(得分:43)

在遇到引用 j 的任何地方,都会将其替换为 i的地址。所以基本上参考内容地址在编译时解析,并且不需要像运行时的​​指针那样取消引用它。

只是为了澄清我的意思是我的地址:

void function(int& x)
{
    x = 10;
}

int main()
{
    int i = 5;
    int& j = i;

    function(j);
}

在上面的代码中, j 不应占用 主堆栈 的空间,但引用 x 功能将在其堆栈中占据一席之地。这意味着当使用 j 作为参数调用函数时, 函数的堆栈上将推送的地址 。编译器可以而且不应该在主要堆栈上为 j 预留空间。

对于数组部分,标准说::

  

C ++标准8.3.2 / 4:

     

不应引用引用,也不引用引用数组,   并没有指向参考的指针。

Why arrays of references are illegal?

答案 1 :(得分:37)

  

C ++参考如何看,   存储器明智?

没有。 C ++标准只说明它应该如何表现,而不是它应该如何实现。

在一般情况下,编译器通常将引用实现为指针。但是它们通常有更多关于引用可能指向的信息,并将其用于优化。

请记住,引用的唯一要求是它表现为引用对象的别名。因此,如果编译器遇到此代码:

int i = 42;
int& j = i;
int k = 44;

它看到的不是“创建指向变量i的指针”(虽然这是编译器在某些情况下可能选择实现它的方式),而是“在符号表中记下{ {1}}现在是j的别名。“

编译器不必为i创建一个新变量,它只需要记住,从现在开始引用j时,它应该真正交换它并使用{{1相反。

至于创建一个引用数组,你不能这样做,因为它没用,毫无意义。

创建数组时,所有元素都是默认构造的。默认构造引用意味着什么?它指向什么?引用中的重点是它们初始化以引用另一个对象,之后它们无法重新定位。

因此,如果可以完成,最终会得到一个对 nothing 的引用数组。并且您无法更改它们以引用某些内容,因为它们已经初始化。

答案 2 :(得分:9)

实际上,引用等同于指针,除了允许使用引用的额外约束可以允许编译器在更多情况下“优化它”(取决于编译器的智能程度,它的优化设置等当然)。

答案 3 :(得分:9)

很抱歉使用程序集来解释这一点,但我认为这是了解参考资料的最佳方法。

    #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 << "refToI = " << refToI << "\n";
        //cout << "*refToI = " << *refToI << "\n";
        cout << "&refToI = " << &refToI << "\n";

        return 0;
    }

此代码的输出就像这样

    i = 10
    &i = 0xbf9e52f8
    ptrToI = 0xbf9e52f8
    *ptrToI = 10
    &ptrToI = 0xbf9e52f4
    refToI = 10
    &refToI = 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)refToI的地址。它在堆栈上分配。但是,您永远无法从代码中获取此地址,因为您无需知道地址。

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

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

16          cout << "*ptrToI = " << *ptrToI << "\n";
0x08048746 <main()+192>:        mov    -0x14(%ebp),%eax
0x08048749 <main()+195>:        mov    (%eax),%ebx
19          cout << "refToI = " << 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 << "&refToI = " << &refToI << "\n";
0x080487fb <main()+373>:        mov    -0xc(%ebp),%eax

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

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

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

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

答案 4 :(得分:8)

您无法定义引用数组,因为没有语法来初始化它们。 C ++不允许未初始化的引用。至于你的第一个问题,编译器没有义务为不必要的变量分配空间。没有办法让j指向另一个变量,因此它实际上只是函数范围内i的别名,而这就是编译器对待它的方式。

答案 5 :(得分:6)

在其他地方提到的东西 - 如何让编译器将一些存储空间用于引用:

class HasRef
{
    int &r;

public:
    HasRef(int &n)
        : r(n) { }
};

这使编译器无法简单地将其视为编译时别名(同一存储的替代名称)。

答案 6 :(得分:3)

在他们需要物理表现形式(即作为聚合的成员)之前,参考文献实际上并不存在。

由于上述原因,有一系列参考文献是非法的。但没有什么能阻止你创建一个具有引用成员的结构/类数组。

我确信有人会指出提及这一切的标准条款。

答案 7 :(得分:3)

它没有修复 - 编译器在如何根据具体情况实现引用方面有很大的自由度。所以在你的第二个例子中,它将j视为i的别名,不需要其他任何东西。传递ref参数时,它也可以使用堆栈偏移量,同样没有开销。但在其他情况下,它可以使用指针。