¿Cuáles son las diferencias entre una variable puntero y una variable de referencia?
¿Cuál es la diferencia entre una variable puntero y una variable de referencia?
Se puede reasignar un puntero:
int x = 5; int y = 6; int *p; p = &x; p = &y; *p = 10; assert(x == 5); assert(y == 10);
Una referencia no se puede volver a vincular y se debe vincular en la inicialización:
int x = 5; int y = 6; int &q; // error int &r = x;
Una variable de puntero tiene su propia identidad: una dirección de memoria visible y distinta que se puede tomar con el
&
operador unario y una cierta cantidad de espacio que se puede medir con elsizeof
operador. El uso de esos operadores en una referencia devuelve un valor correspondiente a cualquier cosa a la que esté vinculada la referencia; La propia dirección y tamaño de la referencia son invisibles. Dado que la referencia asume de esta manera la identidad de la variable original, es conveniente pensar en una referencia como otro nombre para la misma variable.int x = 0; int &r = x; int *p = &x; int *p2 = &r; assert(p == p2); // &x == &r assert(&p != &p2);
Es posible crear un puntero a un puntero, pero no un puntero a una referencia.
int **pp; // OK, pointer to pointer int &*pr; // ill-formed, pointer to reference
Es posible crear una serie de punteros, pero no una serie de referencias.
int *ap[]; // OK, array of pointers int &ar[]; // ill-formed, array of references
Puede tener punteros anidados arbitrariamente en punteros que ofrezcan niveles adicionales de direccionamiento indirecto. Las referencias sólo ofrecen un nivel de indirección porque las referencias a referencias colapsan .
int x = 0; int y = 0; int *p = &x; int *q = &y; int **pp = &p; **pp = 2; pp = &q; // *pp is now q **pp = 4; assert(y == 4); assert(x == 2);
Se puede asignar un puntero
nullptr
, mientras que una referencia debe estar vinculada a un objeto existente. Si lo intentas lo suficiente, puedes vincular una referencia anullptr
, pero esto no está definido y no se comportará de manera consistente./* the code below is undefined; your compiler may optimise it * differently, emit warnings, or outright refuse to compile it */ int &r = *static_cast<int *>(nullptr); // prints "null" under GCC 10 std::cout << (&r != nullptr ? "not null" : "null") << std::endl; bool f(int &r) { return &r != nullptr; } // prints "not null" under GCC 10 std::cout << (f(*static_cast<int *>(nullptr)) ? "not null" : "null") << std::endl;
Sin embargo, puede tener una referencia a un puntero cuyo valor sea
nullptr
.Los punteros son iteradores contiguos (de una matriz). Puede utilizar
++
para ir al siguiente elemento al que apunta un puntero y+ 4
para ir al quinto elemento.Es necesario eliminar la referencia de un puntero
*
para acceder al objeto al que apunta, mientras que una referencia se puede utilizar directamente. Un puntero a una clase/estructura se utiliza->
para acceder a sus miembros, mientras que una referencia utiliza un archivo.
.Las referencias constantes y las referencias rvalue se pueden vincular a temporales (consulte materialización temporal ). Los indicadores no pueden (no sin alguna dirección indirecta):
const int &x = int(12); // legal C++ int *y = &int(12); // illegal to take the address of a temporary.
Esto hace que
const &
su uso sea más conveniente en listas de argumentos, etc.
¿Qué es una referencia de C++ ( para programadores de C ) ?
Una referencia puede considerarse como un puntero constante (¡no debe confundirse con un puntero a un valor constante!) con dirección indirecta automática, es decir, el compilador aplicará el *
operador por usted.
Todas las referencias deben inicializarse con un valor no nulo o la compilación fallará. No es posible obtener la dirección de una referencia (el operador de dirección devolverá la dirección del valor referenciado) ni es posible hacer aritmética con las referencias.
A los programadores de C puede que no les gusten las referencias de C++, ya que ya no será obvio cuando ocurra la indirección o si un argumento se pasa por valor o por puntero sin mirar las firmas de funciones.
A los programadores de C++ puede no gustarles el uso de punteros porque se consideran inseguros (aunque las referencias no son realmente más seguras que los punteros constantes excepto en los casos más triviales) carecen de la conveniencia de la dirección indirecta automática y tienen una connotación semántica diferente.
Considere la siguiente declaración de las preguntas frecuentes de C++ :
Aunque una referencia a menudo se implementa utilizando una dirección en el lenguaje ensamblador subyacente, no piense en una referencia como un puntero de aspecto extraño a un objeto. Una referencia es el objeto. No es un puntero al objeto ni una copia del objeto. Es el objeto.
Pero si una referencia fuera realmente el objeto, ¿cómo podrían haber referencias pendientes? En lenguajes no administrados, es imposible que las referencias sean más "seguras" que los punteros; en general, simplemente no hay una manera de asignar alias de manera confiable a los valores a través de los límites del alcance.
Por qué considero útiles las referencias de C++
Al tener experiencia en C, las referencias de C++ pueden parecer un concepto un tanto tonto, pero aun así se deben usar en lugar de punteros siempre que sea posible: la dirección indirecta automática es conveniente y las referencias se vuelven especialmente útiles cuando se trata de RAII , pero no debido a ninguna seguridad percibida. ventaja, sino más bien porque hacen que escribir código idiomático sea menos complicado.
RAII es uno de los conceptos centrales de C++, pero interactúa de manera no trivial con la semántica de copia. Pasar objetos por referencia evita estos problemas ya que no implica copia. Si las referencias no estuvieran presentes en el lenguaje, tendría que usar punteros en su lugar, que son más engorrosos de usar, violando así el principio de diseño del lenguaje de que la solución de mejores prácticas debería ser más fácil que las alternativas.
Si quieres ser realmente pedante, hay una cosa que puedes hacer con una referencia que no puedes hacer con un puntero: extender la vida útil de un objeto temporal. En C++, si vincula una referencia constante a un objeto temporal, la vida útil de ese objeto se convierte en la vida útil de la referencia.
std::string s1 = "123";
std::string s2 = "456";
std::string s3_copy = s1 + s2;
const std::string& s3_reference = s1 + s2;
En este ejemplo, s3_copy copia el objeto temporal resultante de la concatenación. Mientras que s3_reference en esencia se convierte en el objeto temporal. En realidad, es una referencia a un objeto temporal que ahora tiene la misma duración que la referencia.
Si intenta esto sin, no const
debería poder compilarse. No puede vincular una referencia no constante a un objeto temporal, ni puede tomar su dirección.
Aparte del azúcar sintáctico, una referencia es un const
puntero ( no un puntero a a const
). Debes establecer a qué se refiere cuando declaras la variable de referencia, y no puedes cambiarla más adelante.
Actualización: ahora que lo pienso un poco más, hay una diferencia importante.
El objetivo de un puntero constante se puede reemplazar tomando su dirección y usando una conversión constante.
El objetivo de una referencia no se puede reemplazar de ninguna manera excepto UB.
Esto debería permitir al compilador optimizar más una referencia.
Contrariamente a la opinión popular, es posible tener una referencia que sea NULL.
int * p = NULL;
int & r = *p;
r = 1; // crash! (if you're lucky)
Por supuesto, es mucho más difícil hacerlo con una referencia, pero si lo logras, te arrancarás los pelos tratando de encontrarla. ¡Las referencias no son inherentemente seguras en C++!
Técnicamente, esta es una referencia no válida , no una referencia nula. C++ no admite referencias nulas como concepto, como puede encontrar en otros lenguajes. También existen otros tipos de referencias inválidas. Cualquier referencia no válida plantea el espectro de un comportamiento indefinido , tal como lo haría el uso de un puntero no válido.
El error real está en la desreferenciación del puntero NULL, antes de la asignación a una referencia. Pero no conozco ningún compilador que genere errores en esa condición: el error se propaga a un punto más adelante en el código. Eso es lo que hace que este problema sea tan insidioso. La mayoría de las veces, si eliminas la referencia a un puntero NULL, fallas justo en ese lugar y no se necesita mucha depuración para resolverlo.
Mi ejemplo anterior es breve y artificial. Aquí hay un ejemplo más real.
class MyClass
{
...
virtual void DoSomething(int,int,int,int,int);
};
void Foo(const MyClass & bar)
{
...
bar.DoSomething(i1,i2,i3,i4,i5); // crash occurs here due to memory access violation - obvious why?
}
MyClass * GetInstance()
{
if (somecondition)
return NULL;
...
}
MyClass * p = GetInstance();
Foo(*p);
Quiero reiterar que la única forma de obtener una referencia nula es mediante código con formato incorrecto y, una vez que lo tienes, obtienes un comportamiento indefinido. Nunca tiene sentido buscar una referencia nula; por ejemplo, puede intentarlo, if(&bar==NULL)...
pero el compilador podría optimizar la declaración para que no exista. Una referencia válida nunca puede ser NULL, por lo que desde el punto de vista del compilador la comparación siempre es falsa y es libre de eliminar la if
cláusula como código inactivo; esta es la esencia del comportamiento indefinido.
La forma correcta de evitar problemas es evitar eliminar la referencia a un puntero NULL para crear una referencia. Aquí hay una forma automatizada de lograr esto.
template<typename T>
T& deref(T* p)
{
if (p == NULL)
throw std::invalid_argument(std::string("NULL reference"));
return *p;
}
MyClass * p = GetInstance();
Foo(deref(p));
Para obtener una visión más antigua de este problema por parte de alguien con mejores habilidades de escritura, consulte las referencias nulas de Jim Hyslop y Herb Sutter.
Para ver otro ejemplo de los peligros de eliminar la referencia a un puntero nulo, consulte Exponer un comportamiento indefinido al intentar transferir código a otra plataforma de Raymond Chen.