[C con Clase] Diferencia entre punteros y referencias

Steven Davidson srd4121 en njit.edu
Mar Jul 8 08:36:26 CEST 2008


Hola David,

David Reza wrote:
> Me gustaría saber cuál es la diferencia entre éstas dos  
> 'herramientas' del C++.
> 
> ¿Se pueden usar las 2 en una misma circunstancia?
> 

No estoy seguro a qué te refieres con "misma circunstancia". A veces, 
podemos optar por usar punteros y otras por referencias. Es posible que 
te refieras a esto.

> ¿Son muy parecidas?¿Cuál es la mejor y por qué?
> 

Como he mencionado anteriormente, a veces podemos optar por uno o por el 
otro, por lo que supongo que pueden ser mecanismos parecidos. Ninguno es 
mejor que el otro, pero dependiendo del caso, se suele elegir uno sobre 
el otro.

A ver si podemos resumir algunos casos con sus recomendaciones:

1. Usamos punteros conjuntamente con la adjudicación de memoria 
dinámica. Aquí, no podríamos usar referencias.

double *ptr1 = new double;
double *ptr2 = new double[100];

2. También usamos punteros cuando se trata de manejar arrays, tanto para 
pasarlos como parámetros a funciones como para retornarlos. Por ejemplo,

char *aMayusculas( char *pszCad );

3. Pasar una variable con el fin de modificar su contenido. En este 
caso, podemos usar tanto punteros como referencias. El uso del puntero 
es más afín a C, pero podemos seguir usándolo en C++. La referencia es 
más sencilla de usar ya que no hace falta usar operadores para poder 
manejar la variable referida ni tampoco para pasar tal variable a una 
función.

Ahora bien, se aconseja seguir usando punteros en lugar de referencias. 
Esto es porque el código de lo que se pide es más legible, ya que la 
"complejidad" del uso de tales punteros hace más aparente la intención. 
Por ejemplo,

void asignar( int *pvar )
{
   *pvar = 5;
}

int main()
{
   int num;

   asignar( &num );
   ...
}

Está claro que 'asignar()' requiere un puntero el cual servirá para 
modificar la variable 'num'.

Por el otro contrario, si usamos referencias, esto no es tan aparente. 
Por ejemplo,

void asignar( int &ref )
{
   ref = 5;
}

int main()
{
   int num;

   asignar( num );
   ...
}

Usamos 'asignar()' como cualquier otra función. Si no tenemos cuidado, 
es posible que modifiquemos las variables pasadas por referencia. Es 
decir, que con este mecanismo no podemos discernir, en la llamada, el 
paso por copia (o por valor) por el paso por referencia. Es decir, 
'asignar( num )' puede ser usada para cualquiera de los dos casos. Sin 
embargo, 'asignar( &num )' sí es aparente que se trata de punteros, 
seguramente para modificar el valor de 'num'.

Ahora bien, si pasamos información para sólo su lectura, entonces es 
aconsejable pasarla por referencia "constante". Por ejemplo,

struct nombre
{
   char szNombre1[32];
   char szNombre2[32];
   char szApellido1[128];
   char szApellido2[128];
};

void mostrar( const nombre &ref )
{
   cout << ref.szNombre1 << ref.szNombre2
        << ref.szApellido1 << ref.szApellido2;
}

int main()
{
   nombre nom;

   mostrar( nom );
   ...
}

Podríamos pensar que pasamos 'nom' por copia, aunque realmente lo 
hacemos por referencia. La característica adicional es que garantizamos 
su uso correcto al no "temer" que el estado de 'nom' sea cambiado; es 
decir, la información en 'nom' queda invariable.

El uso de 'const &' aparece bastante en el diseño de plantillas al igual 
que clases y sus funciones miembros como el constructor copia.

4. A veces, especialmente con objetos dinámicamente adjudicados, nos 
interesa la posibilidad de pasar un puntero nulo. Esto puede ser 
aceptable para que la función realice otra tarea; quizás instanciar la 
memoria dinámicamente. Con referencias, no podríamos crear una que fuera 
nula.

Esta idea es aparente a la hora de usar objetos polimórficos con el 
cásting dinámico. Por ejemplo,

void f( claseAbstracta *ptr )
{
   // No hacer nada
   if( !ptr ) return;

   if( dynamic_cast< claseConcreta1 * >( ptr ) )
   {
     claseConcreta1 *ptr1 = dynamic_cast< claseConcreta1 * >( ptr );
     ...
   }
   else if( dynamic_cast< claseConcreta2 * >( ptr ) )
   {
     claseConcreta2 *ptr2 = dynamic_cast< claseConcreta2 * >( ptr );
     ...
   }
}

void g( claseAbstracta &ref )
{
   try
   {
     claseConcreta1 &ref1 = dynamic_cast< claseConcreta1 & >( ref );
     ...
   }
   catch( bad_cast &ex )
   {
     claseConcreta2 &ref2 = dynamic_cast< claseConcreta2 & >( ref );
     ...
   }
   catch( ... )  {}
}

int main()
{
   claseAbstracta *pObj;
   ...
   pObj = new claseConcreta2;
   f( pObj );
   g( *pObj );
   ...
}

Como puedes ver, tenemos un pequeño problema con 'g()', por lo que 
hacemos uso de excepciones, porque no existe la "referencia nula". En 
este caso, existe otra solución que se basa en la sobrecarga; por ejemplo,

void g( claseAbstracta &ref )  {}

void g( claseConcreta1 &ref )
{
   ...
}

void g( claseConcreta2 &ref )
{
   ...
}

Obviamente, necesitamos varias funciones sobrecargadas para cada tipo 
polimórfico, por lo que quizá no sea la mejor opción. Esto significaría 
que usaríamos una sola función, pero con referencias, no podemos 
tratarlas como "nulas", porque este concepto no existe en C++. Por esta 
razón, tenemos que emplear el mecanismo de excepciones. Con sólo dos 
clases derivadas, no parece que sea tan complicado, pero si tenemos más, 
estaríamos usando varios bloques anidados de 'try/catch'. Esto sí puede 
ser engorroso de escribir y por supuesto para la legibilidad del código.


Seguro que me dejo algún otro matiz, pero espero haber aclarado las dudas.

Steven





Más información sobre la lista de distribución Cconclase