W jaki sposób referencje są implementowane wewnętrznie?

W jaki sposób referencje są implementowane wewnętrznie?

Aby powtórzyć niektóre z rzeczy, o których wszyscy mówili, spójrzmy na niektóre dane wyjściowe kompilatora:

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

Możemy to skompilować za pomocą LLVM (z wyłączoną optymalizacją) i otrzymamy:

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
}

Ciała obu funkcji są identyczne


Przepraszamy za użycie asemblera do wyjaśnienia tego, ale myślę, że jest to najlepszy sposób na zrozumienie, w jaki sposób referencje są implementowane przez kompilatory.

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

Wynik tego kodu wygląda tak

 i = 10
 &i = 0xbf9e52f8
 ptrToI = 0xbf9e52f8
 *ptrToI = 10
 &ptrToI = 0xbf9e52f4
 refToNum = 10
 &refToNum = 0xbf9e52f8

Spójrzmy na demontaż (użyłem do tego GDB. 8,9 i 10 to numery linii kodu)

8 int i = 10;
0x08048698 <main()+18>: movl $0xa,-0x10(%ebp)

Tutaj $0xa to 10 (dziesiętne), które przypisujemy do i . -0x10(%ebp) tutaj oznacza zawartość ebp register -16(dziesiętnie).-0x10(%ebp) wskazuje na adres i na stosie.

9 int *ptrToI = &i;
0x0804869f <main()+25>: lea -0x10(%ebp),%eax
0x080486a2 <main()+28>: mov %eax,-0x14(%ebp)

Przypisz adres i do ptrToI . ptrToI ponownie znajduje się na stosie znajdującym się pod adresem -0x14(%ebp) , czyli ebp – 20 (dziesiętnie).

10 int &refToI = i;
0x080486a5 <main()+31>: lea -0x10(%ebp),%eax
0x080486a8 <main()+34>: mov %eax,-0xc(%ebp)

Teraz jest haczyk! Porównaj demontaż linii 9 i 10, a zobaczysz, że -0x14(%ebp) jest zastępowane przez -0xc(%ebp) w wierszu numer 10. -0xc(%ebp) to adres refToNum . Jest alokowany na stosie. Ale nigdy nie będziesz w stanie uzyskać tego adresu od swojego kodu, ponieważ nie musisz znać adresu.

Więc; odwołanie zajmuje pamięć. W tym przypadku jest to pamięć stosu, ponieważ przydzieliliśmy ją jako zmienną lokalną. Ile pamięci zajmuje? Tyle zajmuje wskaźnik.

Zobaczmy teraz, jak uzyskujemy dostęp do referencji i wskaźników. Dla uproszczenia pokazałem tylko część fragmentu kodu montażowego

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

Teraz porównaj powyższe dwie linie, zobaczysz uderzające podobieństwo. -0xc(%ebp) to rzeczywisty adres refToI który nigdy nie jest dla ciebie dostępny. Mówiąc prościej, jeśli myślisz o referencji jak o normalnym wskaźniku, to dostęp do referencji jest jak pobranie wartości pod adresem wskazywanym przez referencję. Co oznacza, że ​​poniższe dwie linie kodu dadzą ten sam wynik

cout << "Value if i = " << *ptrToI << "\n";
cout << " Value if i = " << refToI << "\n";

Teraz porównaj to

15 cout << "ptrToI = " << ptrToI << "\n";
0x08048713 <main()+141>: mov -0x14(%ebp),%ebx
21 cout << "&refToNum = " << &refToI << "\n";
0x080487fb <main()+373>: mov -0xc(%ebp),%eax

Myślę, że jesteś w stanie zauważyć, co się tutaj dzieje. Jeśli poprosisz o &refToI , zawartość -0xc(%ebp) zwracana jest lokalizacja adresu i -0xc(%ebp) jest tam, gdzie refToi rezyduje, a jego zawartość to nic innego jak adres i .

Ostatnia rzecz:dlaczego ta linia jest skomentowana?

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

Ponieważ *refToI jest niedozwolone i spowoduje błąd w czasie kompilacji.


Naturalna implementacja referencji jest rzeczywiście wskaźnikiem. Jednak nie polegaj na tym w swoim kodzie.