Как ссылки реализованы внутри?

Мне просто интересно, как ссылки на самом деле реализованы в разных компиляторах и конфигурациях отладки/выпуска. Предоставляет ли стандарт рекомендации по их внедрению? Отличаются ли реализации?

Я пытался запустить простую программу, в которой я возвращал неконстантные ссылки и указатели на локальные переменные из функций, но они работали так же. Значит ли это, что ссылки внутренне являются просто указателем?

Ответ 1

Естественная реализация ссылки действительно является указателем. Однако, не зависеть от этого в вашем коде.

Ответ 2

Чтобы повторить некоторые вещи, которые все говорили, давайте посмотрим на какой-то вывод компилятора:

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

Те тела обеих функций идентичны

Ответ 3

Извините за использование сборки, чтобы объяснить это, но я думаю, что это лучший способ понять, как ссылки выполняются компиляторами.

    #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 - 10 (десятичный), который мы присваиваем i. -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 до ptrToI. ptrToI снова находится в стеке, расположенном по адресу -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) заменяется на -0xc(%ebp) в строке 10. -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 не разрешено, и он даст вам ошибку времени компиляции.

Ответ 4

В словах Бьярне:

Как указатель, ссылка является псевдонимом для объекта, обычно используется для хранения машинного адреса объекта и не налагает накладные расходы на производительность по сравнению с указателями, но отличается от указатель в том, что:

• Вы получаете доступ к ссылке с точно таким же синтаксисом, как и имя объекта.

• Ссылка всегда ссылается на объект, для которого он был инициализирован.

• Нет нулевой ссылки, и мы можем предположить, что ссылка ссылается на объект


Хотя ссылка на самом деле является указателем, но он не должен использоваться как указатель, а как псевдоним.

Ответ 5

Нет ссылки на указатель. Во многих случаях это так, но в других случаях это просто псевдоним, и нет необходимости в отдельном распределении памяти для указателя. образцы сборки не всегда правильны, поскольку они в значительной степени зависят от оптимизаций и того, насколько "умный" является компилятором.

например: int i; INT & j = i;

не нужно создавать дополнительный код или выделять дополнительную память.

Ответ 6

Я не могу сказать, что это правильно, но я сделал несколько Googling и нашел это утверждение:

Стандарт языка не требует любой конкретный механизм. каждый реализация может сделать это в любом так как поведение совместимый.

Источник: Bytes.com

Ответ 7

Ссылка не указатель. Это факт. Указатель может связываться с другим объектом, имеет свои собственные операции, такие как разыменование и увеличение/уменьшение.

Хотя внутренне ссылка может быть реализована как указатель. Но это детализация реализации, которая не меняет того факта, что ссылки не могут быть взаимозаменяемы с указателями. И нельзя писать код, предполагая, что ссылки реализованы как указатели.

Ответ 8

В общем:

Ссылка, внутренне реализована как константный указатель, который автоматически разыменовывается.