Esta sección presenta extensiones al lenguaje C que fueron introducidas por C++ [6]. También tiene que ver con conceptos orientados a objetos y su puesta en práctica.
C++ agrega un nuevo comentario que se inicia con dos salshes (//) y que llega hasta el final de la línea. Se pueden usar ambos estilos de comentarios, por ejemplo para comentar bloques grandes de código :
/* El comentario de C puede incluir // y se puede extender sobre varias líneas */ // /* Este es el estilo de comentarios de C++ */ hasta el fin de línea
En C tú debes definir las variables al principio de un bloque. C++ te permite definir variables y objetos en cualquier posición dentro de un bloque. Así, las variables y objetos deberían ser definidas donde vayan a ser usadas.
C++ presenta un nuevo tipo de datos llamado referencia. Puedes pensar de ellas como si fueran "alias" de variables u objetos "reales". Como un alias no puede existir sin su contraparte real, no se pueden definir referencias por si solas. El ampersand (& ;) se usa para definir una referencia. Por ejemplo :
int ix; /* ix es la variable "real" */ int &rx = ix; /* rx es el "alias" de ix */ ix = 1; /* también rx == 1 */ rx = 2; /* también ix == 2 */
Las referencias pueden ser usadas como argumentos de funciones y regresar valores. Esto permite pasar parámetros como referencia o regresar un "manejador" (handle) a una variable u objeto calculado(a).
La tabla 8.1 se ha adoptado de [1] y te provee de una revisión a posibles declaraciones. No está completa en el sentido de que no muestra cada posible combinación y algunas de ellas no se presentan aquí, debido a que no las utilizaremos. Sin embargo, éstas son las que probablemente vayas a usar con frecuencia.
En C/C++ tú puedes usar el modificador const para declarar que aspectos particulares de una variable (u objeto) sean constantes. La siguiente tabla 8.2 enlista las posibles combinaciones y describe sus significados. Subsecuentemente, son presentados algunos ejemplos que demuestran el uso de const.
Investiguemos ahora algunos ejemplos de variables constantes y como usarlas. Considera las siguientes declaraciones (nuevamente de [1]):
int i; // simplemente un integer ordinario int *ip; // apuntador no inicializado a // integer int * const cp = &i; // apuntador constante a integer const int ci = 7; // integer constante const int *cip; // apuntador a un integer constante const int * const cicp = &ci; // apuntador constante a un integer // constante
Las siguientes asignaciones son válidas:
i = ci; // asigna un integer constante a un integer *cp = ci; // asigna integer constante a una variable // que está referenciada por un apuntador constante cip = &ci; // cambia apuntador a integer constante cip = cicp; // pone el apuntador de integer constante a // referenciar la variable de apuntador constante a // integer constante
Las siguientes asignaciones son no válidas:
ci = 8; // no se puede cambiar el valor de un integer constante *cip = 7; // no se puede cambiar un integer constante referenciado // por apuntador cp = &ci; // no se puede cambiar el valor de un apuntador constante ip = cip; // esto permitiría cambiar el valor del // integer constante *cip con *ip
Cuando se usan con referencias, se deben considerar algunas peculiaridades. Véase el siguiente programa de ejemplo :
#include <stdio.h> int main() { const int ci = 1; const int &cr = ci; int &r = ci; // crea integer temporal para referencia // cr = 7; // no puede asignar el valor a referencia constante r = 3; // cambia el valor del integer temporal print("ci == %d, r == %d\n", ci, r); return 0; }
Cuando se compila con GNU g++, el compilador emite la siguiente advertencia:
Lo que realmente sucede es, que el compilador automáticamente crea una variable integer temporal con el valor de ci con el cuál la referencia r es inicializada. Por consecuencia, cuando se cambia r el valor del integer temporal es cambiado. Esta variable temporal dura tanto como la referencia r.
La referencia cr es definida como de solo-lectura (referencia constante). Esto inhabilita su uso del lado izquierdo de las asignaciones. Borra por favor el comentario en frente de la línea en particular para checar el resultante mensaje de errror de tu compilador.
C++ permite la sobrecarga de funciones como está definida en la sección 6.3. Por ejemplo, podemos definir dos funciones diferentes max(), una que regrese el mayor de dos enteros y una que regrese el mayor de dos strings:
#include <stdio.h> int max(int a, int b) { if (a > b) return a; return b; } char *max(char *a, char * b) { if (strcmp(a, b) > 0) return a; return b; } int main() { printf("max(19, 69) = %d\n", max(19, 69)); printf("max(abc, def) = %s\n", max("abc", "def")); return 0; }
El programa de ejemplo de arriba define estas dos funciones que difieren en su lista de parámetros, de ahí que, defina dos diferentes funciones. La primera llamada a printf() en la función main() emite una llamada a la primera versión de max(), debido a que lleva dos integers como argumento. En forma similar, la segunda llamada a printf() conduce a una llamada a la segunda versión de max().
Las referencias se pueden usar para proveer una función con un alias de un argumento real de llamada de función. Esto permite cambiar el valor del argumento de llamada de función tal como se conoce de otros lenguajes con parámetros de llamada-por-referencia :
void mira (int porValor, int &porReferencia) { porValor = 42; porReferencia = 42; } void mar () { int ix, jx; ix = jx = 1; mira (ix, jx); /* ix == 1, jx == 42 */ }
C++ permite la declaración y la definición de clases. Las instancias de las clases se llaman objects. Recuerda nuevamente el ejemplo del programa de dibujo de la sección 5. Ahí hemos desarrollado un clase Point (punto). En C++ esto se vería así:
class Point { int _x, _y; // coordinadas del punto public: // principio de la sección de interface void setX(const int val); void setY(const int val); int getX() { return _x; } int getY() { return _y; } }; Point apoint;
Esto declara una clase Point y define un objeto apoint. Se puede pensar de una definición de clase como una definición de una estructura con funciones (o "métodos"). Adicionalmente, tú puedes especificar los derechos de acceso en más detalle. Por ejemplo, _x y _y son private (privados), debido a que los elementos de las clases son privados "por default". Consecuentemente, nosotros debemos explícitamente "switchear" los derechos de acceso para declarar que los siguientes sean públicos. Logramos éso por medio de la palabra clave public seguida de dos puntos ( : ) Cada elemento siguiente a esta palabra clave será ahora accesible desde afuera de la clase.
Podemos volver a los derechos de acceso privados empezando otra sección privada con private:. Esto se puede hacer las veces que sea necesario:
class Foo { // privado "por default" ... public: // lo que sigue es público hasta ... private: // ... aquí, donde regresamos a privado ... public: // ... y de regreso a público. };
Recuerda que una estructura struct es una combinación de varios elementos de datos que son accesibles desde afuera. Ahora, podemos expresar una estructura con la ayuda de una clase, donde todos los elementos son declarados para que sean públicos :
class Struct { public: // Los elementos de estructuras son públicos "por default" // elementos, métodos };
Esto es exactamente lo que C++ hace con struct. Las estructuras son manejadas como clases. Donde los elementos de clases (definidas con class) son privadas "por default", los elementos de de las estructuras (definidos con struct) son públicos. Sin embargo, también podemos usar private: para convertir una sección a privada dentro de una estructura.
Regresemos a nuestra clase Point. Su interface empieza con la sección pública donde definimos cuatro métodos. Dos por cada coordenada para establecer y obtener su valor. Los métodos para establecer (set) solamente están definidos. Su funcionalidad real está aún por definirse. Los métodos para obtener (get) tienen un cuerpo de función : Están definidos dentro de la clase o, en otras palabras, son métodos insertados (inlined methods).
Este tipo de definición de método es útil para cuerpos de función pequeños y sencillos. También mejoran el desempeño, debido a que los cuerpos de los métodos insertados son "copiados" dentro del código dondequiera que una llamada a tal método tenga lugar.
Por el contrario, las llamadas a los métodos para establecer (set methods) resultarían en una "real" llamada a función. Definimos estos métodos afuera de la declaración de la clase. Esto se hace necesario, para indicar a que clase pertenece una definición de método. Por ejemplo, otra clase pdría sencillamente definir un método setX() el cuál es totalmente diferente de aquél en Point. Debemos poder determinar el ámbito de la definición ; por lo tanto, usamos el operador de ámbito " : :" :
void Point::setX(const int val) { _x = val; } void Point::setY(const int val) { _y = val; }
Aquí definimos el método setX() (setY()) dentro del ámbito de la clase Point. El objeto apoint puede usar estos métodos para establecer y para obtener información sobre sí mismo :
Point apoint; apoint.setX(1); // Inicialización apoint.setY(1); // // x es necesaria a partir de aquí, de modo que la definimos aquí y // la inicializamos con el valor de la coordenada-x de apoint // int x = apoint.getX();
El problema estriba en como los métodos "saben" de cuál objeto son invocados. Esto se realiza pasando implícitamente un apuntador al objeto invocante, a dicho método. Podemos accesar este apuntador dentro de los métodos con la palabra this. Las definiciones de los métodos setX() y setY() hacen uso de los miembros de la clase (_x y _y, respectively). Si son invocados por un objeto, estos miembros son "automáticamente" mapeados al objeto correcto. Podríamos usar this para ilustrar los que sucede realmente:
void Point::setX(const int val) { this->_x = val; // Uso de this para referenciar al objeto // invocante } void Point::setY(const int val) { this->_y = val; }
Aquí nosotros usamos explícitamente el apuntador this para desreferenciar explícitamente el objeto invocante. Afortunadamente, el compilador "inserta" en forma automática estas desreferencias para los miembros de la clase, de ahí que, realmente podemos usar las primeras definiciones de setX() y setY(). Sin embargo, algunas veces tiene sentido saber que hay un apuntador this disponible que indica al objeto invocante.
En la práctica, nosotros necesitamos llamar los métodos "set" para inicializar un objeto point . Sin embargo, nos gustaría inicializar el punto en el momento que lo definimos. Para ello, usamos los métodos especiales llamados constructores.
class Point { int _x, _y; public: Point() { _x = _y = 0; } void setX(const int val); void setY(const int val); int getX() { return _x; } int getY() { return _y; } };
Los constructores tienen el mismo nombre de la clase (de ese modo pueden ser identificados como constructores). No regresan ningún valor. Al igual que otros métodos, pueden llevar argumentos. Por ejemplo, nosotros podríamos querer inicializar un punto en otras coordenadas que no fueran (0, 0). Para tal efecto, definimos un segundo constructor que lleve dos argumentos integer dentro de la clase :
class Point { int _x, _y; public: Point() { _x = _y = 0; } Point(const int x, const int y) { _x = x; _y = y; } void setX(const int val); void setY(const int val); int getX() { return _x; } int getY() { return _y; } };
Los constructores son llamados implícitamente cuando definimos objetos de sus clases :
Point apoint; // Point::Point() Point bpoint(12, 34); // Point::Point(const int, const int)
Con los constructores podemos inicializar nuestros objetos al momento de la definición tal como lo hemos pedido en la sección 2 para nuestra lista ligada sencilla. Ahora, nosotros podemos definir una clase List donde los constructores se ocupen de inicializar en forma correcta sus objetos.
Si queremos crear un punto a partir de otro punto, es decir, copiando las propiedades de un objeto a uno recién creado, algunas veces tenemos que tener cuidado con el proceso de copiado. Por ejemplo, considera la clase List que asigna memoria para sus elementos en forma dinámica. Si queremos crear una segunda lista que sea una copia de la primera, debemos asignar memoria y copiar los elementos individuales. En nuestra clase Point para ello nostros añadimos un tercer constructor que se ocupie de copiar correctamente los valores de un objeto al objeto recién creado :
class Point { int _x, _y; public: Point() { _x = _y = 0; } Point(const int x, const int y) { _x = x; _y = y; } Point(const Point &from) { _x = from._x; _y = from._y; } void setX(const int val); void setY(const int val); int getX() { return _x; } int getY() { return _y; } };
El tercer constructor lleva como argumento una referencia constante a un objeto de la clase Point y le asigna a _x y a _y los valores correspondientes del objeto provisto.
Este tipo de constructor es tan importante que tiene su propio nombre : copy constructor (constructor para copia). Es altamente recomendable que tu proveas dicho constructor para cada una de tus clases, aún si es tan simple como en nuestro ejemplo. El "copy constructor" es llamado en los siguientes casos :
Point apoint; // Point::Point() Point bpoint(apoint); // Point::Point(const Point &) Point cpoint = apoint; // Point::Point(const Point &)
Con la ayuda de los constructores hemos cumplido con uno de nuestros requerimientos de implementación de tipos de datos abstractos : La inicialización al momentod de la definición. Aún necesitamos un mecanismo que automáticamente "destruya" un objeto cuando ya no sea válido (por ejemplo, por haber abandonado su ámbito). Para tal efecto, las clases pueden definir destructores.
void foo() { List alist; // List::List() inicializa una // lista vacía. ... // añade/elimina elementos } // ¡Llamada al destructor!
La destrucción de los objetos tiene lugar cuando el objeto abandona su ámbito de definición o es explícitamente destruído. Esto último sucede cuando nosotros dinámicamente asignamos un objeto y lo liberamos cuando ya no nos es necesario.
Los destructores se declaran en forma similar a los constructores. Así, también usan el nombre de la clase que definitoria prefijado con una tilde (~ ):
class Point { int _x, _y; public: Point() { _x = _y = 0; } Point(const int x, const int y) { _x = xval; _y = yval; } Point(const Point &from) { _x = from._x; _y = from._y; } ~Point() { /* ¡Nada qué hacer! */ } void setX(const int val); void setY(const int val); int getX() { return _x; } int getY() { return _y; } };
Los destructores no llevan argumentos. Es hasta ilegal definir alguno ; debido a que los destructores son llamados implícitamente en el momento de eliminación : No tienes ninguna oportunidad de especificar argumentos reales.