17

Un puntero me da la dirección de un objeto, ok. Sin embargo,

  • es int * x_ptr; el revés de &?: le dan un un puntero y me da un objeto

  • es int & x_ref = y; el revés de *?: le dan un objeto y me da un puntero

eferion
  • 49,291
  • 5
  • 30
  • 72
Revolucion for Monica
  • 4,154
  • 4
  • 28
  • 80

2 Answers2

28

Que es la diferencia entre int * y int &?

Son tipos distintos. El primero (int *) es un puntero a entero. El segundo (int &) es una referencia a entero.

Puntero.

Los punteros, apuntan a objetos, su valor es una dirección de memoria. A efectos prácticos serían un tipo de dato que sólo puede apuntar a otros objetos; como analogía serían un código postal (que apunta a una zona urbana) o una matrícula de vehículo (apunta a un vehículo) o un número de pasaporte (apunta a una persona).

Ciclo de vida.

Un puntero a un tipo tipo *, puede apuntar a más de un objeto de tipo tipo en su ciclo de vida, e incluso puede no apuntar a ningún objeto o apuntar a objetos no existentes:

int *puntero = 0; // El puntero no apunta a nada.
{
    int valor = 0;
    puntero = &valor; // Apunta al objeto 'valor'.
}
// Aqui, 'puntero' sigue apuntando a 'valor' aunque dicho objeto ya no exista!
puntero = new int; // 'puntero' puede gestionar memoria dinamica!

Los punteros son el único mecanismo de C y C++ para gestionar memoria dinámica; el operador new genera punteros al ser llamado y la transformación de arreglo a puntero y viceversa es una práctica habitual.

Modificación del objeto apuntado.

Con un puntero se puede apuntar virtualmente a cualquier parte de la memoria, es por esto que los punteros son considerados peligrosos a la vez que útiles; esta flexibilidad de los punteros puede permitir examinar o modificar memoria que no pertenece al programa en ejecución (para realizar análisis o hackeos) lo que puede provocar fallos del programa.

Operaciones.

  1. Aritmética: Se pueden realizar operaciones aritméticas con los punteros: sumando un valor a un puntero obtenemos un puntero apuntando a una zona de memoria desplazada desde el puntero original tantos bytes como tamaño tuviera el valor apuntado:

    char *puntero_char = 0;
    int *puntero_int = 0;
    
    puntero_char = puntero_char + 1;
    puntero_int = puntero_int - 1;
    

    Al sumar 1 a puntero_char, haremos que el puntero apunte a la zona de memoria 1 byte posterior a la zona en que apuntaba originalmente (en sistemas que char ocupe un byte), restando 1 a puntero_int haremos que el puntero apunte a la zona de memoria 4 bytes anteriores a la zona en que apuntaba originalmente (en sistemas en que int ocupe 4 bytes, o 32 bits). En ambos casos estaremos apuntando a memoria que no deberíamos poder leer o escribir, puesto que ambos punteros fueron inicializados a 0 y el sistema operativo suele prohibir el acceso a la dirección de memoria 0 (y varios valores cercanos a 0).

  2. Dirección de: Si usamos el operador de obtener dirección sobre un puntero, añadiremos otro nivel de puntero al puntero, es decir: la dirección de un puntero tiene como tipo puntero a puntero, y el tipo de la dirección del anterior sería puntero a puntero a puntero (y así hasta el infinito):

    int valor = 0;
    int *puntero_int = &valor; // Puntero a int.
    int **puntero_a_puntero = &puntero_int; // Puntero a puntero a int
    int ***puntero_a_puntero_a_puntero = &puntero_a_puntero; // Puntero a puntero a puntero a int
    
  3. Des-referencia: Si usamos el operador de des-referencia sobre un puntero accederemos al objeto apuntado, permitiendonos modificar dicho objeto:

    int valor = 0;
    int *puntero_int = &valor; // Puntero a int.
    *puntero_int = *puntero_int + 1;
    

    El ejemplo anterior hace que valor contenga el valor 1.

  4. Tamaño: Si usamos el operador sizeof sobre un puntero, siempre obtendremos el mismo tamaño sea cual sea el tipo apuntado, este tamaño variará según la arquitectura del sistema; normalmente el tamaño del puntero coincide con el tamaño de la palabra de procesador:

    char *puntero_char = 0;
    int *puntero_int = 0;
    
    // Esta condicion se cumple siempre.
    if (sizeof(puntero_char) == sizeof(puntero_int)) std::cout << "Eureka!";
    

Cualificadores.

Es posible cualificar un puntero con const o volatile de cuatro maneras disintas:

  • Ningún cualificador: Es la cualificación que hemos estado viendo hasta ahora:

    int *puntero = 0; // Sin cualificadores
    

    Esta modalidad permite apuntar a objetos que carezcan de cualificadores:

    int valor = 0;
    int *puntero = &valor;
    
  • Cualificar el objeto apuntado: La cualificación se aplica sobre el objeto apuntado:

    const int *puntero = 0; // Apuntamos a int constantes.
    

    Esta modalidad permite apuntar a objetos con la misma o inferior cualificación:

    int valor = 0;
    const int valor_constante = 0;
    // ERROR! 'valor_constante' solo puede ser apuntado por punteros con la misma cualificacion!
    int *puntero = &valor_constante;
    // Correcto! Usamos un puntero a entero constante
    const int *puntero_a_constante = &valor_constante;
    // Correcto! Apuntamos a un objeto con cualificacion inferior
    const int *otro_puntero_a_constante = &valor;
    

    Es decir, con un puntero podemos apuntar a objetos que no estén cualificados como si estuvieran cualificados con const o volatile.

  • Cualificar el puntero: La cualificación se aplica sobre el puntero:

    int *const puntero_constante = 0;
    

    Nótese que el cualificador está después del asterisco (*), en este caso estamos creando un puntero constante a entero no constante. Dado que el puntero es constante no podremos hacer que apunte a otro objeto pero podremos modificar el objeto apuntado:

    int valor1 = 0;
    int valor2 = 0;
    int *const puntero_constante = &valor1;
    *puntero_constante = *puntero_constante + 1; // valor1 sera 1
    puntero_constante = &valor2; // ERROR! 'puntero_constante' no puede ser cambiado
    
  • Cualificar ambos: La cualificación se aplica sobre ambos:

    const int *const valor = 0;
    

    Nótese que el cualificador está a ambos lados del asterisco (*), en este caso estamos creando un puntero constante a un objeto constante. Así que no podremos modificar el valor del objeto al que apunta ni el objeto apuntado:

    int valor1 = 0;
    int valor2 = 0;
    const int *const puntero_constante_a_constante = &valor1;
    // ERROR! el objeto apuntado es constante!
    *puntero_constante_a_constante = *puntero_constante_a_constante + 1;
    // ERROR! 'puntero_constante_a_constante' no puede ser cambiado
    puntero_constante_a_constante = &valor2;
    

    Como en casos anteriores, vemos que el puntero puede apuntar a objetos con cualificaciones menos restrictivas.

Referencia.

Las referencias, referencian objetos, no contienen una dirección al objeto referenciado son el objeto referenciado. A efectos prácticos una referencia es indistinguible del objeto al que referencian; como analogía serían una partícula enlazada cuánticamente con otra.

Ciclo de vida.

Una referencia a un tipo tipo &, sólo apunta a un objeto de tipo tipo en su ciclo de vida, es por esto que es necesario asignarla en el mismo momento de su declaración:

int valor1 = 1;
int valor2 = 2;
int &referencia;               // ERROR! 'referencia' necesita referenciar un objeto.
int &otra_referencia = valor1; // Correcto! Referenciamos 'valor1'.

Modificación del objeto apuntado.

Las referencias no pueden cambiar el objeto al que referencian, cualquier operación de asignación sobre una referencia modificará el objeto referenciado no la referencia:

int valor1 = 1;
int valor2 = 2;
int &referencia = valor1;
referencia = valor2;

la instrucción referencia = valor2; no hace que referencia referencie a valor2 si no que asigna el valor de valor2 al objeto referenciado por referencia, es decir: valor1 obtendrá el valor 2.

Operaciones.

  1. Aritmética: Dado que las referencias sólo apuntan a un objeto todo su ciclo de vida, no es posible hacer operaciones aritméticas para modificar el objeto referenciado; por el contrario las operaciones realizadas sobre la referencia se aplicarán en el objeto referenciado, no a la referencia en si, exáctamente igual que si la operación se realizase sobre el objeto referenciado:

    int valor1 = 1;
    int &referencia = valor1;
    referencia = referencia + 1;
    

    El ejemplo anterior provocará que valor1 tenga el valor 2 no que referencia apunte al siguiente int en memoria.

  2. Dirección de: No es posible obtener la dirección de memoria de una referencia, por el contrario la dirección de memoria obtenida sería la del objeto referenciado:

    int valor = 0;
    int &referencia = valor;
    
    // Esta condicion se cumple siempre.
    if (&valor == &referencia) std::cout << "Eureka!";
    
  3. Des-referencia: Si usamos el operador de des-referencia sobre una referencia se producirá un error de compilación a no ser que el objeto referenciado acepte dicho operador:

    int valor = 0;
    struct S { int operator *() { return 0; } } s;
    int &referencia_a_int = valor;
    S &referencia_a_S = s;
    
    std::cout << *referencia_a_int; // ERROR! int no acepta operador unario *
    std::cout << *referencia_a_S; // Correcto! S acepta operador unario *
    
  4. Tamaño: Si usamos el operador sizeof sobre una referencia obtendremos el tamaño del objeto referenciado:

    char c = 0;
    int i = 0;
    char &referencia_char = c;
    int &referencia_int = i;
    
    // Esta condicion solo se cumple si char e int son del mismo tamanyo.
    if (sizeof(referencia_char) == sizeof(referencia_int)) std::cout << "Oh oh...";
    

Cualificadores.

Dado que en la práctica una referencia es el mismo objeto al que referencia, sólo es posible referenciar objetos con cualificaciones iguales o inferiores. Al contrario que con los punteros, no es posible cualificar la referencia sólo el objeto referenciado, así pues:

  • Referencias sin cualificación: Pueden referenciar a objetos sin cualifiación:

    int valor = 0;
    const int valor_constante = 0;
    int &referencia1 = valor;
    int &referencia2 = valor_constante; // ERROR! Se necesita una referencia constante
    
  • Referencias con cualificación: Pueden referenciar objetos con y sin cualificación:

    int valor = 0;
    const int valor_constante = 0;
    // Correcto, referencia constante a objeto no constante
    const int &referencia1 = valor;
    // Correcto! referencia constante a objeto constante 
    const int &referencia2 = valor_constante;
    
PaperBirdMaster
  • 44,474
  • 6
  • 44
  • 82
8
  • int* representa a un puntero.
  • int& representa una referencia.

¿En qué se parecen?

A nivel interno podemos considerar que ambos elementos se tratan como punteros. Es decir, al realizar modificaciones estás alterando el valor de una tercera variable:

    int var = 0;

    int* ptr = &var;
    int& ref = var;

    *ptr = 1;
    std::cout << var; // 1

    ref = 2;
    std::cout << var; // 2

¿En qué se diferencian?

Los cambios son varios:

  1. La referencia no te proporciona la dirección de memoria apuntada:

    int var;
    
    int* ptr = &var;
    int& ref = var;
    
    std::cout << (void*)ptr; // Dirección de memoria apuntada por el puntero
    std::cout << (void*)ref; // ¿¿?? No funciona
    
  2. No se puede cambiar de objeto apuntado por una referencia:

    int var1, var2;
    
    int& ref = var1;
    ref = var2; // Esto es equivalente a var1=var2.
    &ref = var2; // ERROR!!!
    
  3. No se puede ejecutar delete sobre una referencia:

    int& func1();
    int* func2();
    
    int& ref = func1();
    delete ref; // ERROR
    delete &ref; // ¿De verdad alguien querría hacer esto?
    
    int* ptr = func2();
    delete ptr; // OK
    
  4. En las referencias no hace falta usar el operador de indirección:

    int& func1();
    int* func2();
    
    int& ref = func1();
    ref = 5; // Nota que no es necesario usar el *
    
    int* ptr = func2();
    *ptr = 5;
    

¿Cómo saber cuándo usar cada tipo?

Las referencias deberían utilizarse cuando no es necesario acceder a alguna de las características que las diferencian de los punteros, ya que su uso es mucho más sencillo y claro que en el caso de los punteros.

Por ejemplo, si una función devuelve una referencia podemos asumir que no tenemos que preocuparnos de borrar la memoria una vez terminemos de usar la variable retornada.

eferion
  • 49,291
  • 5
  • 30
  • 72
  • Cuando hizo `int& ref = func1();` y `delete &ref;`quito el valor de `ref`, verdad ? Entonces, aunque `func1`valga cualquiera cosa, `ref` vale null ? – Revolucion for Monica Jan 31 '17 at 15:28
  • @Marine1 `delete &ref` intentaría liberar la memoria reservada por la variable referida... es un uso rebuscado que es mejor evitar para evitar errores en el código. Si el objeto lo debe liberar quien llame a la función entonces es mejor devolver un puntero – eferion Jan 31 '17 at 15:43