tipos - ¿Cuáles son las diferencias entre una variable de puntero y una variable de referencia en C++?
tipos de punteros c++ (30)
¿Qué es una referencia de C ++ ( para programadores de C )
Una referencia se puede considerar como un puntero constante (¡no debe confundirse con un puntero a un valor constante!) Con direccionamiento automático automático, es decir, el compilador aplicará el operador *
para usted.
Todas las referencias deben inicializarse con un valor que no sea nulo o la compilación fallará. No es posible obtener la dirección de una referencia (el operador de la dirección devolverá la dirección del valor de referencia en su lugar), ni tampoco es posible hacer aritmética en las referencias.
A los programadores de C les pueden disgustar las referencias de C ++ ya que ya no serán obvias cuando ocurra la indirección o si un argumento se pasa por valor o por puntero sin mirar las firmas de las funciones.
A los programadores de C ++ les puede disgustar el uso de punteros, ya que se consideran inseguros, aunque las referencias no son más seguras que los punteros constantes, excepto en los casos más triviales, carecen de la conveniencia de la indirección automática y tienen una connotación semántica diferente.
Considere la siguiente declaración de las preguntas frecuentes de C ++ :
Aunque a menudo se implementa una referencia utilizando una dirección en el lenguaje ensamblador subyacente, no piense en una referencia como un puntero de aspecto divertido 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 realmente fuera el objeto, ¿cómo podría haber referencias colgantes? En lenguajes no administrados, es imposible que las referencias sean "más seguras" que los punteros. En general, simplemente no hay una forma de alias confiable de valores a través de los límites del alcance.
Por qué considero útiles las referencias a C ++
Al provenir de un fondo en C, las referencias a C ++ pueden parecer un concepto un tanto tonto, pero aún se deben usar en lugar de indicadores cuando sea posible: la indirección automática es conveniente, y las referencias son especialmente útiles cuando se trata de RAII , pero no debido a la seguridad percibida ventaja, sino más bien porque hacen que escribir código idiomático sea menos incómodo.
RAII es uno de los conceptos centrales de C ++, pero interactúa de manera no trivial con la copia de la semántica. Pasar objetos por referencia evita estos problemas ya que no se trata de copiar. Si las referencias no estuvieran presentes en el idioma, tendría que usar punteros, que son más incómodos de usar, violando así el principio de diseño del lenguaje de que la mejor solución debería ser más fácil que las alternativas.
Sé que las referencias son azúcar sintáctica, por lo que el código es más fácil de leer y escribir.
Pero ¿cuáles son las diferencias?
Resumen de las respuestas y enlaces a continuación:
- Un puntero se puede reasignar cualquier cantidad de veces, mientras que una referencia no se puede reasignar después del enlace.
- Los punteros no pueden apuntar a ninguna parte (
NULL
), mientras que una referencia siempre se refiere a un objeto. - No puede tomar la dirección de una referencia como puede con punteros.
- No hay "aritmética de referencia" (pero puede tomar la dirección de un objeto señalado por una referencia y hacer aritmética de punteros en él como en
&obj + 5
).
Para aclarar una idea falsa:
El estándar de C ++ es muy cuidadoso para evitar dictar cómo un compilador puede implementar referencias, pero cada compilador de C ++ implementa referencias como punteros. Es decir, una declaración como:
int &ri = i;
si no se optimiza por completo , asigna la misma cantidad de almacenamiento que un puntero y coloca la dirección de
i
en ese almacenamiento.
Entonces, tanto el puntero como la referencia usan la misma cantidad de memoria.
Como regla general,
- Utilice referencias en los parámetros de función y tipos de retorno para proporcionar interfaces útiles y autodocumentadas.
- Utilice punteros para implementar algoritmos y estructuras de datos.
LecturA INTERESANTE:
- Mi favorito de todos los tiempos C ++ FAQ lite .
- Referencias contra punteros .
- Una introducción a las referencias .
- Referencias y const .
Diferencia entre puntero y referencia.
Un puntero se puede inicializar a 0 y una referencia no. De hecho, una referencia también debe referirse a un objeto, pero un puntero puede ser el puntero nulo:
int* p = 0;
Pero no podemos tener int& p = 0;
y tambiénint& p=5 ;
.
De hecho, para hacerlo correctamente, debemos haber declarado y definido un objeto al principio, luego podemos hacer una referencia a ese objeto, por lo que la implementación correcta del código anterior será:
Int x = 0;
Int y = 5;
Int& p = x;
Int& p1 = y;
Otro punto importante es que podemos hacer la declaración del puntero sin inicialización, sin embargo, no se puede hacer tal cosa en el caso de una referencia que debe hacer referencia siempre a la variable o al objeto. Sin embargo, tal uso de un puntero es arriesgado, por lo que generalmente verificamos si el puntero en realidad apunta a algo o no. En el caso de una referencia, no es necesaria tal verificación, porque ya sabemos que la referencia a un objeto durante la declaración es obligatoria.
Otra diferencia es que el puntero puede apuntar a otro objeto; sin embargo, la referencia siempre hace referencia al mismo objeto, tomemos este ejemplo:
Int a = 6, b = 5;
Int& rf = a;
Cout << rf << endl; // The result we will get is 6, because rf is referencing to the value of a.
rf = b;
cout << a << endl; // The result will be 5 because the value of b now will be stored into the address of a so the former value of a will be erased
Otro punto: cuando tenemos una plantilla como una plantilla STL, un tipo de plantilla de clase siempre devolverá una referencia, no un puntero, para facilitar la lectura o asignar un nuevo valor utilizando el operador []:
Std ::vector<int>v(10); // Initialize a vector with 10 elements
V[5] = 5; // Writing the value 5 into the 6 element of our vector, so if the returned type of operator [] was a pointer and not a reference we should write this *v[5]=5, by making a reference we overwrite the element by using the assignment "="
Un puntero puede ser reasignado:
int x = 5; int y = 6; int *p; p = &x; p = &y; *p = 10; assert(x == 5); assert(y == 10);
Una referencia no puede y debe asignarse en la inicialización:
int x = 5; int y = 6; int &r = x;
Un puntero tiene su propia dirección de memoria y tamaño en la pila (4 bytes en x86), mientras que una referencia comparte la misma dirección de memoria (con la variable original) pero también ocupa algo de espacio en la pila. Dado que una referencia tiene la misma dirección que la variable original, es seguro pensar en una referencia como otro nombre para la misma variable. Nota: lo que apunta un puntero puede estar en la pila o el montón. Ditto una referencia. Mi afirmación en esta declaración no es que un puntero debe apuntar a la pila. Un puntero es solo una variable que contiene una dirección de memoria. Esta variable está en la pila. Dado que una referencia tiene su propio espacio en la pila, y como la dirección es la misma que la variable a la que hace referencia. Más en la pila vs montón . Esto implica que hay una dirección real de una referencia que el compilador no le dirá.
int x = 0; int &r = x; int *p = &x; int *p2 = &r; assert(p == p2);
Puede tener punteros a punteros a punteros que ofrecen niveles adicionales de direccionamiento indirecto. Mientras que las referencias solo ofrecen un nivel de direccionamiento indirecto.
int x = 0; int y = 0; int *p = &x; int *q = &y; int **pp = &p; pp = &q;//*pp = q **pp = 4; assert(y == 4); assert(x == 0);
El puntero puede asignarse directamente a
nullptr
, mientras que la referencia no puede. Si se esfuerza lo suficiente, y sabe cómo, puede hacer que la dirección de una referencianullptr
. Del mismo modo, si se esfuerza lo suficiente, puede tener una referencia a un puntero, y esa referencia puede contenernullptr
.int *p = nullptr; int &r = nullptr; <--- compiling error int &r = *p; <--- likely no compiling error, especially if the nullptr is hidden behind a function call, yet it refers to a non-existent int at address 0
Los punteros pueden iterar sobre una matriz, puede usar
++
para ir al siguiente elemento al que apunta un puntero y+ 4
para ir al quinto elemento. Esto no importa el tamaño del objeto al que apunta el puntero.Un puntero debe ser referenciado con
*
para acceder a la ubicación de memoria a la que apunta, mientras que una referencia se puede usar directamente. Un puntero a una clase / estructura usa->
para acceder a sus miembros, mientras que una referencia usa a.
.Un puntero es una variable que contiene una dirección de memoria. Independientemente de cómo se implementa una referencia, una referencia tiene la misma dirección de memoria que el elemento al que hace referencia.
Las referencias no se pueden rellenar en una matriz, mientras que los punteros pueden ser (mencionados por el usuario @litb)
Las referencias const pueden estar vinculadas a los temporales. Los punteros no pueden (no sin alguna indirección):
const int &x = int(12); //legal C++ int *y = &int(12); //illegal to dereference a temporary.
Esto hace que
const&
más seguro para su uso en listas de argumentos y así sucesivamente.
Aparte del azúcar sintáctico, una referencia es un puntero de const
( no un puntero a una const
). Debe establecer a qué se refiere cuando declara la variable de referencia y no puede cambiarla más adelante.
Actualización: ahora que lo pienso un poco más, hay una diferencia importante.
El objetivo de un puntero de const se puede reemplazar tomando su dirección y usando una conversión constante.
El objetivo de una referencia no puede ser reemplazado de ninguna manera por debajo de UB.
Esto debería permitir al compilador hacer más optimización en 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 consigues, te arrancarás el pelo tratando de encontrarlo. 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 idiomas. También hay otros tipos de referencias inválidas. Cualquier referencia no válida eleva el espectro del comportamiento indefinido , al igual que usar un puntero no válido.
El error real está en la eliminación de referencias 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 no se hace referencia a un puntero NULO, se bloquea justo en ese punto y no hace falta mucha depuración para averiguarlo.
Mi ejemplo anterior es corto y artificial. Aquí hay un ejemplo más real del mundo.
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 a través de un código con formato incorrecto, y una vez que lo tenga, tendrá un comportamiento indefinido. Nunca tiene sentido verificar una referencia nula; por ejemplo, puedes probar if(&bar==NULL)...
pero el compilador podría optimizar la declaración de la existencia! Una referencia válida nunca puede ser NULA, por lo que, desde el punto de vista del compilador, la comparación siempre es falsa, y es libre de eliminar la cláusula if
como código muerto: esta es la esencia del comportamiento indefinido.
La forma correcta de evitar problemas es evitar la desreferenciación de un puntero NULO 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 ver un aspecto más antiguo de este problema de alguien con mejores habilidades de escritura, vea Referencias nulas de Jim Hyslop y Herb Sutter.
Para ver otro ejemplo de los peligros de anular la referencia a un puntero nulo, consulte Exponer el comportamiento indefinido al intentar portar el código a otra plataforma por Raymond Chen.
En realidad, una referencia no es realmente como un puntero.
Un compilador mantiene "referencias" a las variables, asociando un nombre con una dirección de memoria; ese es su trabajo para traducir cualquier nombre de variable a una dirección de memoria al compilar.
Cuando creas una referencia, solo le dices al compilador que asignas otro nombre a la variable de puntero; es por eso que las referencias no pueden "apuntar a nulo", porque una variable no puede ser, y no puede ser.
Los punteros son variables; contienen la dirección de alguna otra variable, o pueden ser nulas. Lo importante es que un puntero tiene un valor, mientras que una referencia solo tiene una variable a la que hace referencia.
Ahora una explicación del código real:
int a = 0;
int& b = a;
Aquí no estás creando otra variable que apunta a a
; solo está agregando otro nombre al contenido de la memoria que tiene el valor de a
. Esta memoria ahora tiene dos nombres, a
y b
, y puede direccionarse usando cualquiera de los dos nombres.
void increment(int& n)
{
n = n + 1;
}
int a;
increment(a);
Al llamar a una función, el compilador generalmente genera espacios de memoria para los argumentos a copiar. La firma de función define los espacios que se deben crear y le da el nombre que se debe usar para estos espacios. Declarar un parámetro como referencia simplemente le dice al compilador que use el espacio de memoria de la variable de entrada en lugar de asignar un nuevo espacio de memoria durante la llamada al método. Puede parecer extraño decir que su función manipulará directamente una variable declarada en el alcance de la llamada, pero recuerde que al ejecutar el código compilado, no hay más alcance; solo hay una memoria plana y su código de función podría manipular cualquier variable.
Ahora puede haber algunos casos en los que su compilador no pueda conocer la referencia al compilar, como cuando usa una variable externa. Por lo tanto, una referencia puede o no implementarse como un puntero en el código subyacente. Pero en los ejemplos que te di, lo más probable es que no se implemente con un puntero.
Las referencias son muy similares a los punteros, pero están diseñadas específicamente para ser útiles para optimizar los compiladores.
- Las referencias están diseñadas de tal manera que es sustancialmente más fácil para el compilador rastrear qué alias de referencia y qué variables. Dos características principales son muy importantes: no hay "aritmética de referencia" ni reasignación de referencias. Esto permite al compilador averiguar qué alias de referencia hacen referencia las variables en el momento de la compilación.
- Las referencias pueden hacer referencia a variables que no tienen direcciones de memoria, como las que el compilador elige colocar en registros. Si toma la dirección de una variable local, es muy difícil para el compilador colocarla en un registro.
Como ejemplo:
void maybeModify(int& x); // may modify x in some way
void hurtTheCompilersOptimizer(short size, int array[])
{
// This function is designed to do something particularly troublesome
// for optimizers. It will constantly call maybeModify on array[0] while
// adding array[1] to array[2]..array[size-1]. There''s no real reason to
// do this, other than to demonstrate the power of references.
for (int i = 2; i < (int)size; i++) {
maybeModify(array[0]);
array[i] += array[1];
}
}
Un compilador optimizado puede darse cuenta de que estamos accediendo a un [0] y un [1] bastante. Me encantaría optimizar el algoritmo para:
void hurtTheCompilersOptimizer(short size, int array[])
{
// Do the same thing as above, but instead of accessing array[1]
// all the time, access it once and store the result in a register,
// which is much faster to do arithmetic with.
register int a0 = a[0];
register int a1 = a[1]; // access a[1] once
for (int i = 2; i < (int)size; i++) {
maybeModify(a0); // Give maybeModify a reference to a register
array[i] += a1; // Use the saved register value over and over
}
a[0] = a0; // Store the modified a[0] back into the array
}
Para realizar dicha optimización, debe demostrar que nada puede cambiar la matriz [1] durante la llamada. Esto es bastante fácil de hacer. i nunca es menor que 2, por lo que array [i] nunca puede referirse a array [1]. maybeModify () recibe a0 como referencia (matriz de aliasing [0]). Debido a que no hay una aritmética de "referencia", el compilador solo tiene que probar que maybeModify nunca obtiene la dirección de x, y ha demostrado que nada cambia la matriz [1].
También tiene que demostrar que no hay formas de que una futura llamada pueda leer / escribir un [0] mientras tenemos una copia de registro temporal en a0. Esto es a menudo trivial de probar, porque en muchos casos es obvio que la referencia nunca se almacena en una estructura permanente como una instancia de clase.
Ahora haz lo mismo con los punteros.
void maybeModify(int* x); // May modify x in some way
void hurtTheCompilersOptimizer(short size, int array[])
{
// Same operation, only now with pointers, making the
// optimization trickier.
for (int i = 2; i < (int)size; i++) {
maybeModify(&(array[0]));
array[i] += array[1];
}
}
El comportamiento es el mismo; solo que ahora es mucho más difícil probar que maybeModify nunca modifica la matriz [1], porque ya le dimos un puntero; El gato está fuera de la bolsa. Ahora tiene que hacer una prueba mucho más difícil: un análisis estático de maybeModify para probar que nunca escribe en & x + 1. También tiene que demostrar que nunca guarda un puntero que pueda referirse a la matriz [0], que es simplemente tan complicado
Los compiladores modernos están mejorando cada vez más en el análisis estático, pero siempre es bueno ayudarlos y usar referencias.
Por supuesto, salvo tales optimizaciones inteligentes, los compiladores de hecho convertirán las referencias en indicadores cuando sea necesario.
EDITAR: Cinco años después de publicar esta respuesta, encontré una diferencia técnica real donde las referencias son diferentes a una forma diferente de ver el mismo concepto de direccionamiento. Las referencias pueden modificar la vida útil de los objetos temporales de una manera que los punteros no pueden.
F createF(int argument);
void extending()
{
const F& ref = createF(5);
std::cout << ref.getArgument() << std::endl;
};
Normalmente, los objetos temporales como el creado por la llamada a createF(5)
se destruyen al final de la expresión. Sin embargo, al vincular ese objeto a una referencia, ref
, C ++ extenderá la vida útil de ese objeto temporal hasta que la ref
quede fuera de alcance.
Si desea ser realmente pedante, hay una cosa que puede hacer con una referencia que no puede hacer con un puntero: extender la vida útil de un objeto temporal. En C ++, si vincula una referencia constante a un objeto temporal, el tiempo de vida de ese objeto se convierte en el tiempo de vida 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 que es el resultado de la concatenación. Mientras que s3_reference en esencia se convierte en el objeto temporal. Realmente es una referencia a un objeto temporal que ahora tiene la misma duración que la referencia.
Si intentas esto sin la const
, debería fallar la compilación. No puede enlazar una referencia no constante a un objeto temporal, ni tampoco puede tomar su dirección.
Una referencia nunca puede ser NULL
.
A riesgo de agregar confusión, quiero agregar algo de información, estoy seguro de que depende principalmente de cómo el compilador implementa las referencias, pero en el caso de gcc, la idea de que una referencia solo puede apuntar a una variable en la pila. no es realmente correcto, toma esto por ejemplo:
#include <iostream>
int main(int argc, char** argv) {
// Create a string on the heap
std::string *str_ptr = new std::string("THIS IS A STRING");
// Dereference the string on the heap, and assign it to the reference
std::string &str_ref = *str_ptr;
// Not even a compiler warning! At least with gcc
// Now lets try to print it''s value!
std::cout << str_ref << std::endl;
// It works! Now lets print and compare actual memory addresses
std::cout << str_ptr << " : " << &str_ref << std::endl;
// Exactly the same, now remember to free the memory on the heap
delete str_ptr;
}
Que produce esto:
THIS IS A STRING
0xbb2070 : 0xbb2070
¡Si observa que incluso las direcciones de memoria son exactamente iguales, significa que la referencia apunta con éxito a una variable en el montón! Ahora si realmente quieres ponerte raro, esto también funciona:
int main(int argc, char** argv) {
// In the actual new declaration let immediately de-reference and assign it to the reference
std::string &str_ref = *(new std::string("THIS IS A STRING"));
// Once again, it works! (at least in gcc)
std::cout << str_ref;
// Once again it prints fine, however we have no pointer to the heap allocation, right? So how do we free the space we just ignorantly created?
delete &str_ref;
/*And, it works, because we are taking the memory address that the reference is
storing, and deleting it, which is all a pointer is doing, just we have to specify
the address with ''&'' whereas a pointer does that implicitly, this is sort of like
calling delete &(*str_ptr); (which also compiles and runs fine).*/
}
Que produce esto:
THIS IS A STRING
Por lo tanto, una referencia ES un puntero debajo del capó, ambos están simplemente almacenando una dirección de memoria, donde la dirección que apunta es irrelevante, ¿qué crees que pasaría si llamara a std :: cout << str_ref; DESPUÉS de llamar a delete & str_ref? Bueno, obviamente se compila bien, pero causa una falla de segmentación en tiempo de ejecución porque ya no apunta a una variable válida, esencialmente tenemos una referencia rota que aún existe (hasta que queda fuera de alcance), pero es inútil.
En otras palabras, una referencia no es más que un puntero que tiene abstraída la mecánica del puntero, lo que lo hace más seguro y más fácil de usar (sin matemáticas de punteros accidentales, sin mezclar ''.'' Y ''->'', etc.), suponiendo no intentes tonterías como mis ejemplos anteriores;)
Ahora, independientemente de cómo un compilador maneje las referencias, siempre tendrá algún tipo de puntero debajo del capó, porque una referencia debe referirse a una variable específica en una dirección de memoria específica para que funcione como se espera, no hay forma de evitar esto (por lo tanto, el término ''referencia'').
La única regla importante que es importante recordar con referencias es que deben definirse en el momento de la declaración (con la excepción de una referencia en un encabezado, en ese caso, debe definirse en el constructor, una vez que el objeto en el que está contenido es construido es demasiado tarde para definirlo).
Recuerde, mis ejemplos anteriores son solo eso, ejemplos que demuestran qué es una referencia, ¡nunca querría usar una referencia de esa manera! Para el uso adecuado de una referencia, ya hay muchas respuestas aquí que golpean el clavo en la cabeza.
Además, una referencia que es un parámetro de una función que está en línea puede manejarse de manera diferente a un puntero.
void increment(int *ptrint) { (*ptrint)++; }
void increment(int &refint) { refint++; }
void incptrtest()
{
int testptr=0;
increment(&testptr);
}
void increftest()
{
int testref=0;
increment(testref);
}
Muchos compiladores al insertar la versión de puntero uno forzarán realmente una escritura en la memoria (estamos tomando la dirección explícitamente). Sin embargo, dejarán la referencia en un registro que sea más óptimo.
Por supuesto, para las funciones que no están en línea, el puntero y la referencia generan el mismo código y siempre es mejor pasar valores intrínsecos por valor que por referencia si no son modificados y devueltos por la función.
Existe una diferencia no técnica muy importante entre los punteros y las referencias: un argumento que se pasa a una función mediante un puntero es mucho más visible que un argumento que se pasa a una función por una referencia no constante. Por ejemplo:
void fn1(std::string s);
void fn2(const std::string& s);
void fn3(std::string& s);
void fn4(std::string* s);
void bar() {
std::string x;
fn1(x); // Cannot modify x
fn2(x); // Cannot modify x (without const_cast)
fn3(x); // CAN modify x!
fn4(&x); // Can modify x (but is obvious about it)
}
De vuelta en C, una llamada que parece fn(x)
solo puede pasarse por valor, por lo que definitivamente no se puede modificar x
; para modificar un argumento necesitarías pasar un puntero fn(&x)
. Entonces, si un argumento no estuviera precedido por un &
sabías, no se modificaría. (Lo contrario, &
significa que se modificó, no era cierto porque a veces tendría que pasar grandes estructuras de solo lectura con el const
puntero).
Algunos argumentan que esta es una característica tan útil al leer el código, que los parámetros de puntero siempre deben usarse para parámetros modificables en lugar de no const
referencias, incluso si la función nunca espera a nullptr
. Es decir, esas personas argumentan que fn3()
no deben permitirse las firmas de funciones como la anterior. Las pautas de estilo de C ++ de Google son un ejemplo de esto.
Existe una diferencia semántica que puede parecer esotérica si no está familiarizado con el estudio de los lenguajes informáticos de forma abstracta o incluso académica.
En el nivel más alto, la idea de las referencias es que son transparentes "alias". Su computadora puede usar una dirección para hacer que funcionen, pero se supone que no debe preocuparse por eso: se supone que debe pensar en ellos como "solo otro nombre" para un objeto existente y la sintaxis lo refleja. Son más estrictos que los punteros, por lo que su compilador puede advertirle de manera más confiable cuando está a punto de crear una referencia colgante, que cuando está a punto de crear un puntero colgante.
Más allá de eso, hay, por supuesto, algunas diferencias prácticas entre los punteros y las referencias. La sintaxis para usarlos es obviamente diferente, y no puede "volver a colocar" las referencias, tener referencias a la nada, o apuntar a referencias.
No importa la cantidad de espacio que ocupe, ya que realmente no puede ver ningún efecto secundario (sin ejecutar código) del espacio que ocuparía.
Por otro lado, una diferencia importante entre las referencias y los punteros es que los temporales asignados a las referencias const se mantienen en vivo hasta que la referencia de const se sale del alcance.
Por ejemplo:
class scope_test
{
public:
~scope_test() { printf("scope_test done!/n"); }
};
...
{
const scope_test &test= scope_test();
printf("in scope/n");
}
imprimirá:
in scope
scope_test done!
Este es el mecanismo del lenguaje que permite que ScopeGuard funcione.
Otra diferencia es que puede tener punteros a un tipo de vacío (y significa puntero a cualquier cosa) pero las referencias a vacío están prohibidas.
int a;
void * p = &a; // ok
void & p = a; // forbidden
No puedo decir que estoy realmente feliz con esta diferencia particular. Preferiría que se permitiera con el significado de referencia a cualquier cosa con una dirección y, por lo demás, el mismo comportamiento para las referencias. Permitiría definir algunos equivalentes de las funciones de la biblioteca C como memcpy usando referencias.
Otro uso interesante de las referencias es proporcionar un argumento predeterminado de un tipo definido por el usuario:
class UDT
{
public:
UDT() : val_d(33) {};
UDT(int val) : val_d(val) {};
virtual ~UDT() {};
private:
int val_d;
};
class UDT_Derived : public UDT
{
public:
UDT_Derived() : UDT() {};
virtual ~UDT_Derived() {};
};
class Behavior
{
public:
Behavior(
const UDT &udt = UDT()
) {};
};
int main()
{
Behavior b; // take default
UDT u(88);
Behavior c(u);
UDT_Derived ud;
Behavior d(ud);
return 1;
}
El sabor predeterminado utiliza el aspecto ''vincular const de un temporal'' de las referencias.
Siento que hay otro punto que no ha sido cubierto aquí.
A diferencia de los punteros, las referencias son sintácticamente equivalentes al objeto al que hacen referencia, es decir, cualquier operación que se pueda aplicar a un objeto funciona para una referencia, y con la misma sintaxis (la excepción es la inicialización).
Si bien esto puede parecer superficial, creo que esta propiedad es crucial para varias funciones de C ++, por ejemplo:
Plantillas . Como los parámetros de la plantilla son tipográficos, las propiedades sintácticas de un tipo es lo único que importa, por lo que a menudo se puede usar la misma plantilla con
T
yT&
.
(ostd::reference_wrapper<T>
que aún se basa en una conversión implícita aT&
)
Plantillas que cubren ambasT&
yT&&
son aún más comunes.Lvalues . Considere la declaración
str[0] = ''X'';
Sin referencias, solo funcionaría para c-strings (char* str
). Devolver el carácter por referencia permite que las clases definidas por el usuario tengan la misma notación.Copiar constructores . Sintácticamente tiene sentido pasar objetos para copiar constructores, y no punteros a objetos. Pero simplemente no hay forma de que un constructor de copia tome un objeto por valor, lo que daría lugar a una llamada recursiva al mismo constructor de copia. Esto deja las referencias como la única opción aquí.
Sobrecargas del operador . Con las referencias es posible introducir el direccionamiento indirecto a una llamada de operador, por ejemplo,
operator+(const T& a, const T& b)
manteniendo la misma notación de infijo. Esto también funciona para funciones sobrecargadas regulares.
Estos puntos dan poder a una parte considerable de C ++ y la biblioteca estándar, por lo que esta es una propiedad importante de las referencias.
Uso referencias a menos que necesite alguno de estos:
Los punteros nulos pueden usarse como un valor centinela, a menudo una forma barata de evitar la sobrecarga de funciones o el uso de un bool.
Puedes hacer aritmética en un puntero. Por ejemplo,
p += offset;
Este programa puede ayudar a comprender la respuesta de la pregunta. Este es un programa simple de una referencia "j" y un puntero "ptr" que apunta a la variable "x".
#include<iostream>
using namespace std;
int main()
{
int *ptr=0, x=9; // pointer and variable declaration
ptr=&x; // pointer to variable "x"
int & j=x; // reference declaration; reference to variable "x"
cout << "x=" << x << endl;
cout << "&x=" << &x << endl;
cout << "j=" << j << endl;
cout << "&j=" << &j << endl;
cout << "*ptr=" << *ptr << endl;
cout << "ptr=" << ptr << endl;
cout << "&ptr=" << &ptr << endl;
getch();
}
Ejecute el programa y eche un vistazo a la salida y lo entenderá.
Además, dedique 10 minutos y vea este video: https://www.youtube.com/watch?v=rlJrrGV0iOg
Esto se basa en el tutorial . Lo que está escrito lo hace más claro:
>>> The address that locates a variable within memory is
what we call a reference to that variable. (5th paragraph at page 63)
>>> The variable that stores the reference to another
variable is what we call a pointer. (3rd paragraph at page 64)
Simplemente para recordar eso,
>>> reference stands for memory location
>>> pointer is a reference container (Maybe because we will use it for
several times, it is better to remember that reference.)
Además, como podemos referirnos a casi cualquier tutorial de punteros, un puntero es un objeto que es compatible con la aritmética de punteros que hace que el puntero sea similar a una matriz.
Mira la siguiente declaración,
int Tom(0);
int & alias_Tom = Tom;
alias_Tom
Puede entenderse como un alias of a variable
(diferente con typedef
, que es alias of a type
) Tom
. También está bien olvidar que la terminología de dicha declaración es crear una referencia de Tom
.
Hay una diferencia fundamental entre los punteros y las referencias que no vi que nadie haya mencionado: las referencias permiten la semántica paso por referencia en los argumentos de función. Los punteros, aunque no son visibles al principio, no lo hacen: solo proporcionan semántica de paso por valor. Esto ha sido muy bien descrito en este artículo .
Saludos, y rzej
La diferencia es que la variable de puntero no constante (que no debe confundirse con un puntero a constante) puede cambiarse en algún momento durante la ejecución del programa, requiere que se use la semántica de puntero (&, *) operadores, mientras que las referencias se pueden establecer durante la inicialización solo (por eso puede establecerlos solo en la lista de inicializadores de constructores, pero no de otra manera) y usar el valor ordinario para acceder a la semántica. Básicamente, se introdujeron referencias para permitir el soporte para sobrecargas de operadores como había leído en un libro muy antiguo. Como alguien dijo en este hilo, el puntero se puede establecer en 0 o en el valor que desee. 0 (NULL, nullptr) significa que el puntero se inicializa con nada. Es un error anular la referencia al puntero nulo. Pero en realidad, el puntero puede contener un valor que no apunta a alguna ubicación de memoria correcta.A su vez, las referencias intentan no permitir que un usuario inicialice una referencia a algo a lo que no se puede hacer referencia debido al hecho de que siempre le proporciona un valor del tipo correcto. Aunque hay muchas formas de hacer que la variable de referencia se inicialice en una ubicación de memoria incorrecta, es mejor que no profundice en los detalles. En el nivel de la máquina, tanto el puntero como el trabajo de referencia son uniformes, a través de punteros. Digamos que en las referencias esenciales se encuentra el azúcar sintáctico. Las referencias de valores son diferentes a esto: naturalmente son objetos de pila / pila.Aunque hay muchas formas de hacer que la variable de referencia se inicialice en una ubicación de memoria incorrecta, es mejor que no profundice en los detalles. En el nivel de la máquina, tanto el puntero como el trabajo de referencia son uniformes, a través de punteros. Digamos que en las referencias esenciales se encuentra el azúcar sintáctico. Las referencias de valores son diferentes a esto: naturalmente son objetos de pila / pila.Aunque hay muchas formas de hacer que la variable de referencia se inicialice en una ubicación de memoria incorrecta, es mejor que no profundice en los detalles. En el nivel de la máquina, tanto el puntero como el trabajo de referencia son uniformes, a través de punteros. Digamos que en las referencias esenciales se encuentra el azúcar sintáctico. Las referencias de valores son diferentes a esto: naturalmente son objetos de pila / pila.
Si bien tanto las referencias como los punteros se utilizan para acceder indirectamente a otro valor, existen dos diferencias importantes entre las referencias y los punteros. La primera es que una referencia siempre se refiere a un objeto: es un error definir una referencia sin inicializarlo. El comportamiento de la asignación es la segunda diferencia importante: la asignación a una referencia cambia el objeto al que está vinculada la referencia; no vuelve a enlazar la referencia a otro objeto. Una vez inicializada, una referencia siempre se refiere al mismo objeto subyacente.
Considere estos dos fragmentos del programa. En la primera, asignamos un puntero a otro:
int ival = 1024, ival2 = 2048;
int *pi = &ival, *pi2 = &ival2;
pi = pi2; // pi now points to ival2
Después de la asignación, ival, el objeto direccionado por pi permanece sin cambios. La asignación cambia el valor de pi, por lo que apunta a un objeto diferente. Ahora considere un programa similar que asigna dos referencias:
int &ri = ival, &ri2 = ival2;
ri = ri2; // assigns ival2 to ival
Esta asignación cambia ival, el valor referenciado por ri, y no la referencia en sí. Después de la asignación, las dos referencias aún se refieren a sus objetos originales, y el valor de esos objetos ahora también es el mismo.
Siempre decido por this regla de C ++ Core Guidelines:
Prefiero T * sobre T y cuando "sin argumento" es una opción válida
Tal vez algunas metáforas ayuden; En el contexto de su pantalla de escritorio -
- Una referencia requiere que especifique una ventana real.
- Un puntero requiere la ubicación de un espacio en la pantalla para asegurar que contendrá cero o más instancias de ese tipo de ventana.
Tanto las referencias como los punteros se pueden usar para cambiar las variables locales de una función dentro de otra función. Ambos también se pueden usar para guardar la copia de objetos grandes cuando se pasan como argumentos a funciones o se devuelven desde funciones, para obtener mayor eficiencia. A pesar de las similitudes anteriores, hay diferencias siguientes entre referencias y punteros.
Las referencias son menos poderosas que los punteros.
1) Una vez que se crea una referencia, no se puede hacer posteriormente para hacer referencia a otro objeto; no se puede volver a colocar Esto se hace a menudo con punteros.
2) Las referencias no pueden ser NULL. Los punteros a menudo se hacen NULOS para indicar que no están apuntando a ninguna cosa válida.
3) Una referencia debe ser inicializada cuando se declara. No hay tal restricción con punteros
Debido a las limitaciones anteriores, las referencias en C ++ no se pueden usar para implementar estructuras de datos como Lista enlazada, Árbol, etc. En Java, las referencias no tienen las restricciones anteriores y se pueden usar para implementar todas las estructuras de datos. Las referencias son más poderosas en Java, es la razón principal por la que Java no necesita punteros.
Las referencias son más seguras y fáciles de usar:
1) Más seguro: ya que las referencias deben inicializarse, es poco probable que existan referencias salvajes como punteros de comodín. Todavía es posible tener referencias que no se refieren a una ubicación válida
2) Más fácil de usar: las referencias no necesitan un operador de referencia para acceder al valor. Se pueden utilizar como variables normales. El operador ''&'' solo es necesario en el momento de la declaración. Además, se puede acceder a los miembros de una referencia de objeto con el operador de punto (''.''), A diferencia de los punteros donde se necesita el operador de flecha (->) para acceder a los miembros.
Junto con las razones anteriores, hay algunos lugares como el argumento del constructor de copia donde no se puede usar el puntero. Referencia debe ser utilizada pasar el argumento en el constructor de copia. Se deben usar referencias similares para sobrecargar algunos operadores como ++ .
Una referencia a un puntero es posible en C ++, pero lo contrario no es posible significa que no es posible un puntero a una referencia. Una referencia a un puntero proporciona una sintaxis más limpia para modificar el puntero. Mira este ejemplo:
#include<iostream>
using namespace std;
void swap(char * &str1, char * &str2)
{
char *temp = str1;
str1 = str2;
str2 = temp;
}
int main()
{
char *str1 = "Hi";
char *str2 = "Hello";
swap(str1, str2);
cout<<"str1 is "<<str1<<endl;
cout<<"str2 is "<<str2<<endl;
return 0;
}
Y considere la versión C del programa anterior. En C, tiene que usar el puntero al puntero (direccionamiento múltiple), lo que lleva a la confusión y el programa puede parecer complicado.
#include<stdio.h>
/* Swaps strings by swapping pointers */
void swap1(char **str1_ptr, char **str2_ptr)
{
char *temp = *str1_ptr;
*str1_ptr = *str2_ptr;
*str2_ptr = temp;
}
int main()
{
char *str1 = "Hi";
char *str2 = "Hello";
swap1(&str1, &str2);
printf("str1 is %s, str2 is %s", str1, str2);
return 0;
}
Visite lo siguiente para obtener más información sobre la referencia al puntero:
Como dije, un puntero a una referencia no es posible. Prueba el siguiente programa:
#include <iostream>
using namespace std;
int main()
{
int x = 10;
int *ptr = &x;
int &*ptr1 = ptr;
}
Una referencia es un alias para otra variable, mientras que un puntero contiene la dirección de memoria de una variable. Las referencias se utilizan generalmente como parámetros de función para que el objeto pasado no sea la copia sino el objeto en sí.
void fun(int &a, int &b); // A common usage of references.
int a = 0;
int &b = a; // b is an alias for a. Not so common to use.
Una referencia no es otro nombre dado a alguna memoria. Es un puntero inmutable que se elimina automáticamente en el uso. Básicamente se reduce a:
int& j = i;
Se convierte internamente
int* const j = &i;