Как выглядит ссылка на С++, с точки зрения памяти?
Дано:
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
Ответы
Ответ 1
везде встречается ссылка j, она заменяется на адресом i. Таким образом, основной адрес ссылочного содержимого разрешается во время компиляции, и нет необходимости разыгрывать его как указатель во время выполнения.
Просто чтобы уточнить, что я имею в виду по адресу i:
void function(int& x)
{
x = 10;
}
int main()
{
int i = 5;
int& j = i;
function(j);
}
В приведенном выше коде j не должно занимать место в главном стеке , но ссылка x функции strong > займет место в своем стеке. Это означает, что при вызове функции с j в качестве аргумента адрес i, который будет помещен в стек функции strong > . Компилятор может и не должен резервировать место в основном стеке для j.
Для части массива стандарты говорят:
С++ Standard 8.3.2/4:
Не должно быть ссылок на ссылки, нет массивов ссылок, и нет указателей на ссылки.
Почему массивы ссылок являются незаконными?
Ответ 2
Как выглядит ссылка на С++, память-накрест?
Это не так. Стандарт С++ только говорит, как он должен себя вести, а не как его реализовать.
В общем случае компиляторы обычно используют ссылки в качестве указателей. Но они обычно имеют больше информации о том, что может указывать ссылка, и использовать ее для оптимизации.
Помните, что единственным требованием для ссылки является то, что она ведет себя как псевдоним для ссылочного объекта. Поэтому, если компилятор встречает этот код:
int i = 42;
int& j = i;
int k = 44;
то, что он видит, не "создает указатель на переменную i
" (хотя именно так компилятор может реализовать его в некоторых случаях), а скорее "сделайте примечание в таблице символов, что j
теперь является псевдонимом для i
."
Компилятору не нужно создавать новую переменную для j
, она просто должна помнить, что всякий раз, когда j
ссылается с этого момента, она должна поменять ее и использовать i
вместо.
Что касается создания массива ссылок, вы не можете этого сделать, потому что это будет бесполезно и бессмысленно.
При создании массива все элементы создаются по умолчанию. Что означает стандартная конструкция? На что это указывает? Вся ссылка в ссылках состоит в том, что они инициализируются для ссылки на другой объект, после чего они не могут быть переустановлены.
Итак, если это можно сделать, вы получите массив ссылок на ничего. И вы не сможете изменить их, чтобы ссылаться на них, потому что они уже были инициализированы.
Ответ 3
На практике ссылка эквивалентна указателю, за исключением того, что дополнительные ограничения на использование ссылок могут позволить компилятору "оптимизировать его" в большем количестве случаев (в зависимости от того, насколько интеллектуальным является компилятор, его настройки оптимизации и т.д. и т.д.).
Ответ 4
Вы не можете определить массив ссылок, потому что для их инициализации нет синтаксиса. С++ не допускает неинициализированных ссылок. Что касается вашего первого вопроса, компилятор не обязан выделять пространство для ненужных переменных. Нет никакого способа, чтобы J указывала на другую переменную, поэтому она фактически просто псевдоним для я в области функций и то, как компилятор относится к ней.
Ответ 5
Что-то, о чем упоминается только в другом месте - как заставить компилятор посвятить некоторое пространство памяти ссылке:
class HasRef
{
int &r;
public:
HasRef(int &n)
: r(n) { }
};
Это лишает компилятор возможности просто рассматривать его как псевдоним времени компиляции (альтернативное имя для одного и того же хранилища).
Ответ 6
Извините за использование сборки, чтобы объяснить это, но я думаю, что это лучший способ понять ссылки.
#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
не разрешено, и он даст вам ошибку времени компиляции.
Ответ 7
Ссылки фактически не существуют физически, пока они не должны иметь физическое проявление (т.е. как член совокупности).
Наличие массива ссылок является незаконным, вероятно, из-за вышеизложенного. Но ничто не мешает вам создавать массив структур/классов, имеющих ссылочные элементы.
Я уверен, что кто-то укажет стандартное предложение, в котором упоминается все это.
Ответ 8
Это не исправлено - компилятор имеет большую свободу в том, как реализовать ссылку в каждом конкретном случае. Поэтому в вашем втором примере он рассматривает j как псевдоним для i, больше ничего не нужно. При передаче параметра ref он также может использовать смещение стека, опять же никаких накладных расходов. Но в других ситуациях он может использовать указатель.