¿Cómo se implementan internamente las referencias?

¿Cómo se implementan internamente las referencias?

Solo para repetir algunas de las cosas que todos han estado diciendo, veamos algunos resultados del compilador:

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

Podemos compilar esto con LLVM (con las optimizaciones desactivadas) y obtenemos lo siguiente:

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
}

Los cuerpos de ambas funciones son idénticos


Perdón por usar ensamblador para explicar esto, pero creo que esta es la mejor manera de entender cómo los compiladores implementan las referencias.

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

La salida de este código es así

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

Veamos el desmontaje (utilicé GDB para esto. 8,9 y 10 aquí son los números de línea del código)

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

Aquí $0xa es el 10(decimal) que estamos asignando a i . -0x10(%ebp) aquí significa contenido de ebp register –16(decimal).-0x10(%ebp) apunta a la dirección de i en la pila.

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

Asignar dirección de i a ptrToI . ptrToI está nuevamente en la pila ubicada en la dirección -0x14(%ebp) , eso es ebp – 20 (decimales).

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

¡Ahora aquí está la trampa! Compare el desmontaje de las líneas 9 y 10 y observará que ,-0x14(%ebp) se reemplaza por -0xc(%ebp) en la línea número 10. -0xc(%ebp) es la dirección de refToNum . Se asigna en la pila. Pero nunca podrá obtener esta dirección de su código porque no es necesario que sepa la dirección.

Asi que; una referencia ocupa memoria. En este caso es la memoria de la pila ya que la hemos asignado como variable local. ¿Cuánta memoria ocupa? La que ocupa un puntero.

Ahora veamos cómo accedemos a la referencia y los punteros. Para simplificar, he mostrado solo una parte del fragmento de código de ensamblaje

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

Ahora compare las dos líneas anteriores, verá una sorprendente similitud. -0xc(%ebp) es la dirección real de refToI que nunca es accesible para usted. En términos simples, si piensa en la referencia como un puntero normal, acceder a una referencia es como obtener el valor en la dirección a la que apunta la referencia. Lo que significa que las siguientes dos líneas de código le darán el mismo resultado

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

Ahora compara esto

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

Supongo que eres capaz de detectar lo que está sucediendo aquí. Si pides &refToI , el contenido de -0xc(%ebp) se devuelve la ubicación de la dirección y -0xc(%ebp) es donde refToi reside y su contenido no es más que la dirección de i .

Una última cosa, ¿Por qué se comenta esta línea?

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

Porque *refToI no está permitido y le dará un error de tiempo de compilación.


La implementación natural de una referencia es de hecho un puntero. Sin embargo, no dependa de esto en su código.