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.
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.
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);
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».
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).
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++).
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á.
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.