[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