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?
int &x = *(int*)0;
en gcc. De hecho, la referencia puede apuntar a NULL.
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?
int &x = *(int*)0;
en gcc. De hecho, la referencia puede apuntar a NULL.
Respuestas:
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 puede y debe asignarse en la inicialización:
int x = 5;
int y = 6;
int &r = x;
Un puntero tiene su propia dirección y tamaño de memoria 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 en sí, 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. Lo mismo 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 dado que la dirección es la misma que la variable a la que hace referencia. Más información sobre stack 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 ofrezcan niveles adicionales de indirección. Mientras que las referencias solo ofrecen un nivel de indirección.
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);
Se puede asignar un puntero nullptr
directamente, mientras que la referencia no. Si se esfuerza lo suficiente y sabe cómo, puede hacer la dirección de una referencia nullptr
. Del mismo modo, si se esfuerza lo suficiente, puede tener una referencia a un puntero, y luego esa referencia puede contener nullptr
.
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 e + 4
ir al quinto elemento. Esto no importa el tamaño del objeto al que apunta el puntero.
Se debe desreferenciar un puntero *
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 .
.
Las referencias no pueden rellenarse en una matriz, mientras que los punteros pueden (Mencionado por el usuario @litb)
Las referencias constantes pueden estar vinculadas a 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 lo hace const&
más seguro para su uso en listas de argumentos, etc.
Una referencia puede considerarse como un puntero constante (¡no debe confundirse con un puntero a un valor constante!) Con indirección 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á. Tampoco es posible obtener la dirección de una referencia; en su lugar, el operador de la dirección devolverá la dirección del valor referenciado, ni es posible realizar operaciones aritméticas en las referencias.
A los programadores de C no les gustarán las referencias de C ++, ya que ya no será obvio cuando se produce la indirección o si se pasa un argumento por valor o por puntero sin mirar las firmas de funciones.
Los programadores de C ++ pueden no gustar el uso de punteros, ya que 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 indirección 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 usando una dirección en el lenguaje ensamblador subyacente, no piense en una referencia como un puntero de aspecto divertido para un objeto. Una referencia es el objeto. No es un puntero al objeto, ni una copia del objeto. Es es el objeto.
Pero si una referencia fuera realmente el objeto, ¿cómo podría haber referencias colgantes? En los idiomas no administrados, es imposible que las referencias sean más "seguras" que los punteros; en general, ¡no existe una forma de alias confiable de los valores a través de los límites del alcance!
Viniendo de un fondo de C, las referencias de C ++ pueden parecer un concepto un tanto tonto, pero uno debería usarlas en lugar de punteros cuando sea posible: la indirección automática es conveniente, y las referencias se vuelven 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 implica ninguna copia. Si las referencias no estuvieran presentes en el idioma, tendría que usar punteros, que son más engorrosos de usar, lo que viola 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 que es el resultado de la concatenación. Mientras que s3_reference en esencia se convierte en el objeto temporal. Es realmente una referencia a un objeto temporal que ahora tiene la misma vida útil que la referencia.
Si intenta esto sin, const
no debería compilarse. No puede vincular una referencia no constante a un objeto temporal, ni puede tomar su dirección para el caso.
const &
enlace, y solo cuando la referencia queda fuera del alcance se llama al destructor del tipo de referencia real (en comparación con el tipo de referencia, que podría ser una base). Como se trata de una referencia, no se realizarán cortes en el medio.
Animal x = fast ? getHare() : getTortoise()
, x
se enfrentará al clásico problema de corte, mientras Animal& x = ...
que funcionará correctamente.
Además del azúcar sintáctico, una referencia es un const
puntero ( no un puntero a const
). Debe establecer a qué se refiere cuando declara la variable de referencia, y no puede cambiarla más tarde.
Actualización: ahora que lo pienso un poco más, hay una diferencia importante.
El objetivo de un puntero constante puede reemplazarse tomando su dirección y usando un reparto constante.
El objetivo de una referencia no se puede reemplazar de ninguna manera por debajo de UB.
Esto debería permitir que el compilador optimice más una referencia.
T* const
con un azúcar sintáctico diferente (lo que elimina una gran cantidad de * y & de su código).
int i; int const *pci = &i; /* implicit conv to const int* */ int *pi = const_cast<int*>(pci);
está bien.
Contrariamente a la opinión popular, es posible tener una referencia NULL.
int * p = NULL;
int & r = *p;
r = 1; // crash! (if you're lucky)
De acuerdo, es mucho más difícil hacerlo con una referencia, pero si lo logras, 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 lenguajes. También hay otros tipos de referencias no válidas. Cualquier referencia inválida aumenta el espectro de comportamiento indefinido , tal como lo haría un puntero invá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 desreferencia un puntero NULL, se bloquea justo en ese punto y no se necesita mucha depuración para descubrirlo.
Mi ejemplo anterior es breve y artificial. Aquí hay un ejemplo más del mundo 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 a través de un código con formato incorrecto, y una vez que lo tiene, obtiene un comportamiento indefinido. Que no tiene sentido para comprobar si hay una referencia nula; por ejemplo, puede intentarlo, ¡ if(&bar==NULL)...
pero el compilador podría optimizar la declaración fuera de existencia! 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 muerto; esta es la esencia del comportamiento indefinido.
La manera correcta de evitar problemas es evitar desreferenciar 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 una visión más antigua de este problema de alguien con mejores habilidades de escritura, vea Referencias nulas de Jim Hyslop y Herb Sutter.
Para otro ejemplo de los peligros de desreferenciar un puntero nulo, vea Exponer el comportamiento indefinido cuando Raymond Chen intenta portar código a otra plataforma .
Olvidaste la parte más importante:
acceso de miembros con punteros usa ->
acceso de miembros con referencias usa.
foo.bar
es claramente superior de foo->bar
la misma manera que vi es claramente superior a Emacs :-)
->
proporciona referencias a los punteros, al igual que con el puntero mismo.
.
y ->
tiene algo que ver con vi y emacs :)
.
es mejor que usar ->
, pero al igual que vi vs emacs, es completamente subjetivo y no se puede probar nada
Las referencias son muy similares a los punteros, pero están diseñados específicamente para ayudar a optimizar los compiladores.
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 optimizador puede darse cuenta de que estamos accediendo a un [0] y a [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 (aliasing array [0]). Debido a que no existe una aritmética de "referencia", el compilador solo tiene que demostrar que tal vez Modify 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 llamada futura pueda leer / escribir a [0] mientras tengamos 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 demostrar que maybeModify nunca modifica la matriz [1], porque ya le dimos un puntero; El gato está fuera de la bolsa. Ahora tiene que hacer la prueba mucho más difícil: un análisis estático de maybeModify para demostrar que nunca escribe en & x + 1. También tiene que demostrar que nunca ahorra un puntero que pueda referirse a la matriz [0], que es solo 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 convertirán referencias en punteros cuando sea necesario.
EDITAR: Cinco años después de publicar esta respuesta, encontré una diferencia técnica real en la que 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 ref
quede fuera de alcance.
maybeModify
no toma la dirección de nada relacionado x
es sustancialmente más fácil que probar que no se produce un montón de aritmética de puntero.
void maybeModify(int& x) { 1[&x]++; }
que están discutiendo los otros comentarios anteriores
En realidad, una referencia no es realmente como un puntero.
Un compilador mantiene "referencias" a variables, asociando un nombre con una dirección de memoria; ese es su trabajo traducir cualquier nombre de variable a una dirección de memoria al compilar.
Cuando crea una referencia, solo le dice al compilador que le asigna 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 ser.
Los punteros son variables; contienen la dirección de alguna otra variable, o pueden ser nulos. Lo importante es que un puntero tiene un valor, mientras que una referencia solo tiene una variable a la que hace referencia.
Ahora alguna explicación del código real:
int a = 0;
int& b = a;
Aquí no está creando otra variable que apunta a a
; solo está agregando otro nombre al contenido de la memoria que contiene el valor de a
. Esta memoria ahora tiene dos nombres, a
y b
, y se puede abordar con cualquiera de los nombres.
void increment(int& n)
{
n = n + 1;
}
int a;
increment(a);
Cuando se llama a una función, el compilador generalmente genera espacios de memoria para copiar los argumentos. La firma de la función define los espacios que se deben crear y da el nombre que se debe usar para estos espacios. Declarar un parámetro como referencia solo 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 código compilado, no hay más alcance; simplemente hay 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 implementarse o no como puntero en el código subyacente. Pero en los ejemplos que le di, lo más probable es que no se implemente con un puntero.
Una referencia nunca puede ser NULL
.
void Foo::bar() { virtual_baz(); }
ese segfaults. Si no sabe que las referencias pueden ser nulas, no puede rastrear el nulo hasta su origen.
int &r=*p;
es un comportamiento indefinido. En ese momento, usted no tiene un "señalador referencia a NULL," usted tiene un programa que ya no se puede razonar acerca en absoluto .
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 inicializarla. 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 vincular la referencia a otro objeto. Una vez inicializada, una referencia siempre se refiere al mismo objeto subyacente.
Considere estos dos fragmentos de programa. En el primero, 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 dirigido por pi permanece sin cambios. La asignación cambia el valor de pi, haciendo que apunte 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 al que hace referencia ri, y no la referencia en sí. Después de la asignación, las dos referencias todavía se refieren a sus objetos originales, y el valor de esos objetos ahora también es el mismo.
Existe una diferencia semántica que puede parecer esotérica si no está familiarizado con el estudio de lenguajes de computadora de manera abstracta o incluso académica.
En el nivel más alto, la idea de referencias es que son "alias" transparentes. Su computadora puede usar una dirección para que funcionen, pero no debe preocuparse por eso: 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 punteros y referencias. La sintaxis para usarlos es obviamente diferente, y no puede "recolocar" referencias, tener referencias a la nada o tener punteros a las referencias.
Una referencia es un alias para otra variable, mientras que un puntero contiene la dirección de memoria de una variable. Las referencias se usan generalmente como parámetros de función para que el objeto pasado no sea la copia sino el objeto mismo.
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.
No importa cuánto espacio ocupe, ya que no puede ver ningún efecto secundario (sin ejecutar el código) de cualquier espacio que ocupe.
Por otro lado, una diferencia importante entre las referencias y los punteros es que los temporales asignados a las referencias constantes viven hasta que la referencia constante 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 de lenguaje que permite que ScopeGuard funcione.
Esto se basa en el tutorial . Lo que está escrito lo deja 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 puntero, un puntero es un objeto soportado por la aritmética del puntero 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
.
nullptr
? ¿Has leído alguna otra parte de este hilo o ...?
Una referencia no es otro nombre dado a alguna memoria. Es un puntero inmutable que se des-referencia automáticamente en el uso. Básicamente se reduce a:
int& j = i;
Internamente se convierte
int* const j = &i;
const
puntero. Esa flexibilidad no prueba que haya una diferencia entre una referencia y un puntero.
¿Qué es una referencia en C ++? Alguna instancia específica de tipo que no es un tipo de objeto .
¿Qué es un puntero en C ++? Alguna instancia específica de tipo que es un tipo de objeto .
De la definición ISO C ++ de tipo de objeto :
Un tipo de objeto es un tipo (posiblemente calificado por cv ) que no es un tipo de función, no es un tipo de referencia y no es cv void.
Puede ser importante saber que el tipo de objeto es una categoría de nivel superior del universo tipo en C ++. La referencia también es una categoría de nivel superior. Pero el puntero no lo es.
Los punteros y las referencias se mencionan juntos en el contexto del tipo compuesto . Esto se debe básicamente a la naturaleza de la sintaxis del declarador heredada de (y extendida) C, que no tiene referencias. (Además, hay más de un tipo de declarador de referencias desde C ++ 11, mientras que los punteros todavía están "unidos": &
+ &&
vs *
..) Por lo tanto, redactar un lenguaje específico por "extensión" con un estilo similar de C en este contexto es algo razonable . (Todavía voy a argumentar que la sintaxis de desechos declaradores la expresividad sintáctica mucho , hace que tanto los usuarios humanos y las implementaciones frustrante. Por lo tanto, todos ellos no están calificados para ser incorporadaen un nuevo diseño de lenguaje. Sin embargo, este es un tema totalmente diferente sobre el diseño PL).
De lo contrario, es insignificante que los punteros puedan calificarse como un tipo específico de tipos con referencias juntas. Simplemente comparten muy pocas propiedades comunes además de la similitud de sintaxis, por lo que no es necesario juntarlas en la mayoría de los casos.
Tenga en cuenta que las declaraciones anteriores solo mencionan "punteros" y "referencias" como tipos. Hay algunas preguntas interesantes sobre sus instancias (como las variables). También vienen demasiados conceptos erróneos.
Las diferencias de las categorías de nivel superior ya pueden revelar muchas diferencias concretas que no están vinculadas directamente a los punteros:
cv
calificadores de nivel superior . Las referencias no pueden.Algunas reglas especiales más sobre referencias:
&&
parámetros (como las "referencias de reenvío") basadas en el colapso de referencias durante la deducción de parámetros de plantilla permiten el "reenvío perfecto" de parámetros.std::initializer_list
algunas reglas similares de extensión de la vida útil de referencia. Es otra lata de gusanos.Sé que las referencias son azúcar sintáctica, por lo que el código es más fácil de leer y escribir.
Técnicamente, esto es completamente incorrecto. Las referencias no son azúcar sintáctica de ninguna otra característica en C ++, porque no pueden ser reemplazadas exactamente por otras características sin ninguna diferencia semántica.
(Del mismo modo, las expresiones lambda no son azúcar sintáctica de ninguna otra característica en C ++ porque no se pueden simular con precisión con propiedades "no especificadas" como el orden de declaración de las variables capturadas , lo que puede ser importante porque el orden de inicialización de tales variables puede ser significativo.)
C ++ solo tiene unos pocos tipos de azúcares sintácticos en este sentido estricto. Una instancia es (heredada de C) el operador incorporado (no sobrecargado) []
, que se define exactamente con las mismas propiedades semánticas de formas específicas de combinación sobre el operador incorporado unario *
y binario+
.
Entonces, un puntero y una referencia usan la misma cantidad de memoria.
La declaración anterior es simplemente incorrecta. Para evitar tales conceptos erróneos, mire las reglas ISO C ++ en su lugar:
Desde [intro.object] / 1 :
... Un objeto ocupa una región de almacenamiento en su período de construcción, a lo largo de su vida útil y en su período de destrucción. ...
Desde [dcl.ref] / 4 :
No se especifica si una referencia requiere almacenamiento o no.
Tenga en cuenta que estas son propiedades semánticas .
Incluso si los punteros no están lo suficientemente calificados como para combinarlos con referencias en el sentido del diseño del lenguaje, todavía hay algunos argumentos que hacen que sea discutible elegir entre ellos en otros contextos, por ejemplo, al elegir opciones sobre tipos de parámetros.
Pero esta no es toda la historia. Quiero decir, hay más cosas que punteros frente a referencias que debes considerar.
Si no tiene que apegarse a esas elecciones demasiado específicas, en la mayoría de los casos la respuesta es breve: no tiene la necesidad de usar punteros, por lo que no es necesario . Los punteros suelen ser lo suficientemente malos porque implican demasiadas cosas que no esperas y dependerán de demasiados supuestos implícitos que socavan la mantenibilidad e (incluso) portabilidad del código. Innecesariamente depender de punteros es definitivamente un mal estilo y debe evitarse en el sentido de C ++ moderno. Reconsidere su propósito y finalmente encontrará que el puntero es la característica de los últimos tipos en la mayoría de los casos.
&
tipo de referencia como el primer tipo de parámetro. (Y por lo general, debe estar const
calificado).&&
tipo de referencia como el primer tipo de parámetro. (Y generalmente no debería haber calificadores).operator=
como funciones miembro especiales requiere tipos de referencia similares al primer parámetro de los constructores copiar / mover.++
requiere dummy int
.unique_ptr
y shared_ptr
(o incluso con los homebrew solo si necesita que sean opacos ), en lugar de punteros sin procesar.std::optional
, en lugar de punteros sin formato.observer_ptr
en Library Fundamental TS.Las únicas excepciones no se pueden solucionar en el idioma actual:
operator new
. (Sin embargo, cv - void*
sigue siendo bastante diferente y más seguro en comparación con los punteros de objetos ordinarios porque descarta la aritmética de punteros inesperada a menos que esté confiando en alguna extensión no conforme void*
como GNU).Entonces, en la práctica, la respuesta es tan obvia: en caso de duda, evite los punteros . Debe usar punteros solo cuando haya razones muy explícitas de que nada más es más apropiado. Excepto algunos casos excepcionales mencionados anteriormente, tales elecciones casi siempre no son puramente específicas de C ++ (pero es probable que sean específicas de la implementación del lenguaje). Tales instancias pueden ser:
Si llega a ver la pregunta a través de algún resultado de búsqueda de Google (no específico de C ++) , es muy probable que este sea el lugar equivocado.
Las referencias en C ++ es bastante "raro", ya que no es esencialmente de primera clase: van a ser tratados como los objetos o las funciones que se refiere a lo que no tienen la oportunidad de apoyar algunas operaciones de primera clase como ser el operando de la izquierda de la operador de acceso de miembros independientemente del tipo del objeto referido. Otros idiomas pueden o no tener restricciones similares en sus referencias.
Las referencias en C ++ probablemente no conservarán el significado en diferentes lenguajes. Por ejemplo, las referencias en general no implican propiedades no nulas en valores como ellos en C ++, por lo que tales suposiciones pueden no funcionar en otros lenguajes (y encontrará contraejemplos con bastante facilidad, por ejemplo, Java, C #, ...).
Todavía puede haber algunas propiedades comunes entre las referencias en diferentes lenguajes de programación en general, pero dejemos algunas otras preguntas en SO.
(Una nota al margen: la pregunta puede ser significativa antes de cualquier lenguaje "similar a C", como ALGOL 68 vs. PL / I ).
Una referencia a un puntero es posible en C ++, pero lo contrario no es posible significa que un puntero a una referencia no es posible. 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, debe usar puntero a puntero (indirección múltiple), y esto genera 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;
}
Uso referencias a menos que necesite alguno de estos:
Los punteros nulos se pueden usar como 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;
&r + offset
donde r
se declaró como referencia
Hay una diferencia fundamental entre punteros y referencias que no vi que nadie hubiera mencionado: las referencias permiten la semántica de paso por referencia en los argumentos de la función. Los punteros, aunque no es visible al principio, no lo hacen: solo proporcionan una semántica de paso por valor. Esto ha sido muy bien descrito en este artículo .
Saludos, y rzej
A riesgo de aumentar la confusión, quiero agregar algo de entrada, estoy seguro de que depende principalmente de cómo el compilador implemente las referencias, pero en el caso de gcc, la idea de que una referencia solo puede apuntar a una variable en la pila en realidad no es correcto, tome 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;
}
Lo que genera esto:
THIS IS A STRING
0xbb2070 : 0xbb2070
Si observa que incluso las direcciones de memoria son exactamente iguales, lo que 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).*/
}
Lo que genera esto:
THIS IS A STRING
Por lo tanto, una referencia ES un puntero debajo del capó, ambos solo están almacenando una dirección de memoria, donde la dirección apunta es irrelevante, ¿qué crees que pasaría si llamara 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 todavía existe (hasta que cae fuera del alcance), pero es inútil.
En otras palabras, una referencia no es más que un puntero que tiene la mecánica del puntero abstraída, lo que lo hace más seguro y fácil de usar (sin matemática accidental del puntero, sin mezclar '.' Y '->', etc.), suponiendo que no intentes ninguna tontería como mis ejemplos anteriores;)
Ahora, independientemente de cómo un compilador maneja 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 esperaba, no hay forma de evitar esto (por lo tanto el término 'referencia').
La única regla importante que es importante recordar con las 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, después de que el objeto en el que se encuentra está construido es demasiado tarde para definirlo).
Recuerde, mis ejemplos anteriores son solo eso, ejemplos que demuestran lo que es una referencia, ¡nunca querrá usar una referencia de esa manera! Para el uso adecuado de una referencia, aquí hay muchas respuestas que dan en el clavo
Otra diferencia es que puede tener punteros a un tipo 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 en particular. Preferiría que se permitiera con la referencia del significado a cualquier cosa con una dirección y, de lo contrario, el mismo comportamiento para las referencias. Permitiría definir algunos equivalentes de las funciones de la biblioteca C como memcpy usando referencias.
Además, una referencia que es un parámetro a una función que está en línea puede manejarse de manera diferente que 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 alinear la versión del puntero en realidad forzarán 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 funciones que no están en línea, el puntero y la referencia generan el mismo código y siempre es mejor pasar intrínsecos por valor que por referencia si la función no los modifica y devuelve.
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 referencia constante a un temporal' de las referencias.
Este programa podría 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 comprenderá.
Además, dedica 10 minutos y mira este video: https://www.youtube.com/watch?v=rlJrrGV0iOg
Siento que hay otro punto que no se ha cubierto aquí.
A diferencia de los punteros, las referencias son sintácticamente equivalentes al objeto al que se refieren, es decir, cualquier operación que se pueda aplicar a un objeto funciona para una referencia y con la misma sintaxis exacta (la excepción es, por supuesto, la inicialización).
Si bien esto puede parecer superficial, creo que esta propiedad es crucial para una serie de características de C ++, por ejemplo:
Plantillas . Dado que los parámetros de la plantilla son de tipo pato, las propiedades sintácticas de un tipo son lo único que importa, por lo que a menudo se puede usar la misma plantilla con ambos T
y T&
.
(o std::reference_wrapper<T>
que todavía se basa en una conversión implícita a T&
)
Plantillas que cubren ambos T&
y T&&
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: daría lugar a una llamada recursiva al mismo constructor de copia. Esto deja referencias como la única opción aquí.
Sobrecargas del operador . Con referencias es posible introducir indirección a una llamada de operador, digamos, operator+(const T& a, const T& b)
mientras se mantiene la misma notación infija. Esto también funciona para funciones regulares sobrecargadas.
Estos puntos potencian una parte considerable de C ++ y la biblioteca estándar, por lo que esta es una propiedad bastante importante de las referencias.
Hay una diferencia no técnica muy importante entre punteros y referencias: un argumento pasado a una función por puntero es mucho más visible que un argumento pasado a una función por 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 se puede pasar por valor, por lo que definitivamente no se puede modificar x
; para modificar un argumento necesitaría pasar un puntero fn(&x)
. Entonces, si una discusión no fue precedida por una, &
usted sabía que no se modificaría. (Lo contrario, &
significa modificado, no era cierto porque a veces tendría que pasar grandes estructuras de solo lectura por const
puntero).
Algunos argumentan que esta es una característica tan útil cuando se lee 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 un nullptr
. Es decir, esas personas argumentan que las firmas de funciones como las fn3()
anteriores no deberían permitirse. Las pautas de estilo C ++ de Google son un ejemplo de esto.
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én int& 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 siempre una referencia a la variable o al objeto. Sin embargo, el uso de un puntero es arriesgado, por lo que generalmente verificamos si el puntero está apuntando a algo o no. En el caso de una referencia, no es necesaria dicha verificación, porque ya sabemos que es obligatorio hacer referencia a un objeto durante la declaración.
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, este 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 "="
const int& i = 0
.
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 del puntero (&, *), mientras que las referencias se pueden establecer en la inicialización solo (es por eso que puede configurarlos solo en la lista de inicializador de constructor, pero no de otra manera) y usar la semántica de acceso de valor ordinario. Básicamente, se introdujeron referencias para permitir el soporte para los operadores que sobrecargan, como había leído en un libro muy antiguo. Como alguien dijo en este hilo, el puntero se puede establecer en 0 o el valor que desee. 0 (NULL, nullptr) significa que el puntero se inicializa sin nada. Es un error desreferenciar el puntero nulo. Pero en realidad el puntero puede contener un valor que no apunta a alguna ubicación de memoria correcta. Las referencias a su vez 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 de tipo correcto. Aunque hay muchas maneras de hacer que la variable de referencia se inicialice en una ubicación de memoria incorrecta, es mejor no profundizar tanto en los detalles. A nivel de máquina, tanto el puntero como la referencia funcionan de manera uniforme, a través de punteros. Digamos en referencias esenciales son el azúcar sintáctico. Las referencias de valor son diferentes a esto: son, naturalmente, objetos de pila / montón. Aunque hay muchas maneras de hacer que la variable de referencia se inicialice en una ubicación de memoria incorrecta, es mejor no profundizar tanto en los detalles. A nivel de máquina, tanto el puntero como la referencia funcionan de manera uniforme, a través de punteros. Digamos en referencias esenciales son el azúcar sintáctico. Las referencias de valor son diferentes a esto: son, naturalmente, objetos de pila / montón. Aunque hay muchas maneras de hacer que la variable de referencia se inicialice en una ubicación de memoria incorrecta, es mejor no profundizar tanto en los detalles. A nivel de máquina, tanto el puntero como la referencia funcionan de manera uniforme, a través de punteros. Digamos en referencias esenciales son el azúcar sintáctico. Las referencias de valor son diferentes a esto: son, naturalmente, objetos de pila / montón.
En palabras simples, podemos decir que una referencia es un nombre alternativo para una variable, mientras que un puntero es una variable que contiene la dirección de otra variable. p.ej
int a = 20;
int &r = a;
r = 40; /* now the value of a is changed to 40 */
int b =20;
int *ptr;
ptr = &b; /*assigns address of b to ptr not the value */