C++ FAQ

Referencias

¿Que es una refencia?

Es un alias (un nombre alternativo) de un objeto.

Referencias son usadas frecuentemente para realizar pase por referencia:

void swap(int& i, int& j)
{
   int tmp = i;
   i = j;
   j = tmp;
}
 
int main()
{
   int x, y;
   ...
   swap(x,y);
   ...
}

Donde i y j son los alias de x e y respectivamente los cuales están en la función main. En otras palabras, i es x, no un puntero a x, ni una copia de x, pero sí x en sí misma. Cualquier cosa que hagamos con i también le hacemos a x, y viceversa.

Ok. Así es como debe pensar un programador sobre las referencias. Ahora, con el riesgo confundirle dándole una perspectiva diferente, así es como referencias se implementan. Detrás de todo, una referencia i al objeto x suele ser la dirección de memoria del objeto x. Pero cuando un programador utiliza: i++, el compilador genera código que incrementa el valor de x. En particular, la dirección que el compilador utiliza para hallar x no cambia. Un programador de C va a pensar en esto como un paso de variables mediante punteros, con la variante sintáctica (1) de mover el & desde la variable origen a la variable de destino, y (2) la eliminación de los *s. En otras palabras, un programador de C pensará que i es una macro para acceder a (*p), donde p es un puntero a x (por ejemplo, el compilador automáticamente des-referencia el puntero subyacente: i++ cambia a (*p)++; i = 7 se cambia automáticamente a p* = 7).

Nota importante: A pesar de que una referencia a menudo se implementa utilizando una dirección en el lenguaje ensamblador, por favor, no pensar alegremente en una referencia como si fuera un puntero a un objeto. Una referencia es el objeto. No es un puntero al objeto, ni una copia del objeto. Es el objeto.

¿Qué pasa si modifico una referencia?

Cambia el estado del referente (el referente es el objeto al que se refiere la referencia).

Recuerde: la referencia es el referente, por lo que los cambios de referencia cambian al estado del referente. En la jerga del compilador, una referencia es un “lvalue” (del inglés left value) algo que puede aparecer en el lado izquierdo de un operador de asignación.

¿Qué sucede si retorno una referencia?

Una llamada a una función puede aparecer en el lado izquierdo de un operador de asignación.

Esta característica puede parecer extraño al principio. Por ejemplo, nadie piensa que la expresión f() = 7 tiene sentido. Sin embargo, si a es un objeto de clase Array, la mayoría piensa que a[j] = 7 tiene sentido, pero a[j] es en realidad una llamada a una función (declarada como Array::operator[] (int), que es el operador de subíndice para la clase Array).

class Array
{
public:
   int size() const;
   float& operator[] (int index);
   ...
};
 
int main()
{
   Array a;
   for (int i = 0; i < a.size(); ++i)
     a = 7;    // Esta linea invoca a Array::operator[](int)
   ...
}

¿Qué significa objeto.metodo1().metodo2()?

Se trata de una cadena de llamada a métodos/funciones, por esa razón se le llama también cadena de métodos.

Lo primero que se ejecuta es objeto.metodo1(). Esto devuelve un objeto, lo que podría ser una referencia a un objeto (es decir, metodo1() podría terminar con devolver *this;), o podría ser algún otro objeto. Vamos a llamarle objetoB al objeto devuelto. Luego objetoB se convierte en el objeto que llama a metodo2().

El uso más común de encadenamiento método está en la libreria iostream. Por ejemplo: cout<<x<<y funciona porque cout<<x es una función que devuelve cout.

Un uso menos común, pero bastante pulido, para el encadenamiento de métodos de es la notación de parametros con nombre o named parameters.

¿Cómo puedo cambiar la referencia a un nuevo objeto?

De ninguna manera.

No se puede separar la referencia del referente.

A diferencia de un puntero, una vez que una referencia está vinculada a un objeto, no se puede “modificar” para que refiera a otro objeto. La propia referencia no es un objeto (que no tiene identidad, tomar la dirección de una referencia que da la dirección del referente, recuerde: la referencia es su referente).

En ese sentido, una referencia es similar a un puntero constante, como int* const p; (a diferencia de un puntero a un constante como const int* p;). Pero por favor, no confunda las referencias con los punteros, que son muy diferentes desde el punto de vista del programador.

¿Cuándo debo utilizar referencias, y cuando punteros?

Utilice referencias cuando pueda, y punteros cuando sea necesario.

Las referencias son generalmente preferibles a los punteros cuando no es necesario “volver a asignar la referencia“. Esto generalmente significa que las referencias son más útiles en las interfaces públicas de una clase. Las referencias aparecen típicamente como la parte visible de un objeto, y los punteros como la parte interior.

La excepción a lo anterior es que cuando el parámetro de una función o un valor de retorno necesita un “centinela” de referencia -una referencia que no refiere a un objeto. Esto generalmente se realiza mejor mediante la devolución de un puntero, y cuando éste puntero es NULL tiene un significado especial (referencias deben ser siempre alias de objetos, no una desreferencia a un puntero NULL).

Nota: Antiguos programadores C a veces no les gusta las referencias, ya que proporciona la semántica de referencia que no es explícita en el código del referente. Después de un poco de experiencia en C++, uno rápidamente se da cuenta de que esto es una forma de ocultar la información, es una ventaja mas que un riesgo. Por ejemplo, los programadores deben escribir código en el lenguaje del problema y no en lenguaje máquina.

¿Qué significa que una referencia debe apuntar a un objeto, y no des-referenciar a un NULL?

El siguiente código es ilegal:

 T* p = NULL;
 T& r = *p;  //← ilegal

NOTA: Por favor, no me envíe correos electrónicos diciendo que el código anterior funciona en el compilador C++ particular suyo. Sigue siendo ilegal. El lenguaje C++, como se define en el estándar de C++, dice que es ilegal, que establece que es ilegal. El estándar C++ no requiere de un diagnóstico de este error particular, lo que significa que el compilador en particular no está obligado a notar que p es NULL o mostrar un mensaje de error, pero sigue siendo ilegal. El lenguaje C++ también no requiere que el compilador genere código para que el programa explote en tiempo de ejecución. De hecho, la particular versión de su compilador puede, o no, generar código que usted piensa que tiene sentido si escribe lo anterior en un programa. Pero ese es el punto: dado que el compilador no necesariamente va a generar el código sensible, usted no sabe lo que el compilador va a hacer. Así que por favor no me envíe correos electrónicos diciendo que el compilador genera un código particular, bueno, no me importa. Sigue siendo ilegal. Tómese un tiempo para revisar el estándar de C++ en la sección 8.3.2p4.

A modo de ejemplo y no como limitación, un compilador puede optimizar haciendo una prueba de NULL a referencias, ya que “sabe” que todas las referencias deben referir a objetos reales -las referencias no son (legalmente) desreferencias a un puntero NULL. Usted puede ver si su compilador puede hacer eso mediante la siguiente prueba:

 ...del ejemplo anterior...
 T* p2 = &r;
 if (p2 == NULL) {
   ...
 }

Como se mencionó anteriormente, esto es sólo un ejemplo de algo que podría escribir para su compilador basado en las reglas del lenguaje que dice que la referencia debe referirse a un objeto válido. No limite su pensamiento para el ejemplo anterior, el mensaje en este punto es que el compilador no está obligado a hacer algo razonable si usted viola las reglas. Así que no se violen las reglas!

[quote]Paciente: “Doctor, doctor, mi ojo me duele cuando lo toco con una cuchara”.

Doctor: “Entonces deja de usar la cuchara con tu ojo” [/quote]

¿Qué es un handle de un objeto? ¿Es un puntero? ¿Es una referencia? ¿Es un puntero a otro puntero? ¿Qué es?

El término handle (manejador) se utiliza para referirse a cualquier técnica que le permite llegar a otro objeto – un pseudo puntero generalizado. El término es (deliberadamente) ambiguo y vago.

La ambigüedad es en realidad una ventaja en ciertos casos. Por ejemplo, durante el diseño inicial no podría estar listo para hacer a una representación específica de los handles. Usted no pudo estar seguro de si te quería punteros vs referencias vs punteros a punteros vs referencias a punteros vs índices enteros de una matriz vs cadenas (o alguna otra clave/índice) que se pueden obtener de una tabla hash (u otra estructura de datos) vs claves de bases de datos vs alguna otra técnica. Si se limita a saber que usted necesitará algún tipo de cosita que únicamente le ayudará a identificar y llegar a un objeto, esa cosita es un handle.

Así que si su objetivo final es permitir que un segmento código identifique/busque un objeto específico de una clase Fred, necesita pasar un handle de Fred a ese codigo. El handle puede ser una cadena que se puede utilizar como una clave, conocidos por algunos como tabla de búsqueda (por ejemplo, una clave en std::map<std::string,Fred> o un std::map), o puede ser un número entero de que sería un índice en un conjunto conocido (por ejemplo, Fred* array = new Fred[maxNumFreds]), o puede ser un simple puntero Fred*, o puede ser algo más.

Los novatos a menudo piensan en términos de punteros, pero en realidad hay riesgos bajos en la utilización de punteros en bruto. Por ejemplo, si quiero mover el objeto Fred? ¿Cómo saber cuándo es seguro eliminar los objetos Fred? ¿Qué pasa si el objeto Fred necesita ser serializado (temporalmente) en el disco? etc, etc La mayoría de las veces que agregar más capas de indirección para manejar situaciones como estas. Por ejemplo, los handles pueden ser Fred**, donde puntero a Fred * está garantizados que no se moverá nunca, pero cuando el objeto Fred necesite moverse, solo deben actualizar los punteros a Fred *. O bien, hacer que el handle sea un número entero que tenga los objetos Fred (o punteros a los objetos Fred) levantado en una tabla/matriz/o lo que sea.

El punto es que usamos la palabra handle, cuando todavía no conocemos los detalles de lo que vamos a hacer.

Podemos usar también la palabra handle cuando queremos ser vagos acerca de lo que ya hemos hecho (a veces el término cookie mágica se utiliza para estos casos, como en “El software pasa alrededor de una cookie mágica que se utiliza de manera única identificar y localizar, en este caso, al objeto Fred “). La razón por la que (a veces) quieremos ser vagos acerca de lo que ya hemos hecho es reducir al mínimo el efecto dominó si/cuando los detalles específicos/representación del handle cambie. Por ejemplo, si/cuando alguien cambia el handle de una cadena en una tabla de hash a un entero que se busca en una matriz, no quisiéramos ir a actualizar un trillón de líneas de código.

Para facilitar aún más el mantenimiento si/cuando los detalles/representación de un handle cambian (o en general hacer que el código sea más fácil de leer/escribir), a menudo vamos a encapsular el handle en una clase. Esta clase a menudo trandrá sobrecargas del operador -> y el operador * (ya que el handle actúa como un puntero, esto va hacer que se parezca mas a un puntero).

Dejar una Respuesta