Home MundoTec Software Código fuente Tutorial / pdf Minijuegos
Cerrar

Tutorial Programación C++

Tutorial PROGRAMACION en C++. PARTE 2







FUNCIONES

Declaración de funciones

La declaración de una función nos da el nombre de la función, el tipo del valor que retorna y el número y tipo de parámetros que deben pasársele. La sintaxis es:

tipo_retorno nom_funcion (lista_tipos_param);
lista_tipos_param = tipo_param_1, tipo_param_2, ... , tipo_param_n

Donde los paréntesis y el punto y coma son obligatorios. El tipo de retorno se puede omitir pero el compilador asume que es int. En C una función sin lista de parámetros se considera que tiene un número de parámetros indefinidos, mientras que en C++ se entiende que no se le pasa nada (para pasar un número de parámetros indefinido se ponen tres puntos (...) en la lista de parámetros). Lo más recomendable para evitar confusiones es poner siempre void como parámetro cuando no vamos ha pasar nada.
En la lista de parámetros podemos ponerle nombre a los parámetros, pero el compilador los ignorará.

Definición de funciones

Una definición de una función es una declaración en la que se incluye el cuerpo de la misma y se da nombre a los parámetros. La sintaxis es:

tipo_retorno nom_funcion (lista_param) {
cuerpo de la función
}
lista_param = tipo_param_1 nom_param_1, ... , tipo_param_n nom_param_2;

Donde los nombres de los parámetros son obligatorios.

Hay que indicar que una lista de parámetros debe indicar explícitamente el tipo de cada uno de ellos, así:
int producto (int a, b) { return (a*b); }

sería incorrecto, ya que el segundo parámetro no tiene tipo, habría que escribir:

int producto (int a, int b) { return (a*b); }

Es decir, una lista de parámetros no es una declaración de variables.
Si no deseamos usar algún parámetro podemos indicarlo no poniéndole nombre en la definición de la función.
Dentro del cuerpo de la función podemos declarar variables, pero no funciones, es decir, no se permiten funciones anidadas.

Paso de parámetros

En C++ todos los parámetros se pasan por valor, es decir, se hace una copia de los parámetros cuando llamamos a la función. En la llamada, antes de hacer la copia, se chequea que los parámetros actuales (los valores o variables especificados en la llamada) son del mismo tipo que los parámetros formales (los que declaramos en la lista de parámetros de la declaración de la función). Si los tipos son iguales o existe una conversión implícita la llamada puede realizarse, si no son iguales deberemos realizar conversiones explícitas.
Veamos un ejemplo de paso de parámetros:

#include
void f (int val, int& ref) {
val++;
ref++;
}
main (){
int i=1;
int j=1;
f(i.j);
cout << "i vale " << i << " y j " << j << endl;
}

El resultado de ejecutar el programa será i vale 1 y j vale 2. Esto se debe a que cuando pasamos el parámetro val, esperamos un entero y lo que recibe la función es una copia del valor de i (1), al incrementar val en 1 modificamos la copia y la i queda igual. Sin embargo, el segundo parámetro es una referencia (la dirección de una variable entera), y al llamar a la función con j lo que se copia es su dirección, luego las modificaciones de ref se producen en la posición en la que se encuentra j. Como se ve, podemos considerar que al poner un parámetro de tipo referencia estamos realmente pasando el parámetro por referencia.
Hay que indicar que las referencias se usan igual que las variables del tipo, es decir, el ref++ no modifica la dirección, sino que modifica el valor referenciado.

Parámetros array

Hay un caso especial en el que aparentemente no se copia el parámetro, que es al pasar vectores.
Por ejemplo:

void multiplica_matriz (int a[5], int val) {
for (int i=0; i<5; i++)
a[i] *= val;
}
main () {
int m[5] = {1, 2, 3, 4, 5};
multiplica_matriz(m, 2);
}

Después de la llamada a multiplica_matriz m valdría {2,4,6,8,10}. ¿Qué ha sucedido?
El problema está en la forma de tratar los vectores del C++, ya que el nombre de un vector o una matriz es en realidad un puntero al primer elemento de la misma, y utilizando el operador [] lo que hacemos es sumarle a ese puntero el tamaño de los elementos del array multiplicado por el valor entre corchetes. Esta es la razón de que los vectores comiencen en 0, ya que el primer elemento es el apuntado por el nombre del vector.
Todo esto implica que realmente su hayamos copiado el parámetro, sólo que el parámetro es el puntero al primer elemento, no el contenido del vector, y por tanto las modificaciones del contenido se hagan sobre los valores de la matriz pasada como parámetro.
Veremos en el punto dedicado a variables dinámicas que realmente los tipos vector y puntero son equivalentes (podremos usarlos indistintamente).

Retorno de valores

Para retornar valores una función emplea la instrucción return, que como ya vimos se puede colocar en cualquier punto de la misma y más de una vez. Cuando se ejecuta un return la función sale.
Hay que indicar que también se chequea el tipo de los retornos, es decir, lo que retornemos debe ser del mismo tipo (o convertible implícitamente) que el declarado como de retorno para la función.
Cuando una función se declara de manera que retorna algo, es un error no poner ningún return en la misma.

Sobrecarga de funciones

Una de las características más interesantes del C++ en cuanto a funciones se refiere es la posibilidad de definir distintas funciones con el mismo nombre aunque con distintos parámetros.
Esta capacidad se denomina sobrecarga de funciones y es útil para llamar de la misma forma a funciones que realizan operaciones similares pero sobre operandos distintos. En está frase hemos dado una clave para la definición de los TAD en C++, los operadores son funciones y como tales también pueden ser sobrecargadas. Este aspecto se estudiará una vez hayamos visto las clases, ya que tienen su mayor aplicación en estas.
El tipo de retorno debe ser igual, ya que de lo contrario se pueden provocar ambigüedades como que llamemos a una función esperando un real y tengamos dos funciones idénticas, una que retorna reales y otra enteros, ¿Cuál debemos usar? Si no hubiera conversiones implícitas estaría claro, pero podemos querer usar la que retorna enteros y que se transforme en real. La solución es no permitir retornos diferentes, lo único que debemos hacer es darles nombres diferentes a las funciones.

Parámetros por defecto

Algunas veces una función necesita determinados parámetros en condiciones excepcionales pero estos suelen tener unos valores fijos en los casos normales. Para no tener que dar estos parámetros en los casos normales el C++ permite el uso de parámetros por defecto. La sintaxis es muy simple, al declarar una función ponemos lo mismo que antes sólo que los parámetros por defecto van al final y se escriben no sólo poniendo el tipo sino también un signo igual y un valor:

tipo nom_funcion (lista_param, lista_param_por_defecto);
lista_param_por_defecto = tipo_pd_1 = vd1, ..., tipo_pd_n = vd_n

Para usar la función podemos llamarla especificando sólo los parámetros indefinidos o poniendo estos y algunos parámetros por defecto con un valor.
Ejemplo:

void imprime_int (int valor, int base = 10);
// también se puede declarar como
// void imprime_int (int , int = 10);
// los nombres no importan
void imprime_int (int valor, int base) {
switch (base) {
case 8:
cout << oct << i;
break;
case 10:
cout << dec << i;
break;
case 16:
cout << hex << i;
break;
default:
cerr << "Función imprime_int (): Representación en base" << base \
<< " indefinida\n";
}
}

Para imprimir un número en decimal haremos:

imprime_int (num);

Si queremos cambiar la base hacemos:

imprime_int (num, 8); // imprime en octal
imprime_int (num,16); // imprime en hexadecimal
imprime_int (num, 4); // imprime un error.

Parámetros indefinidos

Para determinadas funciones puede ser imposible especificar el número y tipo de todos los parámetros esperados, por lo que el C++ define un mecanismo para utilizar funciones con un número de parámetros indefinido. La sintaxis para declarar las funciones es:
tipo nom_funcion (lista_args ...); // al terminar la lista de args no ponemos coma

de esta manera, podemos declarar funciones que reciban siempre una serie de parámetros de forma normal y luego otros indefinidos.
Para acceder a los parámetros variables empleamos un conjunto de macros definidas en la cabecera estándar . Haremos lo siguiente:

1. Declaramos una variable del tipo va_list que es el tipo que asignamos a la lista de parámetros indefinidos.
2. Para inicializar la lista de argumentos empleamos la macro va_start que toma como argumentos la variable de tipo va_list y el último parámetro formal.
3. Para ir accediendo a los distintos elementos utilizamos la macro va_arg , que va leyendo los parámetros en orden y toma como argumentos la lista de tipo va_list y el tipo de la siguiente variable.
4. Antes de salir de una función que ha llamado a va_start debemos llamar a va_end con la variable de tipo va_list como parámetro.

El problema fundamental es saber que tipo asignar a cada variable y cuantas hay, la solución depende del tipo de parámetros que pasemos y lo que haga la función. Una forma fácil de saber 31el tipo y número de parámetros variables es pasarle a la función una cadena de caracteres con los tipos.
Veamos como se usa con un ejemplo:

#include
#include // macros parámetros variables
#include // función strlen
void param_test
(char * ...);
void param_test (char *tipos ...) {
int i;
va_list ap; // ap es la lista de parámetros
va_start (ap, tipos); // inicialización lista de param.
i = strlen (tipos); // la longitud de la cadena es el no de param
// nuestra función reconoce tipos enteros y tipos reales (los reales sólo de tipo double).
for (int j=0; j< i; j++) {
switch (tipos[j]) {
case 'e':
int iv = va_arg(ap, int);
cout << "Parámetro " << j << " = " << iv << " de tipo entero\n";
break;
case 'r':
double dv = va_arg(ap, double);
cout << "Parámetro " << j << " = " << dv << " de tipo real\n";
break;
default:
cout << "Parámetro " << j << " de tipo desconocido\n";
return;
} // end switch
} // end for
va_end(ap); // terminamos con la lista de param.
}
void main (void) {
param_test ("eer", 12, 5, 5.35);
};

La salida de este ejemplo es:

Parámetro 0 = 12 de tipo entero
Parámetro 1 = 5 de tipo entero
Parámetro 2 = 5.35 de tipo real

Hay que decir que en el programa salimos abruptamente cuando no conocemos el tipo porque el resto de parámetros se leerán mal si realmente nos hemos equivocado. Por otro lado, si le decimos que el parámetro es de un tipo y lo pasado como parámetro es de otro lo más normal es que la impresión sea incorrecta, ya que no hacemos chequeo de tipos y si pasamos un entero (que, por ejemplo, ocupa 2 bytes) y leemos un real (por ejemplo de 4 bytes), habremos leído dos bytes de más, que le harán perder el sentido al real y además consumirán 2 bytes del siguiente parámetro. Es decir, si pasamos un parámetro erróneo lo más probable es que los demás también se pierdan.

Recursividad

Las funciones del C++ pueden ser recursivas, es decir, se pueden llamar a sí mismas. Lo único interesante de las funciones recursivas es que las variables declaradas dentro del cuerpo de la función que sean estáticas ( static ) no pierden su valor entre llamadas, es decir, no se crean y se destruyen en la pila, sino que ocupan siempre la misma posición, y por tanto cada modificación que se haga de la variable será valida entre sucesivas llamadas. Otra ventaja de esta aproximación es que si no importa para la recursividad que la variable mantenga su valor 32después de una llamada recursiva (por ejemplo una variable temporal para intercambios), al declararla estática no tenemos que reservar espacio para ella en cada llamada (las llamadas recursivas consumen menos pila).
Aunque comento esto para funciones recursivas, la verdad es que esto se cumple para todas las funciones, luego podemos tener una variable estática que se use para contar el número de veces que se llama a una función (por ejemplo).

Punteros a funciones

Con las funciones sólo podemos hacer dos cosas, llamarlas u obtener su dirección, es decir, podemos definir punteros a funciones (que luego nos sirven para llamarlas).
La declaración de un puntero a función se hace:

tipo_retorno (*nom_var) (lista_tipos_argumentos);

El paréntesis es necesario, ya que el operador de función tiene más preferencia que el puntero, por lo que si escribimos:

tipo_retorno *nom_var (lista_tipos_argumentos);

el compilador interpretará que tenemos una función que retorna un puntero a un elemento de tipo tipo_retorno.
Para llamar a la función sólo tenemos que escribir:

(*nom_var) (param_actuales);

si ponemos:

nom_var (param_actuales);

el compilador seguramente identificará que nom_var es una función y llamará correctamente a la función, aunque es mejor no fiarse de eso.
Para asignar valor a un puntero a función sólo tenemos que escribir:

nom_var= &nom_funcion;

donde nom_función corresponde a una función con parámetros y retorno idénticos a los definidos en el puntero a función.

La función main()

Para terminar con las funciones hablaremos de la función principal de los programas de C++, la función main() .
La función main() se puede definir de varias formas distintas:

1. void main (); // no recibe parámetros ni retorna nada
2. int main (); // no recibe parámetros y retorna un entero al SO (un código de error (generalmente negativo) o 0 si no hay errores)
main (); // igual que la anterior
3. void main (int argc, char *argv[]); // recibe un array con 'argc' cadenas de caracteres y no retorna nada
4. int main (int argc, char *argv[]); // igual que la anterior pero retorna un código de error al SO

La tercera y cuarta formas reciben parámetros desde la línea de comandos en Sistemas Operativos como UNIX o MS-DOS. Es decir, cuando en MS-DOS escribimos un comando como:

C:\> COPY A:\MIO\PROGRAM.C C:

lo que en realidad hacemos es llamar al programa COPY pasándole una serie de parámetros. Lo que recibe la función main() es la línea de comandos, es decir, el nombre de la función seguido de los parámetros, cada palabra será una de las cadenas del array argv[] . Para el ejemplo, main() recibirá:

argc = 3
argv[1] = "COPY"
argv[1] = "A:\MIO\PROGRAM.C"
argv[2] = "C:"

Por último, para salir de un programa en C++ tenemos dos opciones, retornando un valor al SO en la función main() (o cuando esta termine si no retorna nada) o empleando la función exit() , declarada en la cabecera estándar :

void exit (int);

El entero que recibe la función exit() será el valor que se retorne al SO. Generalmente la salida con exit() se utiliza cuando se produce un error.

VARIABLES DINÁMICAS

En la mayoría de los lenguajes de alto nivel actuales existe la posibilidad de trabajar con variables dinámicas, que son aquellas que se crean en tiempo de ejecución. Para soportar el empleo de estas variables aparecen los conceptos de puntero y referencia que están íntimamente relacionados con el concepto de dirección física de una variable.

Punteros y direcciones

Ya hemos mencionado que el C++ es un lenguaje que pretende acercarse mucho al nivel de máquina por razones de eficiencia, tanto temporal como espacial. Por está razón el C++ nos permite controlar casi todos los aspectos de la ocupación y gestión de memoria de nuestros programas (sabemos lo que ocupan las variables, podemos trabajar con direcciones, etc.).
Uno de los conceptos fundamentales en este sentido es el de puntero. Un puntero es una variable que apunta a la dirección de memoria donde se encuentra otra variable. La clave aquí está en la idea de que un puntero es una dirección de memoria.
Pero, ¿cómo conocemos la dirección de una variable declarada en nuestro programa? La solución a esto está en el uso del operador de referencia (&), ya mencionado al hablar de los operadores de indirección. Para obtener la dirección de una variable solo hay aplicarle el operador de referencia (escribiendo el símbolo & seguido de la variable), por ejemplo:

int i = 2;
int *pi = & i; // ahora pi contiene la dirección de la variable i.

El operador de indirección sólo se puede ser aplicado a variables y funciones, es decir, a LValues. Por tanto, sería un error aplicarlo a una expresión (ya que no tiene dirección).
Por otro lado, para acceder a la variable apuntada por un puntero se emplea el operador de indirección (*) poniendo el * y después el nombre del puntero:

int j = *pi; // j tomaría el valor 2, que es el contenido de la variable i anterior

Para declarar variables puntero ponemos el tipo de variables a las que va a apuntar y el nombre del puntero precedido de un asterisco. Hay que tener cuidado al definir varios punteros en una misma línea, ya que el asterisco se asocia al nombre de la variable y no al tipo. Veamos esto con un ejemplo:

char *c, d, *e; // c y e son punteros a carácter, pero d es una variable carácter

Como los punteros son variables podemos emplearlos en asignaciones y operaciones, pero hay que diferenciar claramente entre los punteros como dato (direcciones) y el valor al que apuntan. Veremos esto con un ejemplo, si tenemos los punteros:

int *i, *j;

La operación:

i= j;

hace que i y j apunten a la misma dirección de memoria, pero la operación:

*i = *j;

hace que lo apuntado por i pase a valer lo mismo que lo apuntado por j, es decir, los punteros no han cambiado, pero lo que contiene la dirección a la que apunta i vale lo mismo que lo que contiene la dirección a la que apunta j. Es decir, si i y j apuntaran a las variables enteras a y b respectivamente:

int a, b;
int *i = & a;
int *j = & b;
lo anterior sería equivalente a:

a= b;

Por último indicaremos que hay que tener mucho cuidado para no utilizar punteros sin inicializar, ya que no sabemos cuál puede ser el contenido de la dirección indefinida que contiene una variable puntero sin inicializar.

El puntero NULL

Siempre que trabajamos con punteros solemos necesitar un valor que nos indique que el puntero es nulo (es decir, que no apuntamos a nada). Esto se consigue dándole al puntero el valor 0 o NULL. NULL no es una palabra reservada, sino que se define como una macro en las cabeceras estándar y , y por tanto será necesario incluirlas para usarlo. Si no queremos usar las cabeceras podemos definirlo nosotros de alguna de las siguientes formas:

#define NULL (0)
#define NULL (0L)
#define NULL ((void *) 0)

la primera y segunda formas son válidas porque 0 y 0L tienen conversión implícita a puntero, y la tercera es válida porque convertimos explícitamente el 0 a puntero void . Una forma adecuada de definir NULL es escribiendo:

#ifndef NULL
#define NULL ((void *) 0)
#endif

que define NULL sólo si no está definido (podría darse el caso de que nosotros no incluyéramos las cabeceras que definen NULL, pero si se hiciese desde alguna otra cabecera que si hemos incluido).
Cualquier indirección al puntero NULL se transforma en un error de ejecución.

Punteros void

Ya hemos mencionado que el C++ define un tipo especial denominado void (vacío), que utilizábamos para indicar que una función no retorna nada o no toma parámetros. Además el tipo void se emplea como base para declarar punteros a variables de tipo desconocido.
Debemos recordar que no se pueden declarar variables de tipo void , por lo que estos punteros tendrán una serie de restricciones de uso.
Un puntero void se declara de la forma normal:

void *ptr;

y se usa sólo en asignaciones de punteros. Para trabajar con los datos apuntados por un puntero tendremos que realizar conversiones de tipo explícitas (casts):

char a;
char *p = (char *) ptr;
a = *p;

¿Cuál es la utilidad de estos punteros si sólo se pueden usar en asignaciones? Realmente se emplean para operaciones en las que no nos importa el contenido de la memoria apuntada, sino sólo la dirección, como por ejemplo en las funciones estándar de C para manejo de memoria dinámica (que también son válidas en C++, aunque este lenguaje ha introducido un operador que se encarga de lo mismo y que es más cómodo de utilizar). La definición de algunas de estas funciones es:

void *malloc (size_t N); // reserva N bytes de memoria
void free (void *); // libera la memoria reservada con malloc
/*
size_t es un tipo que se usa para almacenar tamaños de memoria definido en las
cabeceras estándar mediante un typedef, generalmente lo consideraremos un entero sin
más
*/

y para usarlas hacemos:

void *p;
p= malloc (1000); // reservamos 1000 bytes y asignamos a p la dirección del primer byte
free(p); // liberamos la memoria asignada con malloc.

Aunque podemos ahorrarnos el puntero void haciendo una conversión explícita del retorno de la función:

long *c;
c=(long *)malloc(1000); // aunque se transforma en puntero a long sigue reservando 1000 bytes, luego si cada long ocupa 4 bytes sólo nos cabrán 250 variables de tipo long

Por último señalar que en todas las ocasiones en las que hemos hecho conversiones con punteros hemos usado él método de C y no el de C++:

c = long *(malloc (1000));

ya que esto último da error de compilación. La solución a este problema es definir tipos puntero usando typedef :

typedef long *ptr_long;
c = ptr_long(malloc(1000));

Aritmética con punteros

Las variables de tipo puntero contienen direcciones, por lo que todas ellas ocuparan la misma memoria: tantos bytes como sean necesarios para almacenar una dirección en el computador sobre el que trabajemos. De todas formas, no podemos declarar variables puntero sin especificar a qué tipo apuntan (excepto en el caso de los punteros void , ya mencionados), ya que no es sólo la dirección lo que nos interesa, sino que también debemos saber que es lo apuntado para los chequeos de tipos cuando dereferenciamos (al tomar el valor apuntado para usarlo en una expresión) y para saber que cantidades debemos sumar o restar a un puntero a la hora de incrementar o decrementar su valor.
Es decir, el incremento o decremento de un puntero en una unidad se traduce en el incremento o decremento de la dirección que contiene en tantas unidades como bytes ocupan las variables del tipo al que apunta.
Además de en sumas y restas los punteros se pueden usar en comparaciones (siempre y cuando los punteros a comparar apunten al mismo tipo). No podemos multiplicar ni dividir punteros.

Punteros y parámetros de funciones

Lo único que hay que indicar aquí es que los punteros se tratan como las demás variables al ser empleadas como parámetro en una función, pero tienen la ventaja de que podemos poner como parámetro actual una variable del tipo al que apunta el puntero a la que aplicamos el operador de referencia. Esta frase tan complicada se comprende mejor con un ejemplo, si tenemos una función declarada como:

void f (int *);

es decir, una función que no retorna nada y recibe como parámetro un puntero a entero, podemos llamarla con:

int i;
f(&i);

Lo que f recibirá será el puntero a la variable i.

Punteros y arrays

La relación entre punteros y arrays en C++ es tan grande que muchas veces se emplean indistintamente punteros y vectores. La relación entre unos y otros se basa en la forma de tratar los vectores. En realidad, lo que hacemos cuando definimos un vector como:

int v[100];

es reservar espacio para 100 enteros. Para poder acceder a cada uno de los elementos ponemos el nombre del vector y el índice del elemento al que queremos acceder:

v[8] = 100;

Pero, ¿cómo gestiona el compilador los vectores?. En realidad, el compilador reserva un espacio contiguo en memoria de tamaño igual al número de elementos del vector por el número de bytes que ocupan los elementos del array y guarda en una variable la dirección del primer elemento del vector. Para acceder al elemento i lo que hace el compilador es sumarle a la primera dirección el número de índice multiplicado por el tamaño de los elementos del vector. Esta es la razón de que los vectores comiencen en 0, ya que la primera dirección más cero es la dirección del primer elemento.
Hemos comentado todo esto porque en realidad esa variable que contiene la dirección del primer elemento de un vector es en realidad un puntero a los elementos del vector y se puede utilizar como tal. La variable puntero se usa escribiendo el nombre de la variable array sin el operador de indexado (los corchetes):

v [0] = 1100;

es igual que:

*v = 1100;

Para acceder al elemento 8 hacemos:

*(v + 8)

Es decir, la única diferencia entre declarar un vector y un puntero es que la primera opción hace que el compilador reserve memoria para almacenar elementos del tipo en tiempo de compilación, mientras que al declarar un puntero no reservamos memoria para los datos que va a apuntar y sólo lo podremos hacer en tiempo de ejecución (con los operadores new y delete). Excepto por la reserva de memoria y porque no podemos modificar el valor de la variable de tipo vector (no podemos hacer que apunte a una distinta a la que se ha reservado para ella), los vectores y punteros son idénticos.
Todo el tiempo hemos hablado sobre vectores, pero refiriéndonos a vectores de una dimensión, los vectores de más de una dimensión se acceden sumando el valor del índice más a la derecha con el segundo índice de la derecha por el número de elementos de la derecha, etc. Veamos como acceder mediante punteros a elementos de un vector de dos dimensiones:

int mat[4][8];
*(mat + 0*8 + 0) // accedemos a mat[0][0]
*(mat + 1*8 + 3) // accedemos a mat[1][3]
*(mat + 3*8 + 7) // accedemos a mat[3][7] (último elemento de la matriz)

Por último mencionar que podemos mezclar vectores con punteros (el operador de vector tiene más precedencia que el de puntero, para declarar punteros a vectores hacen falta paréntesis).
Ejemplos:

int (*p)[20]; // puntero a un vector de 20 enteros
int p[][20]; // igual que antes, pero p no se puede modificar
int *p[20]; // vector de 20 punteros a entero

Operadores new y delete

Hemos mencionado que en C se usaban las funciones malloc() y free() para el manejo de memoria dinámica, pero dijimos que en C++ se suelen emplear los operadores new y delete .
El operador new se encarga de reservar memoria y delete de liberarla. Estos operadores se emplean con todos los tipos del C++, sobre todo con los tipos definidos por nosotros (las clases). La ventaja sobre las funciones de C de estos operadores está en que utilizan los tipos como operandos, por lo que reservan el número de bytes necesarios para cada tipo y cuando reservamos más de una posición no lo hacemos en base a un número de bytes, sino en función del número de elementos del tipo que deseemos.
El resultado de un new es un puntero al tipo indicado como operando y el operando de un delete debe ser un puntero obtenido con new .
Veamos con ejemplos como se usan estos operadores:

int * i = new int; // reservamos espacio para un entero, i apunta a él
delete i; // liberamos el espacio reservado para i
int * v = new int[10]; // reservamos espacio contiguo para 10 enteros, v apunta al primero
delete []v; // Liberamos el espacio reservado para v
/*

En las versiones del ANSI C++ 2.0 y anteriores el delete se debía poner como:

delete [10]v; // Libera espacio para 10 elementos del tipo de v
*/

Hay que tener cuidado con el delete , si ponemos:

delete v;

sólo liberamos la memoria ocupada por el primer elemento del vector, no la de los 10 elementos.

Con el operador new también podemos inicializar la variable a la vez que reservamos la memoria:

int *i = new int (5); // reserva espacio para un entero y le asigna el valor

En caso de que se produzca un error al asignar memoria con new, se genera una llamada a la función apuntada por

void (* _new_handler)(); // puntero a función que no retorna nada y no tiene parámetros

Este puntero se define en la cabecera y podemos modificarlo para que apunte a una función nuestra que trate el error. Por ejemplo:

#include
void f() {
...
cout << "Error asignando memoria" << endl;
...
}
void main () {
...
_new_handler = f;
...
}

Punteros y estructuras

Podemos realizar todas las combinaciones que queramos con punteros y estructuras: podemos definir punteros a estructuras, campos de estructuras pueden ser punteros, campos de estructuras pueden ser punteros a la misma estructura, etc.
La particularidad de los punteros a estructuras está en que el C++ define un operador que a la vez que indirecciona el puntero a la estructura accede a un campo de la misma. Este operador es el signo menos seguido de un mayor que (->). Veamos un ejemplo:

struct dos_enteros {
int i1;
int i2;
};
dos_enteros *p;
(*p).i1 = 10; // asignamos 10 al campo i1 de la estructura apuntada por p
p->i2 = 20; // asignamos 20 al campo i2 de la estructura apuntada por p

A la hora de usar el operador -> lo único que hay que tener en cuenta es la precedencia de operadores. Ejemplo:

++p->i1; // preincremento del campo i1, es como poner ++ (p->i1)
(++p)->i1; // preincremento de p, luego acceso a i1 del nuevo p.

Por último diremos que la posibilidad de definir campos de una estructura como punteros a elementos de esa misma estructura es la que nos permite definir los tipos recursivos como los nodos de colas, listas, árboles, etc.

Punteros a punteros

Además de definir punteros a tipos de datos elementales o compuestos también podemos definir punteros a punteros. La forma de hacerlo es poner el tipo y luego tantos asteriscos como niveles de indirección:

int *p1; // puntero a entero
int **p2; // puntero a puntero a entero
char *c[]; // vector de punteros a carácter

Para usar las variables puntero a puntero hacemos lo mismo que en la declaración, es decir, poner tantos asteriscos como niveles queramos acceder:

int ***p3; // puntero a puntero a puntero a entero
p3 = &p 2; // trabajamos a nivel de puntero a puntero a puntero a entero no hay indirecciones, a p3 se le asigna un valor de su mismo tipo
*p3 = &p 1; // el contenido de p2 (puntero a puntero a entero) toma la dirección de p1 (puntero a entero). Hay una indirección, accedemos a lo apuntado por p3
p1 = **p3; // p1 pasa a valer lo apuntado por lo apuntado por p3 (es decir, lo apuntado por p2). En nuestro caso, no cambia su valor, ya que p2 apuntaba a p1 desde la operación anterior
***p3 = 5 // El entero apuntado por p1 toma el valor 5 (ya que p3 apunta a p2 que apunta a p1)

Como se ve, el uso de punteros a punteros puede llegar a hacerse muy complicado, sobre todo teniendo en cuenta que en el ejemplo sólo hemos hecho asignaciones y no incrementos o decrementos (para eso hay que mirar la precedencia de los operadores).

PROGRAMACIÓN EFICIENTE

En este punto veremos una serie de mecanismos del C++ útiles para hacer que nuestros programas sean más eficientes. Comenzaremos viendo como se organizan y compilan los programas, y luego veremos que construcciones nos permiten optimizar los programas.

Estructura de los programas

El código de los programas se almacena en ficheros, pero el papel de los ficheros no se limita al de mero almacén, también tienen un papel en el lenguaje: son un ámbito para determinadas funciones (estáticas y en línea) y variables (estáticas y constantes) siempre que se declaren en el fichero fuera de una función.
Además de definir un ámbito, los ficheros nos permiten la compilación independiente de los archivos del programa, aunque para ello es necesario proporcionar declaraciones con la información necesaria para analizar el archivo de forma aislada.
Una vez compilados los distintos ficheros fuente (que son los que terminan en .c, .cpp, etc.), es el linker el que se encarga de enlazarlos para generar un sólo fichero fuente ejecutable.
En general, los nombres que no son locales a funciones o a clases se deben referir al mismo tipo, valor, función u objeto en cada parte de un programa.
Si en un fichero queremos declarar una variable que está definida en otro fichero podemos hacerlo declarándola en nuestro fichero precedida de la palabra extern .
Si queremos que una variable o función sólo pertenezca a nuestro fichero la declaramos static .
Si declaramos funciones o variables con los mismos nombres en distintos ficheros producimos un error (para las funciones el error sólo se produce cuando la declaración es igual, incluyendo los tipos de los parámetros).
Las funciones y variables cuyo ámbito es el fichero tienen enlazado interno (es decir, el linker no las tiene en cuenta).

Los ficheros cabecera

Una forma fácil y cómoda de que todas las declaraciones de un objeto sean consistentes es emplear los denominados ficheros cabecera, que contienen código ejecutable y/o definiciones de datos. Estas definiciones o código se corresponderán con la parte que queremos utilizar en distintos archivos.
Para incluir la información de estos ficheros en nuestro fichero .c empleamos la directiva include , que le servirá al preprocesador para leer el fichero cabecera cuando compile nuestro código.

Un fichero cabecera debe contener:

Definición de tipos === struct punto { int x, y; };
Templates === template class V { ... }
Declaración de funciones === extern int strlen (const char *);
Definición de funciones inline === inline char get { return *p++ ;}
Declaración de variables === extern int a;
Definiciones constantes === const float pi = 3.141593;
Enumeraciones === enum bool { false, true };
Declaración de nombres === class Matriz;
Directivas include === #include
Definición de macros === #define Case break;case
Comentarios === /* cabecera de mi_prog.c */

Y no debe contener:

Definición de funciones ordinarias === char get () { return *p++}
Definición de variables === int a;
Definición de agregados constantes === const tabla[] = { ... }

Si nuestro programa es corto, lo más usual es crear un solo fichero cabecera que contenga los tipos que necesitan los diferentes ficheros para comunicarse y poner en estos ficheros sólo las funciones y definiciones de datos que necesiten e incluir la cabecera global.
Si el programa es largo o usamos ficheros que pueden ser reutilizados lo más lógico es crear varios ficheros cabecera e incluirlos cuando sean necesarios.
Por último indicaremos que las funciones de biblioteca suelen estar declaradas en ficheros cabecera que incluimos en nuestro programa para que luego el linker las enlace con nuestro programa. Las bibliotecas estándar son:

Bibliotecas de C:

assert.h Define la macro assert()
ctype.h Manejo de caracteres
errno.h Tratamiento de errores
float.h Define valores en coma flotante dependientes de la implementación
limits.h Define los límites de los tipos dependientes de la implementación
locale.h Define la función setlocale()
math.h Definiciones y funciones matemáticas
setjmp.h Permite saltos no locales
signal.h Manejo de señales
stdarg.h Manejo de listas de argumentos de longitud variable
stddef.h Algunas constantes de uso común
stdio.h Soporte de E/S
sdlib.h Algunas declaraciones estándar
string.h Funciones de manipulación de cadenas
time.h Funciones de tiempo del sistema

Bibliotecas de C++:

fstream.h Streams fichero
iostream.h Soporte de E/S orientada a objetos (streams)
new.h Definición de _new_handler
strstream.h Definición de streams cadena

El preprocesador

El preprocesador es un programa que se aplica a los ficheros fuente del C++ antes de compilarlos. Realiza diversas tareas, algunas de las cuales podemos controlarlas nosotros mediante el uso de directivas de preprocesado. Como veremos, estas directivas nos permiten definir macros como las de los lenguajes ensambladores (en realidad no se trata más que de una sustitución).
A continuación veremos las fases de preprocesado y las directivas, así como una serie de macros predefinidas. Por último explicaremos lo que son las secuencias trigrafo.

Fases de preprocesado

1. Traduce los caracteres de fin de línea del fichero fuente a un formato que reconozca el compilador. Convierte los trigrafos en caracteres simples.
2. Concatena cada línea terminada con la barra invertida (\) con la siguiente.
3. Elimina los comentarios. Divide cada línea lógica en símbolos de preprocesado y espacios en blanco.
4. Ejecuta las directivas de preprocesado y expande los macros.
5. Reemplaza las secuencias de escape dentro de constantes de caracteres y cadenas de literales por sus caracteres individuales equivalentes.
6. Concatena cadenas de literales adyacentes.
7. Convierte los símbolos de preprocesado en símbolos de C++ para formar una unidad de compilación.

Estas fases se ejecutan exactamente en este orden.

Directivas del preprocesador

#define ID VAL Define la macro ID con valor VAL
#include Incluye un fichero del directorio actual
"fichero"
#include Incluye un fichero del directorio por defecto

#defined id Devuelve 1 si id está definido
#defined (id) Lo mismo que el anterior
#if expr Si la expresión se cumple se compila todo lo que sigue. Si no se pasa hasta un #else o un #endif
#ifdef id Si el macro id ha sido definido con un #define la condición se cumple y ocurre lo del caso anterior. Es equivalente a if defined id
#ifndef id Si el macro id no ha sido definido con un #define, la condición se cumple. es equivalente a if !defined id
#else Si el #if, #ifdef o #ifndef más reciente se ha cumplido todo lo que haya después del #else hasta #endif no se compila. Si no se ha cumplido si se compila
#elif expr Contracción de #else if expr
#endif Termina una condición
#line CONST ID Cambia el número de línea según la constante CONST y el nombre del fichero de salida de errorea a ID. Modifica el valor de los macros predefinidos __LINE__ y __FILE__
#pragma OPCION Especifica al compilador opciones especificas de la implementación
#error CADENA Causa la generación de un mensaje de error con la cadena dada

Ejemplo de macros:

#define MIN(a,b) (((a) < (b)) ? (a) : (b) )
main () {
int i, j=6, k=8;
i = MIN(j*3, k-1);
}

Después del preprocesado tendremos:

main () {
int i, j=6, k=8;
i = (((j*3) < (k-1)) ? (j*3) : (k-1));
}

Si no hubiéramos puesto paréntesis en la definición de la macro, al sustituir a y b podíamos haber introducido operadores con mayor precedencia que ?: y haber obtenido un resultado erróneo al ejecutar la macro. Notar que la macro no hace ninguna comprobación en los parámetros simplemente sustituye, por lo que a veces puede producir resultados erróneos.

Macros predefinidos

Existe un conjunto de macros predefinidos que contienen cierta información actualizada durante el preprocesado:

__LINE__ Constante decimal con el número de línea del fichero fuente donde se utiliza
__FILE__ Constante literal con el nombre del fichero fuente que se está compilando
__DATE__ Constante literal con la fecha de la compilación. Es de la forma "Mmm dd yyy"
__TIME__ Constante literal con la hora de compilación. Es de la forma "hh:mm:ss"
__cplusplus Está definida cuando compilamos un programa en C++. En C no.

Secuencias trigrafo

Se definen en el estándar para poder escribir programas C/C++ en máquinas con código de caracteres reducido (7 bits). Consiste en reemplazar caracteres normales por secuencias de 3 caracteres que representan lo mismo. Las equivalencias son:

??= #
??/ /
??' ~
??( {
??) }
??! |

Prácticamente no se usan.

Funciones inline

Como ya hemos visto, las macros permiten que resumamos una serie de operaciones con una sola palabra, pero cuando las usamos como funciones corremos el peligro de pasar parámetros erróneos. Por este motivo el C++ introdujo el concepto de función inline. Una función inline es igual que una función normal (excepto que su declaración viene precedida por la palabra inline), que no genera código de llamada a función, sino que sustituye las llamadas a la misma por el código de su definición. La principal ventaja frente a las macros es que estas funciones si que comprueban el tipo de los parámetros.
No se pueden definir funciones inline recursivas (evidentemente, no podemos sustituir infinitas veces su código).

Inclusión de rutinas en ensamblador

Para hacer más eficientes determinadas partes del código podemos incluir rutinas en ensamblador dentro de nuestro código C++ usando la palabra asm . La sintaxis es:

asm { ... }

Dentro de las llaves escribimos código ensamblador de la máquina destino. El uso de esta facilidad debe estudiarse en el manual del compilador que utilicemos.

Eficiencia y claridad de los programas

Utilizando macros, funciones inline, rutinas en ensamblador o declarando variables de tipo register conseguimos una serie de optimizaciones sobre el código. El empleo de estas facilidades debe realizarse siempre que tenga sentido, es decir, mejore la velocidad de nuestro programa.
De todas formas, utilizando muchas técnicas de programación podemos mejorar programas en cualquier lenguaje de alto nivel (mejora de bucles sacando variables, eliminación de la recursividad en procedimientos recursivos, etc.).
Algunas de estas optimizaciones las pueden hacer los compiladores (aunque mejor no fiarse), y es recomendable aplicarlas sólo cuando vayamos a compilar la versión definitiva de un programa, ya que consumen mucho tiempo.
También es recomendable que las optimizaciones que realicemos en alto nivel sean claras, es decir, que el resultado de la optimización no haga el programa ilegible. Si esto no es posible no hay ningún problema siempre y cuando el código este comentado y sepamos de donde viene (como era el algoritmo antes de optimizar). Lo mejor es escribir los programas dos veces, una vez con algoritmos simples (que suelen ser poco eficientes pero claros) y después reescribirlo optimizando los algoritmos más cruciales, manteniendo la primera versión como documentación. Si escribís unos programas muy eficientes pero no los documentáis es fácil que se llegue ha hacer imposible su modificación o corrección por parte de otros (o de vosotros mismos después de un tiempo, y si no echarles un vistazo a vuestros programas de fundamentos).

CLASES

Introducción

Comenzamos en este punto con las características específicas del C++ que realmente lo hacen merecedor de ese postincremento que lo diferencia del C: las clases.
La idea de clase junto con la sobrecarga de operadores, que estudiaremos más adelante, permite al usuario diseñar tipos de datos que se comporten de manera similar a los tipos estándar del lenguaje. Esto significa que debemos poder declararlos y usarlos en diversas operaciones como si fueran tipos elementales, siempre y cuando esto sea necesario. La idea central es que los tipos del usuario sólo se diferencian de los elementales en la forma de crearlos, no en la de usarlos.
Pero las clases no sólo nos permiten crear tipos de datos, sino que nos dan la posibilidad de definir tipos de datos abstractos y definir sobre estos nuevas operaciones sin relación con los operadores estándar del lenguaje. Introducimos un nuevo nivel de abstracción y comenzamos a ver los tipos como representaciones de ideas o conceptos que no necesariamente tienen que tener una contrapartida a nivel matemático, como sucedía hasta ahora, sino que pueden ser conceptos relacionados con el mundo de nuestro programa. Así, podremos definir un tipo coche y un tipo motor en un programa de mecánica o un tipo unidad funcional en un programa de diseño de arquitectura de computadores, etcétera. Sobre estos tipos definiremos una serie de operaciones que definirán la interacción de las variables (objetos) de estos tipos con el resto de entidades de nuestro programa.
Otra idea fundamental a la hora de trabajar con clases es la distinción clara entre la definición o interface de nuestros tipos de datos y su implementación, esto es, la distinción entre qué hace y cómo lo hace. Una buena definición debe permitir el cambio de la estructura interna de nuestras clases sin que esto afecte al resto de nuestro programa. Deberemos definir los tipos de manera que el acceso a la información y operaciones que manejan sea sencillo, pero el acceso a las estructuras y algoritmos utilizados en la implementación sea restringido, de manera que una modificación en estos últimos sólo sea percibido en la parte de nuestro programa que implementa la clase.
Por último es interesante tener presentes las ideas ya mencionadas en el bloque anterior de objeto y paso de mensajes entre objetos. Estas ideas pueden resultar de gran ayuda a la hora del diseño e implementación de programas, ya que podemos basarnos en el comportamiento de los objetos de interés en nuestro programa para abstraer clases y métodos (mensajes).

Clases y miembros

Comenzaremos el estudio de las clases partiendo de la siguiente idea: una clase es un tipo de datos que se define mediante una serie de miembros que representan atributos y operaciones sobre los objetos de ese tipo. Hasta ahora conocemos la forma de definir un tipo de datos por los atributos que posee, lo hacemos mediante el uso de las estructuras. Pensemos por ejemplo en como definimos un tipo de datos empleado en un programa de gestión de personal:

struct empleado {
char * nombre;
long DNI;
float sueldo;
...
};

Con esta definición conseguimos que todas las características que nos interesan del empleado se puedan almacenar conjuntamente, pero nos vemos obligados a definir funciones que tomen como parámetro variables de tipo empleado para trabajar con estos datos:

void modificar_sueldo (empleado *e, float nuevo_sueldo);
...

Pero el C++ nos da una nueva posibilidad, incluir esas funciones como miembros del tipo empleado:

struct empleado {
char * nombre;
long DNI;
float sueldo;
...
void modificar_sueldo (float nuevo_sueldo);
...
};

A estas funciones se les denomina miembros función o métodos, y tienen la peculiaridad de que sólo se pueden utilizar junto con variables del tipo definido. Es interesante señalar, aunque sea anticipar acontecimientos, que la función miembro no necesita que se le pase la estructura como parámetro, ya que al estar definida dentro de ella tiene acceso a los datos que contiene.
Como distintas clases pueden emplear el mismo nombre para los miembros, a la hora de definir las funciones miembro debemos especificar el nombre de la estructura a la que pertenecen:

void empleado::modificar_sueldo (float nuevo_sueldo) {
sueldo = nuevo_sueldo;
};

Si definimos la función dentro de la estructura esto último no es necesario, ya que no hay lugar para la confusión.

Acceso a miembros: la palabra class

Hasta ahora hemos empleado la palabra struct para definir las clases, este uso es correcto, pero tiene una connotación específica: todos los miembros del tipo son accesibles desde el exterior del tipo, es decir, podemos modificar los datos o invocar a las funciones del mismo desde el exterior de la definición:

empleado pepe; // declaramos un objeto de tipo empleado
pepe.sueldo = 500; // asignamos el valor 500 al campo sueldo
pepe.modificar_sueldo(600) // le decimos a pepe que cambie su sueldo a 600

En el caso del ejemplo puede parecer poco importante que se pueda acceder a los datos del tipo, pero hemos dicho que lo que nos interesa es que la forma de representar los datos o de implementar los algoritmos sólo debe ser vista en la definición de la clase. Para que lo que contiene la clase sólo sea accesible desde la definición empleamos la palabra class en lugar de struct para definir el tipo:

class empleado {
...
}

Acceso a miembros: etiquetas de control de acceso

El uso de class da lugar a un problema ¿cómo accedemos a los miembros de la clase que si deben ser vistos desde fuera? La solución está en emplear la etiqueta public delante de los miembros que queremos que sean accesibles:

class empleado {
char * nombre;
long DNI;
float sueldo;
...
public:
void modificar_sueldo (float nuevo_sueldo);
...
} pepe;
pepe.sueldo = 500; // ERROR, sueldo es un miembro privado
pepe.modificar_sueldo (600); // CORRECTO, modificar_sueldo() es un método público

Además de public también podemos emplear las etiquetas protected y private dentro de la declaración de la clase. Todo lo que aparezca después de una etiqueta será accesible (o inaccesible) hasta que encontremos otra etiqueta que cambie la accesibilidad o inaccesibilidad. La etiqueta protected tiene una utilidad especial que veremos cuando hablemos de herencia, de momento la usaremos de la misma forma que private , es decir, los miembros declarados después de la etiqueta serán inaccesibles desde fuera de la clase.
Utilizando las etiquetas podemos emplear indistintamente la palabra struct o class para definir clases, la única diferencia es que si no ponemos nada con struct se asume acceso público y con class se asume acceso privado (con el sentido de la etiqueta private , no protected ). Mi consejo es usar siempre la palabra class y especificar siempre las etiquetas de permiso de acceso, aunque podamos tener en cuenta el hecho de que por defecto el acceso es privado es más claro especificarlo.
Hemos de indicar que también se puede definir una clase como union , que implica acceso público pero sólo permite el acceso a un miembro cada vez (es lo mismo que sucedía con las uniones como tipo de datos compuesto).

Operadores de acceso a miembros

El acceso a los miembros de una clase tiene la misma sintaxis que para estructuras (el operador . y el operador -> ), aunque también se emplea muy a menudo el operador de campo ( :: ) para acceder a los miembros de la clase. Por ejemplo se emplea el operador de campo para distinguir entre variables de un método y miembros de la clase:

class empleado {
...
float sueldo;
...
public:
void modificar_sueldo (float sueldo) {
empleado::sueldo = sueldo;
}
...
};

El puntero this

En uno de los puntos anteriores comentábamos que un método perteneciente a una clase tenía acceso a los miembros de su propia clase sin necesidad de pasar como parámetro el objeto con el que se estaba trabajando. Esto no es tan sencillo, puesto que es lógico pensar que los atributos (datos) contenidos en la clase son diferentes para cada objeto de la clase, es decir, se reserva memoria para los miembros de datos, pero no es lógico que cada objeto ocupe memoria con una copia de los métodos, ya que replicaríamos mucho código.
En realidad, los objetos de una clase tienen un atributo específico asociado, su dirección. La dirección del objeto nos permitirá saber que variables debemos modificar cuando accedemos a un miembro de datos. Esta dirección se pasa como parámetro (implícito) a todas las funciones miembro de la clase y se llama this .
Si en alguna función miembro queremos utilizar nuestra propia dirección podemos utilizar el puntero como si lo hubiéramos recibido como parámetro. Por ejemplo, para retornar el valor de

un atributo escribimos:

float empleado::cuanto_cobra (void) {
return sueldo;
}

Pero también podríamos haber hecho lo siguiente:

float empleado::cuanto_cobra (void) {
return this->sueldo;
}

Utilizar el puntero dentro de una clase suele ser redundante, aunque a veces es útil cuando trabajamos con punteros directamente.

Funciones miembro constantes

Un método de una clase se puede declarar de forma que nos impida modificar el contenido del objeto (es decir, como si para la función el parámetro this fuera constante). Para hacer esto basta escribir la palabra después de la declaración de la función:

class empleado {
...
float cuanto_cobra (void) const;
...
};
float empleado::cuanto_cobra (void) const
{
return sueldo;
}

Las funciones miembro constantes se pueden utilizar con objetos constantes, mientras que las que no lo son no pueden ser utilizadas (ya que podrían modificar el objeto).

De cualquier forma, existen maneras de modificar un objeto desde un método constante: el empleo de cast sobre el parámetro this o el uso de miembros puntero a datos no constantes. Veamos un ejemplo para el primer caso:

class empleado {
private:
...
long num_accesos_a_empleado;
...
public:
...
float cuanto_cobra (void) const
...
};
float empleado::cuanto_cobra (void) const
{
((empleado *)this)->num_accesos_a_empleado += 1; // hemos accedido una vez más a la clase empleado return sueldo;
}

Y ahora un ejemplo del segundo:

struct contabilidad {
long num_accesos_a_clase;
};
class empleado {
private:
...
contabilidad *conta;
...
public:
...
float cuanto_cobra (void) const
...
};
float empleado::cuanto_cobra (void) const
{
conta->num_accesos_a_clase += 1; // hemos accedido una vez más a la clase empleado
return sueldo;
}

Esta posibilidad de modificar objetos desde métodos constantes se permite en el lenguaje por una cuestión conceptual: un método constante no debe modificar los objetos desde el punto de vista del usuario, y declarándolo como tal el usuario lo sabe, pero por otro lado, puede ser interesante que algo que, para el que llama a una función miembro, no modifica al objeto si lo haga realmente con variables internas (no visibles para el usuario) para llevar contabilidades o modificar estados. Esto es especialmente útil cuando declaramos objetos constantes de una clase, ya que podemos modificar variables mediante funciones constantes.

Funciones miembro inline

Al igual que se podían declarar funciones de tipo inline generales, también se pueden definir funciones miembro inline . La idea es la misma, que no se genere llamada a función.
Para hacer esto en C++ existen dos posibilidades: definir la función en la declaración de la clase (por defecto implica que la función miembro es inline ) o definir la función fuera de la clase precedida de la palabra inline :

inline float empleado::cuanto_cobra {
return sueldo;
}

Lo único que hay que indicar es que no podemos definir la misma función inline dos veces (en dos ficheros diferentes).

Atributos estáticos

Cuando en la declaración de una clase ponemos atributos (datos) estáticos, queremos indicar que ese atributo es compartido por todos los objetos de la clase. Para declararlo estático sólo hay que escribir la palabra static antes de su declaración:

class empleado {
...
static long num_total_empleados;
...
};

Con esto conseguimos que el atributo tenga características de variable global para los miembros de la clase, pero que permanezca en el ámbito de la misma. Hay que tener presente que los atributos estáticos ocupan memoria aunque no declaremos ningún objeto.
Si un atributo se declara público para acceder a él desde el exterior de la clase debemos identificarlo con el operador de campo:

empleado::num_total_empleados = 1000;

El acceso desde los miembros de la clase es igual que siempre.
Los atributos estáticos se deben definir fuera del ámbito de la clase, aunque al hacerlo no se debe poner la palabra static (podrían producirse conflictos con el empleo de static para variables y funciones globales). Si no se inicializan en su definición toman valor 0:

long empleado::num_total_empleados; // definición, toma valor 0

El uso de atributos estáticos es más recomendable que el de las variables globales.

Tipos anidados

Dentro de las clases podemos definir nuevos tipos (enumerados, estructuras, clases, ...), pero para utilizarlos tendremos las mismas restricciones que para usar los miembros, es decir, serán accesibles según el tipo de acceso en el que se encuentren y para declarar variables de esos tipos tendremos que emplear la clase y el operador de campo:

class lista {
private:
struct nodo {
int val;
nodo *sig;
};
nodo *primero;
public:
enum tipo_lista { FIFO, LIFO };
void inserta (int);
int siguiente ();
...
};
nodo n1; // ERROR, nodo no es un tipo definido, está en otro ámbito
tipo_lista tl1; // ERROR, tipo_lista no definido
lista::nodo n2; // ERROR, tipo nodo privado
lista::tipo_lista tl2; // CORRECTO

Punteros a miembros

Cuando dimos la lista de operadores de indirección mencionamos dos de ellos que aún no se han visto: el operador de puntero selector de puntero a miembro ( ->* ) y el operador selector de puntero a miembro ( .* ). Estos operadores están directamente relacionados con los punteros a miembros de una clase (como sus nombres indican). Suele ser especialmente interesante tomar la dirección de los métodos por la misma razón que era interesante tomar la dirección de funciones, aunque en clases se utiliza más a menudo (de hecho las llamadas a métodos de una clase hacen uso de punteros a funciones, aunque sea implícitamente).
Para tomar la dirección de un miembro de la clase X escribimos &X::miembro . Una variable del tipo puntero a miembro de la clase X se obtiene declarándolo de la forma X::* .

Por ejemplo si tenemos la clase:

class empleado {
...
void imprime_sueldo (void);
...
};

Podemos definir una variable que apunte a un método de la clase que retorna void y no tiene parámetros:

void (empleado::*ptr_a_metodo) (void);

o usando typedef :

typedef void (empleado::*PMVV) (void);
PMVV ptr_a_metodo;
Para usar la variable podemos hacer varias cosas:

empleado e; empleado *pe; PMVV ptr_a_metodo = &empleado::imprime_sueldo; e.imprime_sueldo(); // llamada normal (e.*ptr_a_metodo)(); // acceso a miembro apuntado por puntero a través de un objeto (pe->*ptr_a_metodo)(); // acceso a miembro apuntado por puntero a través de un puntero a objeto

En el ejemplo se usan paréntesis porque .* y ->* tienen menos precedencia que el operador de función.
En realidad el uso de estos punteros es poco usual, ya que se puede evitar usando funciones virtuales (que se estudiarán más adelante).

Métodos estáticos y funciones amigas

Dentro de las peculiaridades de las clases encontramos dos tipos de funciones especiales: los métodos estáticos y las funciones amigas. Los comentamos separados del bloque relativo a clases y miembros por su similitud y por la importancia de las funciones amigas en la sobrecarga de operadores.
Su característica común es que no poseen parámetro implícito this . Aunque las expliquemos juntas sus aplicaciones son muy diferentes.
Métodos estáticos

Al igual que los atributos estáticos mencionados en el punto anterior, las funciones miembro estáticas son globales para los miembros de la clase y deben ser definidas fuera del ámbito de la declaración de la clase.
Estos métodos son siempre públicos, se declaren donde se declaren. Al no tener parámetro this no pueden acceder a los miembros no estáticos de la clase (al menos directamente, ya que se le podría pasar un puntero al objeto para que modificara lo que fuera).

Funciones amigas (friend)

Son funciones que tienen acceso a los miembros privados de una clase sin ser miembros de la misma. Se emplean para evitar la ineficiencia que supone el tener que acceder a los miembros privados de una clase a través de métodos.
Como son funciones independientes de la clase no tienen parámetro this , por lo que el acceso a objetos de una clase se consigue pasándoles como parámetro una referencia al objeto (una referencia como tipo implica pasar el objeto sin copiar, aunque se trata como si fuera el objeto y no un puntero), un puntero o el mismo objeto. Por la misma razón, no tienen limitación de acceso, ya que se definen fuera de la clase.
Para hacer amiga de una clase a una función debemos declararla dentro de la declaración de la clase precedida de la palabra friend :

class X {
private:
int i;
...
friend int f(X&, int); // función amiga que toma como parámetros una referencia a un objeto del tipo X y un entero y retorna un entero
}

En la definición de la función (que se hace fuera de la clase como las funciones normales) podremos usar y modificar los miembros privados de la clase amiga sin ningún problema:

int f(X& objeto, int i) {
int j = objeto.i;
objeto.i = i;
return j;
}

Es importante ver que aunque las funciones amigas no pertenecen a la clase se declaran explícitamente en la misma, por lo que forman parte de la interface de la clase.
Una función miembro de una clase puede ser amiga de otra:

class X {
...
void f();
...
};
class Y {
...
friend void X::f();
};

Si queremos que todas las funciones de una clase sean amigas de una clase podemos poner:

class X {
friend class Y;
...
};

En el ejemplo todas las funciones de la clase Y son amigas de la clase X , es decir, todos los métodos de Y tienen acceso a los miembros privados deX .
Construcción y destrucción

Hasta ahora hemos hablado de la declaración y definición de clases, pero hemos utilizado los objetos sin saber como se crean o se destruyen. En este punto veremos como las clases se crean y destruyen de distintas maneras y que podemos controlar que cosas se hacen al crear o destruir un objeto.

Creación de objetos

Podemos clasificar los objetos en cuatro tipos diferentes según la forma en que se crean:

1. Objetos automáticos: son los que se crean al encontrar la declaración del objeto y se destruyen al salir del ámbito en que se declaran.
2. Objetos estáticos: se crean al empezar la ejecución del programa y se destruyen al terminar la ejecución.
3. Objetos dinámicos: son los que se crean empleando el operador new y se destruyen con el operador delete .
4. Objetos miembro: se crean como miembros de otra clase o como un elemento de un array.

Los objetos también se pueden crear con el uso explícito del constructor (lo vemos en seguida) o como objetos temporales. En ambos casos son objetos automáticos.
Hay que notar que estos modelos de creación de objetos también es aplicable a las variables de los tipos estándar del C++, aunque no tenemos tanto control sobre ellos.

Inicialización y limpieza de objetos

Con lo que sabemos hasta ahora sería lógico pensar que si deseamos inicializar un objeto de una clase debemos definir una función que tome como parámetros los valores que nos interesan para la inicialización y llamar a esa función nada más declarar la función. De igual forma, nos interesará tener una función de limpieza de memoria si nuestro objeto utiliza memoria dinámica, que deberíamos llamar antes de la destrucción del objeto.
Bien, esto se puede hacer así, explícitamente, con funciones definidas por nosotros, pero las llamadas a esos métodos de inicialización y limpieza pueden resultar pesadas y hasta difíciles de localizar en el caso de la limpieza de memoria.
Para evitar el tener que llamar a nuestras funciones el C++ define dos funciones especiales para todas las clases: los métodos constructor y destructor. La función constructor se invoca automáticamente cuando creamos un objeto y la destructor cuando lo destruimos. Nosotros podemos implementar o no estas funciones, pero es importante saber que si no lo hacemos el C++ utiliza un constructor y destructor por defecto.
Estos métodos tienen una serie de características comunes muy importantes:

— No retornan ningún valor, ni siquiera de tipo void . Por lo tanto cuando las declaramos no debemos poner ningún tipo de retorno.
— Como ya hemos dicho, si no se definen se utilizan los métodos por defecto.
— No pueden ser declarados constantes, volátiles ni estáticos.
— No se puede tomar su dirección.
— Un objeto con constructores o destructores no puede ser miembro de una unión.
— El orden de ejecución de constructores y destructores es inverso, es decir, los objetos que se construyen primero se destruyen los últimos. Ya veremos que esto es especialmente importante al trabajar con la herencia.

Los constructores

Los constructores se pueden considerar como funciones de inicialización, y como tales pueden tomar cualquier tipo de parámetros, incluso por defecto. Los constructores se pueden sobrecargar, por lo que podemos tener muchos constructores para una misma clase (como ya sabemos, cada constructor debe tomar parámetros distintos).
Existe un constructor especial que podemos definir o no definir que tiene una función muy específica: copiar atributos entre objetos de una misma clase. Si no lo definimos se usa uno por defecto que copia todos los atributos de los objetos, pero si lo definimos se usa el nuestro.
Este constructor se usa cuando inicializamos un objeto por asignación de otro objeto.
Para declarar un constructor lo único que hay que hacer es declarar una función miembro sin tipo de retorno y con el mismo nombre que la clase, como ya hemos dicho los parámetros pueden ser los que queramos:

class Complejo {
private:
float re;
float im;
public:
Complejo (float = 0, float = 0); // constructor con dos parámetros por defecto Lo podremos usar con 0, 1, o 2 parámetros.
Complejo (&Complejo); // constructor copia
...
};
// Definición de los constructores // Inicialización
Complejo::Complejo (float pr, float pi) { re = pr; im = pi; } // Constructor copia
Complejo::Complejo (Complejo& c) { re= c.re; im= c.im; }

Los constructores se suelen declarar públicos, pero si todos los constructores de una clase son privados sólo podremos crear objetos de esa clase utilizando funciones amigas. A las clases que sólo tienen constructores privados se las suele denominar privadas.
Los constructores se pueden declarar virtuales (ya lo veremos).

El destructor

Para cada clase sólo se puede definir un destructor, ya que el destructor no puede recibir parámetros y por tanto no se puede sobrecargar. Ya hemos dicho que los destructores no pueden ser constantes, volátiles ni estáticos, pero si pueden ser declarados virtuales (ya veremos más adelante que quiere decir esto).
Para declarar un destructor escribimos dentro de la declaración de la clase el símbolo ~ seguido del nombre de la clase. Se emplea el símbolo ~ para indicar que el destructor es el complemento del constructor.
Veamos un ejemplo:

class X {
private:
int *ptr;
public:
X(int =1); // constructor
~X(); // destructor
};
// declaración del constructor
X::X(int i){
ptr = new int [i];
}


// declaración del destructor
X::~X() {
delete []ptr;
}

Variables locales
El constructor de una variable local se ejecuta cada vez que encontramos la declaración de la variable local y su destructor se ejecuta cuando salimos del ámbito de la variable. Para ejecutar un constructor distinto del constructor por defecto al declarar una variable hacemos:

Complejo c (1, -1); // Crea el complejo c llamando al constructor. Complejo (float, float)

y para emplear el constructor copia para inicializar un objeto hacemos:

Complejo d = c; // crea el objeto d usando el constructor copia. Complejo(Complejo&)

Si definimos c y luego d, al salir del bloque de la variable primero llamaremos al destructor de d, y luego al de c.

Almacenamiento estático

Cuando declaramos objetos de tipo estático su constructor se invoca al arrancar el programa y su destructor al terminar. Un ejemplo de esto esta en los objetos cin , cout y cerr . Estos objetos se crean al arrancar el programa y se destruyen al acabar. Como siempre, constructores y destructores se ejecutan en orden inverso.
El único problema con los objetos estáticos está en el uso de la función exit() . Cuando llamamos a exit() se ejecutan los destructores de los objetos estáticos, luego usar exit() en uno de ellos provocaría una recursión infinita. Si terminamos un programa con la función abort() los destructores no se llaman.

Almacenamiento dinámico

Cuando creamos objetos dinámicos con new ejecutamos el constructor utilizado para el objeto. Para liberar la memoria ocupada debemos emplear el operador delete , que se encargará de llamar al destructor. Si no usamos delete no tenemos ninguna garantía de que se llame al destructor del objeto.
Para crear un objeto con new ponemos:

Complejo *c= new Complejo (1); // Creamos un complejo llamando al constructor con un parámetro.

y para destruirlo:

delete c;

El usuario puede redefinir los operadores new y delete y puede modificar la forma de interacción de los constructores y destructores con estos operadores. Veremos todo esto al hablar de sobrecarga de operadores. La creación de arrays de objetos se discute más adelante.

Objetos como miembros

Cuando definimos una clase podemos emplear objetos como miembros, pero lo que no sabemos es como se construyen estos objetos miembro. Si no hacemos nada los objetos se construyen llamando a su constructor por defecto (aquel que no toma parámetros). Esto no es ningún problema, pero puede ser interesante construir los objetos miembro con parámetros del constructor del objeto de la clase que los define. Para hacer esto lo único que tenemos que hacer es poner en la definición del constructor los constructores de objetos miembro que queramos invocar. La sintaxis es poner dos puntos después del prototipo de la función constructora (en la definición, es decir, cuando implementamos la función) seguidos de una lista de constructores (invocados con el nombre del objeto, no el de la clase) separados por comas. El cuerpo de la definición de la función se pone después.
Estos constructores se llamaran en el orden en el que los pongamos y antes de ejecutar el constructor de la clase que los invoca.
Veamos un ejemplo:

class cjto_de_tablas {
private:
tabla elementos; // objeto de clase tabla
tabla componentes; // objeto de clase tabla
int tam_tablas;
...
public:
cjto_de_tablas (int tam); // constructor
~cjto_de_tablas (); // destructor
...
};
cjto_de_tablas::cjto_de_tablas (int tam)
:elementos (tam), componentes(tam), tam_tablas(tam)
{
... // Cuerpo del constructor
}

Como se ve en el ejemplo podemos invocar incluso a los constructores de los objetos de tipos estándar. Si en el ejemplo no inicializáramos componentes el objeto se crearía invocando al constructor por defecto (el que no tiene parámetros, que puede ser un constructor nuestro con parámetros por defecto).
Este método es mejor que emplear punteros a objetos y construirlos con new en el constructor y liberarlos con delete en el destructor, ya que el uso de objetos dinámicos consume más memoria que los objetos estáticos (ya que usan un puntero y precisan llamadas al sistema para reservar y liberar memoria). Si dentro de una clase necesitamos miembros objeto pero no necesitamos que sean dinámicos emplearemos objetos miembro con la inicialización en el constructor de la clase.

Arrays de objetos

Para declarar un array de objetos de una clase determinada es imprescindible que la clase tenga un constructor por defecto (que como ya hemos dicho es uno que no recibe parámetros pero puede tener parámetros por defecto).
Al declarar el array se crearan tantos objetos como indiquen los índices llamando al constructor por defecto. La destrucción de los elementos para arrays estáticos se realiza por defecto al salir del bloque de la declaración (igual que con cualquier tipo de objetos estáticos), pero cuando creamos un array dinámicamente se siguen las mismas reglas explicadas al hablar de new y delete , es decir, debemos llamara a delete indicándole que queremos liberar un array.
Veamos varios ejemplos:

tabla at[20]; // array de 20 tablas, se llama a los constructores por defecto
void f(int tam) {
tabla *t1 = new tabla; // puntero a un elemento de tipo tabla
tabla *t2 = new tabla [tam]; // puntero a un array de 'tam' tablas
...
delete t1; // destrucción de un objeto
detele []t2; // destrucción de un array
}