Ответ 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
}
Те тела обеих функций идентичны
Извините за использование сборки, чтобы объяснить это, но я думаю, что это лучший способ понять, как ссылки выполняются компиляторами.
#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
не разрешено, и он даст вам ошибку времени компиляции.
В словах Бьярне:
Как указатель, ссылка является псевдонимом для объекта, обычно используется для хранения машинного адреса объекта и не налагает накладные расходы на производительность по сравнению с указателями, но отличается от указатель в том, что:
• Вы получаете доступ к ссылке с точно таким же синтаксисом, как и имя объекта.
• Ссылка всегда ссылается на объект, для которого он был инициализирован.
• Нет нулевой ссылки, и мы можем предположить, что ссылка ссылается на объект
Хотя ссылка на самом деле является указателем, но он не должен использоваться как указатель, а как псевдоним.
Нет ссылки на указатель. Во многих случаях это так, но в других случаях это просто псевдоним, и нет необходимости в отдельном распределении памяти для указателя. образцы сборки не всегда правильны, поскольку они в значительной степени зависят от оптимизаций и того, насколько "умный" является компилятором.
например: int i; INT & j = i;
не нужно создавать дополнительный код или выделять дополнительную память.
Я не могу сказать, что это правильно, но я сделал несколько Googling и нашел это утверждение:
Стандарт языка не требует любой конкретный механизм. каждый реализация может сделать это в любом так как поведение совместимый.
Источник: Bytes.com
Ссылка не указатель. Это факт. Указатель может связываться с другим объектом, имеет свои собственные операции, такие как разыменование и увеличение/уменьшение.
Хотя внутренне ссылка может быть реализована как указатель. Но это детализация реализации, которая не меняет того факта, что ссылки не могут быть взаимозаменяемы с указателями. И нельзя писать код, предполагая, что ссылки реализованы как указатели.
В общем:
Ссылка, внутренне реализована как константный указатель, который автоматически разыменовывается.