¿Cuáles son las diferencias entre una variable de puntero y una variable de referencia en C ++?

3265

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?

prakash
fuente
100
Creo que el punto 2 debería ser "Se permite que un puntero sea NULL pero una referencia no. Solo el código con formato incorrecto puede crear una referencia NULL y su comportamiento es indefinido".
Mark Ransom
19
Los punteros son solo otro tipo de objeto y, como cualquier objeto en C ++, pueden ser una variable. Las referencias, por otro lado, nunca son objetos, solo variables.
Kerrek SB
19
Esto se compila sin advertencias: int &x = *(int*)0;en gcc. De hecho, la referencia puede apuntar a NULL.
Calmarius
20
referencia es un alias variable
Khaled.K
20
Me gusta cómo la primera oración es una falacia total. Las referencias tienen su propia semántica.
Carreras ligeras en órbita el

Respuestas:

1708
  1. 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;
  2. 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);
  3. 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);
  4. Se puede asignar un puntero nullptrdirectamente, 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
  5. Los punteros pueden iterar sobre una matriz; puede usar ++para ir al siguiente elemento al que apunta un puntero e + 4ir al quinto elemento. Esto no importa el tamaño del objeto al que apunta el puntero.

  6. 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 ..

  7. Las referencias no pueden rellenarse en una matriz, mientras que los punteros pueden (Mencionado por el usuario @litb)

  8. 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.

Brian R. Bondy
fuente
23
... pero desreferenciar NULL no está definido. Por ejemplo, no puede probar si una referencia es NULL (por ejemplo, & ref == NULL).
Pat Notz
69
El número 2 no es cierto. Una referencia no es simplemente "otro nombre para la misma variable". Las referencias pueden pasarse a funciones, almacenarse en clases, etc. de una manera muy similar a los punteros. Existen independientemente de las variables a las que apuntan.
Derek Park el
31
Brian, la pila no es relevante. Las referencias y los punteros no tienen que ocupar espacio en la pila. Ambos se pueden asignar en el montón.
Derek Park
22
Brian, el hecho de que una variable (en este caso un puntero o referencia) requiera espacio no significa que requiera espacio en la pila. Los punteros y las referencias pueden no solo apuntar al montón, sino que también pueden asignarse al montón.
Derek Park
38
Otra diferencia importante: las referencias no se pueden incluir en una matriz
Johannes Schaub - litb
384

¿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 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!

¿Por qué considero útiles las referencias de C ++?

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.

Christoph
fuente
17
@kriss: No, también puede obtener una referencia colgante devolviendo una variable automática por referencia.
Ben Voigt
12
@kriss: Es prácticamente imposible que un compilador lo detecte en el caso general. Considere una función miembro que devuelve una referencia a una variable miembro de clase: es segura y el compilador no debe prohibirla. Luego, una persona que llama que tiene una instancia automática de esa clase, llama a esa función miembro y devuelve la referencia. Presto: referencia colgante. Y sí, va a causar problemas, @kriss: ese es mi punto. Muchas personas afirman que una ventaja de las referencias sobre los punteros es que las referencias son siempre válidas, pero simplemente no lo es.
Ben Voigt el
44
@kriss: No, una referencia a un objeto de duración de almacenamiento automático es muy diferente de un objeto temporal. De todos modos, solo estaba brindando un contraejemplo a su declaración de que solo puede obtener una referencia no válida al hacer referencia a un puntero no válido. Christoph es correcto: las referencias no son más seguras que los punteros, un programa que utiliza referencias exclusivamente puede romper la seguridad de los tipos.
Ben Voigt
77
Las referencias no son una especie de puntero. Son un nuevo nombre para un objeto existente.
catphive
18
@catphive: verdadero si utiliza la semántica del lenguaje, no es cierto si realmente observa la implementación; C ++ es un lenguaje mucho más "mágico" que C, y si elimina la magia de las referencias, terminará con un puntero
Christoph
191

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, constno debería compilarse. No puede vincular una referencia no constante a un objeto temporal, ni puede tomar su dirección para el caso.

Matt Price
fuente
55
pero cual es el caso de uso para esto?
Ahmad Mushtaq
20
Bueno, s3_copy creará un temporal y luego lo construirá en s3_copy mientras que s3_reference usa directamente el temporal. Luego, para ser realmente pedante, debe mirar la Optimización del valor de retorno por la cual el compilador puede eludir la construcción de la copia en el primer caso.
Matt Price
66
@digitalSurgeon: La magia allí es bastante poderosa. El tiempo de vida del objeto se extiende por el hecho del 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.
David Rodríguez - dribeas
99
Actualización para C ++ 11: la última oración debería leer "No puede vincular una referencia de valor no constante a un temporal" porque puede vincular una referencia de valor no constante a un temporal, y tiene el mismo comportamiento que se extiende durante toda la vida.
Oktalist
44
@AhmadMushtaq: El uso clave de esto son las clases derivadas . Si no hay herencia involucrada, también podría usar la semántica de valor, que será barata o gratuita debido a la construcción de RVO / movimiento. Pero si lo ha hecho Animal x = fast ? getHare() : getTortoise(), xse enfrentará al clásico problema de corte, mientras Animal& x = ...que funcionará correctamente.
Arthur Tacca
128

Además del azúcar sintáctico, una referencia es un constpuntero ( 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.


fuente
8
Creo que esta es la mejor respuesta con diferencia. Otros hablan de referencias y punteros como si fueran bestias diferentes y luego exponen cómo difieren en el comportamiento. No hace las cosas más fáciles en mi humilde opinión. Siempre he entendido que las referencias son T* constcon un azúcar sintáctico diferente (lo que elimina una gran cantidad de * y & de su código).
Carlo Wood
2
"El objetivo de un puntero constante puede ser reemplazado tomando su dirección y usando un molde constante". Hacerlo es un comportamiento indefinido. Consulte stackoverflow.com/questions/25209838/… para más detalles.
dgnuff
1
Intentar cambiar el referente de una referencia o el valor de un puntero constante (o cualquier escalar constante) es una igualdad ilegal. Lo que puede hacer: eliminar una calificación constante que se agregó por conversión implícita: int i; int const *pci = &i; /* implicit conv to const int* */ int *pi = const_cast<int*>(pci);está bien.
curioso
1
La diferencia aquí es UB versus literalmente imposible. No hay sintaxis en C ++ que le permita cambiar los puntos de referencia.
No es imposible, más difícil, solo puede acceder al área de memoria del puntero que está modelando esa referencia y cambiar su contenido. Eso ciertamente se puede hacer.
Nicolas Bousquet
126

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 ifclá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 .

Mark Ransom
fuente
63
El código en cuestión contiene un comportamiento indefinido. Técnicamente, no puede hacer nada con un puntero nulo excepto configurarlo y compararlo. Una vez que su programa invoca un comportamiento indefinido, puede hacer cualquier cosa, incluso parecer funcionar correctamente hasta que esté dando una demostración al gran jefe.
KeithB
99
Mark tiene un argumento válido. el argumento de que un puntero podría ser NULL y, por lo tanto, debe verificarlo tampoco es real: si dice que una función requiere no NULL, la persona que llama tiene que hacer eso. así que si la persona que llama no está invocando un comportamiento indefinido. al igual que Mark hizo con la mala referencia
Johannes Schaub - litb
13
La descripción es errónea. Este código puede o no crear una referencia que sea NULL. Su comportamiento es indefinido. Podría crear una referencia perfectamente válida. Es posible que no pueda crear ninguna referencia en absoluto.
David Schwartz
77
@David Schwartz, si estuviera hablando de cómo las cosas tenían que funcionar de acuerdo con el estándar, estaría en lo correcto. Pero eso no es de lo que estoy hablando: estoy hablando del comportamiento observado real con un compilador muy popular, y extrapolando en base a mi conocimiento de compiladores típicos y arquitecturas de CPU a lo que probablemente sucederá. Si crees que las referencias son superiores a los punteros porque son más seguras y no consideras que las referencias pueden ser malas, algún día te sorprenderá un problema simple como yo.
Mark Ransom
66
Desreferenciar un puntero nulo está mal. Cualquier programa que haga eso, incluso para inicializar una referencia, está mal. Si está inicializando una referencia desde un puntero, siempre debe verificar que el puntero sea válido. Incluso si esto tiene éxito, el objeto subyacente se puede eliminar en cualquier momento dejando la referencia para referirse a un objeto no existente, ¿verdad? Lo que estás diciendo es algo bueno. Creo que el problema real aquí es que la referencia NO necesita ser verificada por "nulidad" cuando veas una y el puntero debe, como mínimo, afirmarse.
t0rakka
115

Olvidaste la parte más importante:

acceso de miembros con punteros usa ->
acceso de miembros con referencias usa.

foo.bares claramente superior de foo->barla misma manera que vi es claramente superior a Emacs :-)

Orion Edwards
fuente
44
@Orion Edwards> usos de acceso de miembros con punteros ->> usos de acceso de miembros con referencias. Esto no es 100% cierto. Puede tener una referencia a un puntero. En este caso, accedería a los miembros del puntero desreferenciado utilizando -> struct Node {Node * next; }; Nodo * primero; // p es una referencia a un puntero void foo (Node * & p) {p-> next = first; } Nodo * bar = nuevo nodo; foo (bar); - OP: ¿Está familiarizado con los conceptos de valores y valores?
3
Los punteros inteligentes tienen ambos. (métodos en la clase de puntero inteligente) y -> (métodos en el tipo subyacente).
JBRWilkinson
1
@ user6105 La declaración de Orion Edwards es en realidad 100% cierta. "miembros de acceso del [puntero] desreferenciado" Un puntero no tiene miembros. El objeto al que se refiere el puntero tiene miembros, y el acceso a ellos es exactamente lo que ->proporciona referencias a los punteros, al igual que con el puntero mismo.
Max Truxa
1
por qué es eso .y ->tiene algo que ver con vi y emacs :)
artm
10
@artM: fue una broma, y ​​probablemente no tiene sentido para los hablantes no nativos de inglés. Mis disculpas. Para explicar, si vi es mejor que emacs es completamente subjetivo. Algunas personas piensan que vi es muy superior, y otras piensan exactamente lo contrario. Del mismo modo, creo que usar .es mejor que usar ->, pero al igual que vi vs emacs, es completamente subjetivo y no se puede probar nada
Orion Edwards
74

Las referencias son muy similares a los punteros, pero están diseñados específicamente para ayudar a optimizar los compiladores.

  • Las referencias están diseñadas de tal manera que es mucho más fácil para el compilador rastrear qué alias de referencia y qué variables. Dos características principales son muy importantes: sin "aritmética de referencia" y sin reasignación de referencias. Permiten al compilador averiguar qué referencias alias qué variables en tiempo de compilación.
  • Se permite que las referencias hagan referencia a variables que no tienen direcciones de memoria, como las que el compilador elige poner en los registros. Si toma la dirección de una variable local, es muy difícil para el compilador ponerla 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 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, refC ++ extenderá la vida útil de ese objeto temporal hasta que refquede fuera de alcance.

Cort Ammon
fuente
Es cierto que el cuerpo tiene que ser visible. Sin embargo, determinar que maybeModifyno toma la dirección de nada relacionado xes sustancialmente más fácil que probar que no se produce un montón de aritmética de puntero.
Cort Ammon
Creo que el optimizador ya hace que "un montón de aritmética de puntero no se produce" compruebe por un montón de otras razones.
Ben Voigt
"Las referencias son muy similares a los punteros" - semánticamente, en contextos apropiados - pero en términos de código generado, solo en algunas implementaciones y no a través de ninguna definición / requisito. Sé que has señalado esto, y no estoy en desacuerdo con ninguna de tus publicaciones en términos prácticos, pero ya tenemos demasiados problemas con personas que leen demasiado en descripciones abreviadas como 'las referencias son como / generalmente implementadas como punteros' .
underscore_d
Tengo la sensación de que alguien marcó erróneamente como obsoleto un comentario en la línea de lo void maybeModify(int& x) { 1[&x]++; }que están discutiendo los otros comentarios anteriores
Ben Voigt,
69

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, ay 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.

Vincent Robert
fuente
2
Una referencia es una referencia al valor l, no necesariamente a una variable. Por eso, está mucho más cerca de un puntero que de un alias real (una construcción en tiempo de compilación). Ejemplos de expresiones a las que se puede hacer referencia son * p o incluso * p ++
55
Bien, solo estaba señalando el hecho de que una referencia no siempre puede insertar una nueva variable en la pila de la misma manera que lo hará un nuevo puntero.
Vincent Robert
1
@ VincentRobert: actuará igual que un puntero ... si la función está en línea, tanto la referencia como el puntero se optimizarán. Si hay una llamada a la función, la dirección del objeto deberá pasarse a la función.
Ben Voigt
1
int * p = NULL; int & r = * p; referencia apuntando a NULL; if (r) {} -> boOm;)
sree
2
Este enfoque en la etapa de compilación parece agradable, hasta que recuerde que las referencias se pueden pasar en tiempo de ejecución, momento en el que el alias estático desaparece de la ventana. (Y luego, las referencias generalmente se implementan como punteros, pero el estándar no requiere este método.)
subrayado_d
45

Una referencia nunca puede ser NULL.

Cole Johnson
fuente
10
Ver la respuesta de Mark Ransom para un contraejemplo. Este es el mito más frecuentemente afirmado sobre las referencias, pero es un mito. La única garantía que tiene según el estándar es que tiene UB inmediatamente cuando tiene una referencia NULL. Pero eso es similar a decir "Este automóvil es seguro, nunca puede salirse de la carretera. (No nos hacemos responsables de lo que pueda suceder si de todos modos lo saca de la carretera. Podría explotar)"
cmaster - reinstalar a monica el
17
@cmaster: en un programa válido , una referencia no puede ser nula. Pero un puntero puede. Esto no es un mito, es un hecho.
user541686
8
@Mehrdad Sí, los programas válidos permanecen en el camino. Pero no hay una barrera de tráfico para imponer que su programa realmente lo hace. A grandes partes del camino en realidad les faltan marcas. Por lo tanto, es extremadamente fácil salir de la carretera por la noche. Y es crucial para depurar tales errores que usted sabe que esto puede suceder: la referencia nula puede propagarse antes de que bloquee su programa, al igual que un puntero nulo. Y cuando lo hace, tiene un código como void Foo::bar() { virtual_baz(); }ese segfaults. Si no sabe que las referencias pueden ser nulas, no puede rastrear el nulo hasta su origen.
cmaster - reinstalar a monica el
44
int * p = NULL; int & r = * p; referencia apuntando a NULL; if (r) {} -> boOm;) -
sree
10
@sree 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 .
cdhowie
35

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.

Kunal Vyas
fuente
"una referencia siempre se refiere a un objeto" es completamente falso
Ben Voigt
32

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.

Carreras de ligereza en órbita
fuente
27

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. 
fatma.ekici
fuente
20

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.

MSN
fuente
1
No puede tomar la dirección de una referencia, pero eso no significa que físicamente no ocupen espacio. Salvo optimizaciones, sin duda pueden hacerlo.
Carreras de ligereza en órbita
2
No obstante el impacto, "Una referencia en la pila no ocupa espacio en absoluto" es evidentemente falsa.
ligereza corre en órbita el
1
@Tomalak, bueno, eso también depende del compilador. Pero sí, decir eso es un poco confuso. Supongo que sería menos confuso eliminar eso.
MSN
1
En cualquier caso específico puede o no puede. Entonces "no lo hace" como una afirmación categórica está mal. Eso es lo que estoy diciendo. :) [No recuerdo lo que dice el estándar sobre el tema; las reglas de los miembros de referencia pueden impartir una regla general de "las referencias pueden ocupar espacio", pero no tengo mi copia del estándar conmigo aquí en la playa: D]
Carreras de ligereza en órbita
20

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_Tompuede 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.

Vida
fuente
1
Y si una clase tiene una variable de referencia, debe inicializarse con un nullptr o un objeto válido en la lista de inicialización.
Misgevolution
1
La redacción de esta respuesta es demasiado confusa para que sea de mucho uso real. Además, @Misgevolution, ¿estás recomendando seriamente a los lectores que inicialicen una referencia con un nullptr? ¿Has leído alguna otra parte de este hilo o ...?
underscore_d
1
Mi pena, perdón por esa estupidez que dije. Debo haber estado privado de sueño en ese momento. 'initialize with nullptr' está totalmente equivocado.
Misgevolution
19

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;
Tanweer Alam
fuente
13
Esto no es lo que dice el estándar C ++, y no es necesario que el compilador implemente referencias de la manera descrita en su respuesta.
jogojapan
@jogojapan: cualquier forma que sea válida para que un compilador de C ++ implemente una referencia también es una forma válida de implementar un constpuntero. Esa flexibilidad no prueba que haya una diferencia entre una referencia y un puntero.
Ben Voigt
2
@BenVoigt Puede ser cierto que cualquier implementación válida de uno también es una implementación válida del otro, pero eso no se deduce de manera obvia de las definiciones de estos dos conceptos. Una buena respuesta habría comenzado a partir de las definiciones y habría demostrado por qué la afirmación de que los dos son en última instancia iguales es cierta. Esta respuesta parece ser algún tipo de comentario sobre algunas de las otras respuestas.
jogojapan
Una referencia es otro nombre dado a un objeto. El compilador puede tener cualquier tipo de implementación, siempre y cuando no pueda notar la diferencia, esto se conoce como la regla "como si". La parte importante aquí es que no puedes notar la diferencia. Si puede descubrir que un puntero no tiene almacenamiento, el compilador está en error. Si puede descubrir que una referencia no tiene almacenamiento, el compilador sigue siendo conforme.
sp2danny
18

La respuesta directa

¿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:

  • Los tipos de objeto pueden tener cvcalificadores de nivel superior . Las referencias no pueden.
  • Las variables de los tipos de objetos ocupan el almacenamiento según la semántica abstracta de la máquina . Las referencias no necesariamente ocupan almacenamiento (consulte la sección sobre conceptos erróneos a continuación para obtener más detalles).
  • ...

Algunas reglas especiales más sobre referencias:

  • Los declaradores compuestos son más restrictivos en las referencias.
  • Las referencias pueden colapsar .
    • Las reglas especiales sobre &&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.
  • Las referencias tienen reglas especiales en la inicialización. La vida útil de la variable declarada como tipo de referencia puede ser diferente a la de los objetos ordinarios a través de la extensión.
    • Por cierto, algunos otros contextos como la inicialización implican std::initializer_listalgunas reglas similares de extensión de la vida útil de referencia. Es otra lata de gusanos.
  • ...

Los conceptos erróneos

Azúcar sintáctica

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+ .

Almacenamiento

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 .

Pragmática

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.

  • A veces, las reglas del lenguaje requieren explícitamente el uso de tipos específicos. Si desea utilizar estas funciones, obedezca las reglas.
    • Los constructores de copia requieren tipos específicos de cv - &tipo de referencia como el primer tipo de parámetro. (Y por lo general, debe estar constcalificado).
    • Los constructores de movimiento requieren tipos específicos de cv - &&tipo de referencia como el primer tipo de parámetro. (Y generalmente no debería haber calificadores).
    • Las sobrecargas específicas de operadores requieren tipos de referencia o no de referencia. Por ejemplo:
      • Sobrecargado operator=como funciones miembro especiales requiere tipos de referencia similares al primer parámetro de los constructores copiar / mover.
      • Postfix ++requiere dummy int.
      • ...
  • Si sabe que el paso por valor (es decir, el uso de tipos sin referencia) es suficiente, úselo directamente, particularmente cuando use una implementación que admita la elisión de copia obligatoria de C ++ 17. ( Advertencia : Sin embargo, razonar exhaustivamente sobre la necesidad puede ser muy complicado ).
  • Si desea operar algunas manijas con propiedad, use punteros inteligentes como unique_ptry shared_ptr(o incluso con los homebrew solo si necesita que sean opacos ), en lugar de punteros sin procesar.
  • Si está haciendo algunas iteraciones sobre un rango, use iteradores (o algunos rangos que todavía no son provistos por la biblioteca estándar), en lugar de punteros sin formato a menos que esté convencido de que los punteros sin formato funcionarán mejor (por ejemplo, para menos dependencias de encabezado) casos.
  • Si sabe que el paso por valor es suficiente y desea una semántica anulable explícita, use un contenedor similar std::optional, en lugar de punteros sin formato.
  • Si sabe que el paso por valor no es ideal por las razones anteriores, y no desea una semántica anulable, use las referencias {lvalue, rvalue, forwarding}.
  • Incluso cuando desea semántica como el puntero tradicional, a menudo hay algo más apropiado, como observer_ptren Library Fundamental TS.

Las únicas excepciones no se pueden solucionar en el idioma actual:

  • Cuando implemente punteros inteligentes anteriores, es posible que tenga que lidiar con punteros sin procesar.
  • Las rutinas específicas de interoperación de lenguaje requieren punteros, como 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).
  • Los punteros de función se pueden convertir de expresiones lambda sin capturas, mientras que las referencias de función no. Debe usar punteros de función en código no genérico para tales casos, incluso si deliberadamente no desea valores anulables.

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:

  • Debe servir a las API de estilo antiguo (C).
  • Debe cumplir con los requisitos de ABI de implementaciones específicas de C ++.
  • Debe interoperar en tiempo de ejecución con diferentes implementaciones de idiomas (incluidos varios ensamblajes, tiempo de ejecución de idiomas y FFI de algunos idiomas de clientes de alto nivel) en función de suposiciones de implementaciones específicas.
  • Debe mejorar la eficiencia de la traducción (compilación y vinculación) en algunos casos extremos.
  • Debe evitar la hinchazón de símbolos en algunos casos extremos.

Advertencias de neutralidad del lenguaje

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 ).

FrankHB
fuente
17

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;
}
Incinerador de basuras
fuente
16

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;

Cerdo hormiguero
fuente
55
Puede escribir &r + offsetdonde rse declaró como referencia
MM
15

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

Andrzej
fuente
1
Las referencias y los punteros son ambos identificadores. Ambos le dan la semántica donde se pasa su objeto por referencia, pero el identificador se copia. Ninguna diferencia. (También hay otras formas de tener identificadores, como una clave para buscar en un diccionario)
Ben Voigt
También solía pensar así. Pero vea el artículo vinculado que describe por qué no es así.
Andrzej
2
@Andrzj: Esa es solo una versión muy larga de la oración en mi comentario: el identificador se copia.
Ben Voigt
Necesito más explicaciones sobre esto "Se copia el identificador". Entiendo alguna idea básica, pero creo que físicamente la referencia y el puntero apuntan a la ubicación de memoria de la variable. ¿Es como el alias almacena la variable de valor y la actualiza a medida que el valor de la variable es el cambio o algo más? Soy novato, y por favor no lo marque como una pregunta estúpida.
Asim
1
@Andrzej False. En ambos casos, está ocurriendo el paso por valor. La referencia se pasa por valor y el puntero se pasa por valor. Decir lo contrario confunde a los novatos.
Miles Rout
14

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

conservador
fuente
14

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.

kriss
fuente
13

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.

Adisak
fuente
11

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.

Don Wakefield
fuente
11

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

Arlene Batada
fuente
11

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 Ty 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.

Ap31
fuente
" conversión implícita " una conversión es una construcción de sintaxis, existe en la gramática; un reparto siempre es explícito
curioso
9

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 constpuntero).

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 constreferencias, 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.

Arthur Tacca
fuente
8

Quizás algunas metáforas ayuden; En el contexto de su espacio de pantalla de escritorio:

  • Una referencia requiere que especifique una ventana real.
  • Un puntero requiere la ubicación de un espacio en la pantalla que asegure que contendrá cero o más instancias de ese tipo de ventana.
George R
fuente
6

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é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 "="
dhokar.w
fuente
1
Aún podemos tener const int& i = 0.
Revolver_Ocelot
1
En este caso, la referencia se usará solo en lectura, no podemos modificar esta referencia constante incluso usando "const_cast" porque "const_cast" acepta solo el puntero, no la referencia.
dhokar.w
1
const_cast funciona bastante bien con las referencias: coliru.stacked-crooked.com/a/eebb454ab2cfd570
Revolver_Ocelot
1
está haciendo un reparto a la referencia no proyectando una referencia intente esto; const int & i =; const_cast <int> (i); Intento descartar la coherencia de la referencia para hacer posible la escritura y la asignación de un nuevo valor a la referencia, pero esto no es posible. por favor concéntrate !!
dhokar.w
5

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.

Zorgiev
fuente
4

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 */
Sadhana Singh
fuente