13 Operadores II: Más operadores

Veremos ahora más detalladamente algunos operadores que ya hemos mencionado, y algunos nuevos.

Operadores de Referencia (&) e Indirección (*)

El operador de referencia (&) nos devuelve la dirección de memoria del operando.

Sintaxis:

&<expresión simple>

Por ejemplo:

int *punt;
int x = 10;

punt = &x;

El operador de indirección (*) considera a su operando como una dirección y devuelve su contenido.

Sintaxis:

*<puntero>

Ejemplo:

int *punt;
int x;

x = *punt;

Operadores . y ->

Operador de selección (.). Permite acceder a objetos o campos dentro de una estructura.

Sintaxis:

<variable_estructura>.<nombre_de_variable>

Operador de selección de objetos o campos para estructuras referenciadas con punteros. (->)

Sintaxis:

<puntero_a_estructura>-><nombre_de_variable>

Ejemplo:

struct punto {
   int x;
   int y;
};

punto p1;
punto *p2;

p1.x = 10;
p1.y = 20;
p2->x = 30;
p2->y = 40;

Operador de preprocesador

El operador "#" sirve para dar órdenes o directivas al compilador. La mayor parte de las directivas del preprocesador se verán en capítulos posteriores.

El preprocesador es un programa auxiliar, que forma parte del compilador, y que procesa el fichero fuente antes de que sea compilado. En realidad se limita a seguir las órdenes expresadas en forma de directivas del preprocesador, modificando el programa fuente antes de que sea compilado.

Veremos, sin embargo dos de las más usadas.

Directiva define

La directiva define, sirve para definir macros. Cada macro es una especie de fórmula para la sustitución de texto dentro del fichero fuente, y puede usar, opcionalmente parámetros.

Sintaxis:

#define <identificador_de_macro> <secuencia>

El preprocesador sustituirá cada ocurrencia del <identificador_de_macro> en el fichero fuente, por la <secuencia>, (aunque con algunas excepciones). Cada sustitución se denomina "expansión de la macro", y la secuencia se suele conocer como "cuerpo de la macro".

Si la secuencia no existe, el <identificador_de_macro> será eliminado cada vez que aparezca en el fichero fuente.

Después de cada expansión individual, se vuelve a examinar el texto expandido a la búsqueda de nuevas macros, que serán expandidas a su vez. Esto permite la posibilidad de hacer macros anidadas. Si la nueva expansión tiene la forma de una directiva de preprocesador, no será reconocida como tal.

Existen otras restricciones a la expansión de macros:

  • Las ocurrencias de macros dentro de literales, cadenas, constantes alfanuméricas o comentarios no serán expandidas.
  • Una macro no será expandida durante su propia expansión, así #define A A, no será expandida indefinidamente.

Ejemplo:

#define suma(a,b) ((a)+(b))

Los paréntesis en el cuerpo de la macro son necesarios para que funcione correctamente en todos los casos, lo veremos mucho mejor con otro ejemplo:

#include <iostream>
using namespace std;

#define mult1(a,b) a*b
#define mult2(a,b) ((a)*(b))

int main() {
   // En este caso ambas macros funcionan bien: (1)
   cout << mult1(4,5) << endl;
   cout << mult2(4,5) << endl;
   // En este caso la primera macro no funciona, ¿por qué?: (2)
   cout << mult1(2+2,2+3) << endl;
   cout << mult2(2+2,2+3) << endl;

   return 0;
}

¿Por qué falla la macro mult1 en el segundo caso? Para averiguarlo, veamos cómo trabaja el preprocesador.

Cuando el preprocesador encuentra una macro la expande, el código expandido sería:

int main() {
   // En este caso ambas macros funcionan bien:
   cout << 4*5 << endl;
   cout << ((4)*(5)) << endl;
   // En este caso la primera macro no funciona, ¿por qué?:
   cout << 2+2*2+3 << endl;
   cout << ((2+2)*(2+3)) << endl;

   return 0;
}

Al evaluar "2+2*2+3" se asocian los operandos dos a dos de izquierda a derecha, pero la multiplicación tiene prioridad sobre la suma, así que el compilador resuelve 2+4+3 = 9. Al evaluar "((2+2)*(2+3))" los paréntesis rompen la prioridad de la multiplicación, el compilador resuelve 4*5 = 20.

Directiva include

La directiva include, como ya hemos visto, sirve para insertar ficheros externos dentro de nuestro fichero de código fuente. Estos ficheros son conocidos como ficheros incluidos, ficheros de cabecera o "headers".

Sintaxis:

#include <nombre de fichero cabecera>
#include "nombre de fichero de cabecera"
#include identificador_de_macro 

El preprocesador elimina la línea include y la sustituye por el fichero especificado. El tercer caso halla el nombre del fichero como resultado de aplicar la macro.

La diferencia entre escribir el nombre del fichero entre "<>" o """", está en el algoritmo usado para encontrar los ficheros a incluir. En el primer caso el preprocesador buscará en los directorios "include" definidos en el compilador. En el segundo, se buscará primero en el directorio actual, es decir, en el que se encuentre el fichero fuente, si el fichero no existe en ese directorio, se trabajará como el primer caso. Si se proporciona el camino como parte del nombre de fichero, sólo se buscará en ese directorio.

El tercer caso es "raro", no he encontrado ningún ejemplo que lo use, y yo no he recurrido nunca a él. Pero el caso es que se puede usar, por ejemplo:

#define FICHERO "trabajo.h"

#include FICHERO

int main()
{
...
}

Es un ejemplo simple, pero en el capítulo 23 veremos más directivas del preprocesador, y verás el modo en que se puede definir FICHERO de forma condicional, de modo que el fichero a incluir puede depender de variables de entorno, de la plataforma, etc.

Por supuesto la macro puede ser una fórmula, y el nombre del fichero puede crearse usando esa fórmula.

Operadores de manejo de memoria new y delete

Veremos su uso en el capítulo de punteros II y en mayor profundidad en el capítulo de clases y en operadores sobrecargados.

Operador new

El operador new sirve para reservar memoria dinámica.

Sintaxis:

[::]new [<emplazamiento>] <tipo> [(<inicialización>)]
[::]new [<emplazamiento>] (<tipo>) [(<inicialización>)]
[::]new [<emplazamiento>] <tipo>[<número_elementos>]
[::]new [<emplazamiento>] (<tipo>)[<número_elementos>] 

El operador opcional :: está relacionado con la sobrecarga de operadores, de momento no lo usaremos. Lo mismo se aplica a emplazamiento.

La inicialización, si aparece, se usará para asignar valores iniciales a la memoria reservada con new, pero no puede ser usada con arrays.

Las formas tercera y cuarta se usan para reservar memoria para arrays dinámicos. La memoria reservada con new será válida hasta que se libere con delete o hasta el fin del programa, aunque es aconsejable liberar siempre la memoria reservada con new usando delete. Se considera una práctica muy sospechosa no hacerlo.

Si la reserva de memoria no tuvo éxito, new devuelve un puntero nulo, NULL.

Operador delete

El operador delete se usa para liberar la memoria dinámica reservada con new.

Sintaxis:

[::]delete [<expresión>]
[::]delete[] [<expresión>] 

La expresión será normalmente un puntero, el operador delete[] se usa para liberar memoria de arrays dinámicos.

Es importante liberar siempre usando delete la memoria reservada con new. Existe el peligro de pérdida de memoria si se ignora esta regla.

Cuando se usa el operador delete con un puntero nulo, no se realiza ninguna acción. Esto permite usar el operador delete con punteros sin necesidad de preguntar si es nulo antes.

De todos modos, es buena idea asignar el valor 0 a los punteros que no han sido inicializados y a los que han sido liberados. También es bueno preguntar si un puntero es nulo antes de intentar liberar la memoria dinámica que le fue asignada.

Nota: los operadores new y delete son propios de C++. En C se usan funciones, como malloc y free para reservar y liberar memoria dinámica y liberar un puntero nulo con free suele tener consecuencias desastrosas.

Veamos algunos ejemplos:

int main() {
   char *c;
   int *i = NULL;
   float **f;
   int n;

   // Cadena de 122 más el nulo:
   c = new char[123];
   // Array de 10 punteros a float:
   f = new float *[10]; (1)
   // Cada elemento del array es un array de 10 float
   for(n = 0; n < 10; n++) f[n] = new float[10]; (2)
   // f es un array de 10*10
   f[0][0] = 10.32;
   f[9][9] = 21.39;
   c[0] = 'a';
   c[1] = 0;
   // liberar memoria dinámica
   for(n = 0; n < 10; n++) delete[] f[n];
   delete[] f;
   delete[] c;
   delete i;
   return 0;
}

Nota: f es un puntero que apunta a un puntero que a su vez apunta a un float. Un puntero puede apuntar a cualquier tipo de variable, incluidos otros punteros.
Este ejemplo nos permite crear arrays dinámicos de dos dimensiones. La línea (1) crea un array de 10 punteros a float. La (2) crea 10 arrays de floats. El comportamiento final de f es el mismo que si lo hubiéramos declarado como:
float f[10][10];

Otro ejemplo:

#include <iostream>
using namespace std;

int main() {
   int *x;

   x = new int(67);
   cout << *x << endl;
   delete x;
}

En este caso, reservamos memoria para un entero, se asigna la dirección de la memoria obtenida al puntero x, y además, se asigna el valor 67 al contenido de esa memoria.

Palabras reservadas usadas en este capítulo

delete, new.