2.2. Herramientas para compilación modular

La compilación modular es particularmente importante cuando se construyen grandes proyectos. En C y en C++, un programa se puede crear en pequeñas piezas, manejables y comprobables de forma independiente. La herramienta más importante para dividir un programa en piezas más pequeñas es la capacidad de crear subrutinas o subprogramas que tengan un nombre que las identifique. En C y en C++, estos subprogramas se llamana funciones, que son las piezas de código que se pueden almacenar en diferentes ficheros, permitiendo la compilación separada. Dicho de otra forma, una función es la unidad atómica de código, debido a que no se puede tener una parte de una función en un fichero y el resto en otro (aunque los ficheros pueden contener más de una función).

Cuando se invoca una función, se le suelen pasar una serie de argumentos, que son valores que desea que la función utilice durante su ejecución. Cuando la función termina, normalmente devuelve un valor de retorno, que equivale al resultado. También es posible crear funciones que no tengan ni argumentos ni valor de retorno.

Para crear un programa con múltiples ficheros, las funciones de un fichero deben acceder a las funciones y los datos de otros ficheros. Cuando se compila un fichero, el compilador de C o C++ debe conocer las funciones y los datos de los otros ficheros, en particular sus nombres y su uso apropiado. El compilador asegura que las funciones y los datos son usados correctamente. El proceso de "decirle al compilador" los nombres de las funciones externas y los datos que necesitan es conocido como declaración. Una vez declarada una función o una variable, el compilador sabe cómo comprobar que la función se utiliza adecuadamente.

2.2.1. Declaraciones vs definiciones

Es importante comprender la diferencia entre declaraciones y definiciones porque estos términos se usarán de forma precisa en todo el libro. Básicamente todos los programas escritos en C o en C++ requieren declaraciones. Antes de poder escribir su primer programa, necesita comprender la manera correcta de escribir una declaración.

Una declaración presenta un nombre -identificador- al compilador. Le dice al compilador «Esta función o esta variable existe en algún lugar, y éste es el aspecto que debe tener». Una definición, sin embargo, dice: «Crea esta variable aquí» o «Crea esta función aquí». Eso reserva memoria para el nombre. Este significado sirve tanto para una variable que para una función; en ambos casos, el compilador reserva espacio en el momento de la definición. Para una variable, el compilador determina su tamaño y reserva el espacio en memoria para contener los datos de la variable. Para una función, el compilador genera el código que finalmente ocupará un espacio en memoria.

Se puede declarar una variable o una función en muchos sitios diferentes, pero en C o en C++ sólo se puede definir una vez (se conoce a veces como Regla de Definición Única (ODR) [35]). Cuando el enlazador une todos los módulos objeto, normalmente se quejará si encuentra más de una definición para la misma función o variable.

Una definición puede ser también una declaración. Si el compilador no ha visto antes el nombre x y hay una definición int x;, el compilador ve el nombre también como una declaración y asigna memoria al mismo tiempo.

Sintaxis de declaración de funciones

La declaración de una función en C y en C++ consiste en escribir el nombre de la función, los tipos de argumentos que se pasan a la función, y el valor de retorno de la misma. Por ejemplo, aquí tenemos la declaración de una función llamada func1() que toma dos enteros como argumentos (en C/C++ los enteros se denotan con la palabra reservada int) y que devuelve un entero:

int func1(int, int);

La primera palabra reservada es el valor de retorno: int. Los argumentos están encerrados entre paréntesis después del nombre de la función en el orden en que se utilizan. El punto y coma indica el final de la sentencia; en este caso le dice al compilador «esto es todo - ¡aquí no está la definición de la función!».

Las declaraciones en C y C++ tratan de mimetizar la forma en que se utilizará ese elemento. Por ejemplo, si a es otro entero la función de arriba se debería usar de la siguiente manera:

a = func1(2, 3);

Como func1() devuelve un entero, el compilador de C/C++ comprobará el uso de func1() para asegurarse que a puede aceptar el valor devuelto y que los argumentos son válidos.

Los argumentos de las declaraciones de funciones pueden tener nombres. El compilador los ignora pero pueden ser útilies como nemotécnicos para el usuario. Por ejemplo, se puede declarar func1() con una apariencia diferente pero con el mismo significado:

int func1(int length, int width);

Una puntualización

Existe una diferencia significativa entre C y el C++ para las funciones con lista de argumentos vacía. En C, la declaración:

int func2();

significa «una funcion con cualquier número y tipo de argumentos», lo cual anula la comprobación de tipos. En C++, sin embargo, significa «una función sin argumentos».

Definición de funciones

La definición de funciones se parece a la declaración excepto en que tienen cuerpo. Un cuerpo es un conjunto de sentencias encerradas entre llaves. Las llaves indican el comienzo y el final del código. Para dar a func1() una definición con un cuerpo vacío (un cuerpo que no contiene código), escriba:

int func1(int ancho, int largo) {}

Note que en la definición de la función las llaves sustituyen el punto y coma. Como las llaves contienen una sentencia o grupo de sentencias, no es necesario un punto y coma. Tenga en cuenta además que los argumentos en la definición de la función deben nombres si los quiere usar en el cuerpo de la función (como aquí no se usan, son opcionales).

Sintaxis de declaración de variables

El significado atribuido a la frase «declaración de variables» históricamente ha sido confuso y contradictorio, y es importante que entienda el significado correcto para poder leer el código correctamente. Una declaración de variable dice al compilador cómo es la variable. Dice al compilador, «Sé que no has visto este nombre antes, pero te prometo que existe en algún lugar, y que es una variable de tipo X».

En una declaración de función, se da un tipo (el valor de retorno), el nombre de la función, la lista de argumentos, y un punto y coma. Con esto el compilador ya tiene suficiente información para saber cómo será la función. Por inferencia, una declaración de variable consistirá en un tipo seguido por un nombre. Por ejemplo:

int a;

podría declarar la variable a como un entero usando la lógica usada anteriormente. Pero aquí está el conflicto: existe suficiente información en el código anterior como para que el compilador pueda crear espacio para un entero llamado a y es exactamente lo que ocurre. Para resolver el dilema, fue necesaria una palabra reservada en C y C++ para decir «Esto es sólo una declaración; esta variable estará definida en algún otro lado». La palabra reservada es extern que puede significar que la definición es externa al fichero, o que la definición se encuentra después en este fichero.

Declarar una variable sin definirla implica usar la palabra reservada extern antes de una descripción de la variable, como por ejemplo:

extern int a;

extern también se puede aplicar a la declaración de funciones. Para func1() sería algo así:

extern int func1(int length, int width);

Esta sentencia es equivalente a las declaraciones anteriores para func1() . Como no hay cuerpo de función, el compilador debe tratarla como una declaración de función en lugar de como definición. La palabra reservada extern es bastante superflua y opcional para la declaración de funciones. Probablemente sea desafortunado que los diseñadores de C no obligaran al uso de extern para la declaración de funciones; hubiera sido más consistente y menos confuso (pero hubiera requerido teclear más, lo cual probablemente explica la decisión).

Aquí hay algunos ejemplos más de declaraciones:

//: C02:Declare.cpp
// Declaration & definition examples
extern int i; // Declaration without definition
extern float f(float); // Function declaration

float b;  // Declaration & definition
float f(float a) {  // Definition
  return a + 1.0;
}

int i; // Definition
int h(int x) { // Declaration & definition
  return x + 1;
}

int main() {
  b = 1.0;
  i = 2;
  f(b);
  h(i);
} ///:~

Listado 2.1. C02/Declare.cpp


En la declaración de funciones, los identificadores de los argumentos son opcionales. En la definición son necesarios (los identificadores se requieren solamente en C, no en C++).

Incluir ficheros de cabecera

La mayoría de las librerías contienen un número importante de funciones y variables. Para ahorrar trabajo y asegurar la consistencia cuando se hacen declaraciones externas para estos elementos, C y C++ utilizan un artefacto llamado fichero de cabecera. Un fichero de cabecera es un fichero que contiene las declaraciones externas de una librería; convencionalmente tiene un nombre de fichero con extensión .h, como headerfile.h (no es difícil encontrar código más antiguo con extensiones diferentes, como .hxx o .hpp, pero es cada vez más raro).

El programador que crea la librería proporciona el fichero de cabecera. Para declarar las funciones y variables externas de la librería, el usuario simplemente incluye el fichero de cabecera. Para ello se utiliza la directiva de preprocesado #include. Eso le dice al preprocesador que abra el fichero de cabecera indicado e incluya el contenido en el lugar donde se encuentra la sentencia #include. Un #include puede indicar un fichero de dos maneras: mediante paréntesis angulares ( < > ) o comillas dobles.

Los ficheros entre paréntesis angulares, como:

#include <header>

hacen que el preprocesador busque el fichero como si fuera particular a un proyecto, aunque normalmente hay un camino de búsqueda que se especifica en el entorno o en la línea de comandos del compilador. El mecanismo para cambiar el camino de búsqueda (o ruta) varía entre maquinas, sistemas operativos, e implementaciones de C++ y puede que requiera un poco de investigación por parte del programador.

Los ficheros entre comillas dobles, como:

#include "header"

le dicen al preprocesador que busque el fichero en (de acuerdo a la especificación) «un medio de definición de implementación», que normalmente significa buscar el fichero de forma relativa al directorio actual. Si no lo encuentra, entonces la directiva se preprocesada como si tuviera paréntesis angulares en lugar de comillas.

Para incluir el fichero de cabecera iostream, hay que escribir:

#include <iostream>

El preprocesador encontrará el fichero de cabecera iostream (a menudo en un subdirectorio llamado «include») y lo incluirá.

Formato de inclusión del estándar C++

A medida que C++ evolucionaba, los diferentes fabricantes de compiladores elegían diferentes extensiones para los nombres de ficheros. Además, cada sistema operativo tiene sus propias restricciones para los nombres de ficheros, en particular la longitud. Estas características crearon problemas de portabilidad del código fuente. Para limar estos problemas, el estándar usa un formato que permite los nombres de ficheros más largos que los famosos ocho caracteres y permite eliminar la extensión. Por ejemplo en vez de escribir iostream.h en el estilo antiguo, que se asemejaría a algo así:

#include <iostream.h>

ahora se puede escribir:

#include <iostream>

El traductor puede implementar la sentencia del include de tal forma que se amolde a las necesidades de un compilador y sistema operativo particular, aunque sea necesario truncar el nombre y añadir una extensión. Evidentemente, también puede copiar las cabeceras que ofrece el fabricante de su compilador a otras sin extensiones si quiere usar este nuevo estilo antes de que su fabricante lo soporte.

Las librerías heredadas de C aún están disponibles con la extensión tradicional «.h». Sin embargo, se pueden usar con el estilo de inclusión más moderno colocando una «c» al nombre. Es decir:

#include <stdio.h>
#include <stdlib.h>

Se transformaría en:

#include <cstdio>
#include <cstdlib>

Y así para todas cabeceras del C Estándar. Eso proporciona al lector una distinción interesante entre el uso de librerías C versus C++.

El efecto del nuevo formato de include no es idéntico al antiguo: usar el «.h» da como resultado una versión más antigua, sin plantillas, y omitiendo el «.h» le ofrece la nueva versión con plantillas. Normalmente podría tener problemas si intenta mezclar las dos formas de inclusión en un mismo programa.



[35] One Definition Rule