C++ Clases I

[forCode]

De entre los varios paradigmas de desarrollo que soporta C++, se encuentra la orientación a objetos (OOP). C++ soporta OOP a través del concepto de clase, que a nivel conceptual, no difiere nada del concepto de clase que pueden tener otros lenguajes como Java o C#. Pero C++ esconde bastantes características que no suelen encontrarse en otros lenguajes. En esta serie de posts veremos como C++ implementa el paradigma del desarrollo orientado a objetos. En este primer post, nos centraremos en un aspecto que diferencia a C++ de la mayoría de lenguajes y que es necesario de entender antes de entrar en materia: los ficheros de cabecera.

Ficheros de cabecera

 

Si vienes del mundo C ya los conocerás, pero si vienes de casi cualquier otro lenguaje, lo más probable es que no. Debemos tener claro el concepto de translation unit. En C++, el compilador no compila proyectos o ficheros de código fuente: compila de forma separada e independiente un conjunto de translation units. Una translation unit debe compilar por si sola, sin necesitar para nada al resto de translation units que pueda haber en el proyecto (el compilador las compilará todas y el orden en el que lo haga digamos que tiene que dar igual). Empecemos por decir que básicamente una translation unit equivale a un fichero .cpp. Por lo tanto, si nuestro proyecto tiene 10 ficheros .cpp, tendremos 10 translation units.

Esto parece una tontería, puro funcionamiento interno del compilador, pero sus implicaciones son muy grandes. Imagina el siguiente código:

int foo() {
	int i = 1;
	return bar(i) + 10;
}

Esto como translation unit no es válido. El compilador no puede compilar este código porque no sabe qué es bar. No sabe si existe, si es una función y, en el caso de que lo sea, si realmente acepta un entero como parámetro. Para que compile necesitamos añadir bar dentro de la translation unit:

int bar(int i) {
	return 0;
}
int foo() {
	int i = 1;
	return bar(i) + 10;
}

Ahora si que tenemos una translation unit válida. Pero el problema nos surge si desde otra translation unit necesitamos usar bar. Recuerda que una translation unit equivale a un fichero .cpp. La única opción que parece que podemos tener, es copiar de nuevo el código de bar dentro de la otra translation unit. Al margen de que eso sea impracticable, tampoco funcionaría porque luego en linker se quejaría (algún día hablaremos del linker). Entonces ¿cuál es la solución?

La solución pasa por el uso de ficheros de cabecera. Fíjate que, para compilar foo, el compilador no necesita para nada el código de bar. Tan solo necesita la declaración de bar. Es decir, algo que indique al compilador que bar existe, que es una función, que acepta y devuelve un entero. La definición de bar, es decir, lo qué hace, no lo necesita para nada el compilador para compilar foo. De ahí nacen los ficheros de cabecera: contienen las declaraciones, mientras que los ficheros de código fuente (.cpp), contienen las definiciones. Ahora todo lo que necesitamos, es un mecanismo para incluír las declaraciones de un fichero de cabecera, dentro de la translation unit actual. Y este método es la directiva #include.

En este caso, tendríamos un fichero de cabecera (se suele usar la extensión .h de header) con el siguiente código:

int bar(int);

Esto es exactamente la declaración de la función bar. Indica al compilador la firma de la función, es decir, su nombre, el tipo de valor de retorno y el tipo de los parámetros. Todo lo que el compilador necesita por el momento. Este código podría estar en un fichero llamado bar.h.

Ahora, imagina que el código que define la función foo, está en foo.cpp. Entonces debemos incluir la declaración de bar (es decir, el fichero bar.h) en el fichero foo.cpp:

#include "bar.h"

int foo() {
	int i = 1;
	return bar(i) + 10;
}

La directiva #include es una directiva del preprocesador. El preprocesador es un elemento del pipeline de compilación de C++, que se ejecuta antes que el compilador y que transforma el código fuente de los ficheros .cpp. Es decir, el compilador no compila el código de los ficheros .cpp tal y como los vemos nosotros, si no tal y como lo modifica el preprocesador. En este caso, el preprocesador sustituye la directiva #include por el contenido del fichero incluido. Es decir, el compilador literalmente compila lo siguiente:

int bar(int);

int foo() {
	int i = 1;
	return bar(i) + 10;
}

El compilador no entiende de ficheros de cabecera, ni de #includes. Para él, todo eso no existe. Es el preprocesador quien se encarga de esta parte. De hecho, en el estándar C++, se define translation unit como el contenido de un fichero de código fuente (es decir, un .cpp), junto con el contenido de todos los ficheros de cabecera incluídos (directa o indirectamente, ya que un fichero de cabecera puede incluir a otros ficheros de cabecera). En otras palabras, una translation unit es el resultado que emite el preprocesador a partir de un fichero de código fuente.

Dependencias a tres

En los ficheros de cabecera no solo se declaran funciones, también se declaran tipos. Bien sea con clases (que veremos más adelante), con structs o con typedefs. Imagina un fichero de cabecera, llamado Point.h que declara un tipo Point usando typedef:

typedef struct {
	int x;
	int y;
} Point, *PPoint;

Este código declara dos tipos, uno llamado Point, que es la structura con los valores x e y, y luego otro llamado PPoint, que es un puntero a dicha estructura. Ahora, imaginemos una función Bar que acepta como parámetro un valor de tipo Point. En el fichero Bar.h debemos poner su declaración:

#include "Point.h"
int Bar(Point p);

Observa como desde el fichero de cabecera Bar.h, incluimos el fichero Point.h, porque si no el compilador dará error, ya que no sabrá que es «Point». Por supuesto, recuerda que el compilador no compila ficheros de cabecera. Necesitamos una translation unit, es decir, un fichero de código fuente (.cpp) que contenga la definición de la función Bar. Este sería el fichero Bar.cpp:

#include "Bar.h"
int Bar(Point p) {
	return p.x + p.y;
}

Este código compila y funciona perfectamente. Pero, añadamos ahora otra función (Foo), que también acepte parámetros de tipo Point. Así, en su fichero de cabecera (Foo.h) declararemos la función de la siguiente manera:

#include "Point.h"
int Foo(Point p1, Point p2);

Y ahora viene el detalle interesante. Imagina que en la definición de la función, necesitamos llamar a Bar, desde dentro de Foo. Es decir, en el fichero de código fuente Foo.cpp metemos el siguiente código:

#include "Foo.h"
#include "Bar.h"
int Foo(Point p1, Point p2) {
	return Bar(p1) + Bar(p2);
}

Incluimos el fichero Foo.h ,pero también debemos incluír el fichero Bar.h, ya que si no el compilador no sabrá que Bar es una función. Pues bien, en este punto el código ya no compila. ¿Por qué? Veamos que es lo que recibe el compilador cuando quiere compilar Foo.cpp. Para ello debemos sustituir los dos includes, por el contenido de sus ficheros. Esta sustitución es recursiva (es decir, si un fichero de cabecera incluye a otro también se sustituye). Así el compilador recibe esto:

// #include "Foo.h" inicio sustitución
typedef struct {
	int x;
	int y;
} Point, *PPoint;
int Foo(Point p1, Point p2);
// #include "Bar.h" inicio sustitución
typedef struct {
	int x;
	int y;
} Point, *PPoint;
int Bar(Point p);
// Resto de código de Foo.cpp
int Foo(Point p1, Point p2) {
	return Bar(p1) + Bar(p2);
}

Tanto Foo.h como Bar.h incluían Point.h, y al incluir Foo.cpp, tanto Foo.h como Bar.h, duplican el contenido de Point.h. El compilador se quejará de que hay un tipo que se ha declarado más de una vez. Debes evitar siempre, que un fichero de cabecera sea incluido más de una vez por el preprocesador.

Este ejemplo es muy sencillo, quizá si piensas alguna alternativa podrías dar con alguna solución, pero en proyectos grandes, esto es imposible. Por suerte, el preprocesador viene en nuestra ayuda. Para ello, usa #pragma once en todos los ficheros de cabecera que solo deban ser incluídos una sola vez. Es decir, en todos. Usa #pragma once en todos tus ficheros de cabecera, siempre. Usando #pragma once, le decimos al preprocesador que evite incluir un mismo fichero de cabecera más de una vez.

En nuestro caso si tan solo lo usamos en Point.h:

#pragma once
typedef struct {
	int x;
	int y;
} Point, *PPoint;

El código ya compila, ya que este era el único fichero de cabecera que se duplicaba, pero úsalo siempre. Deja que el procesador se encargue de evitar que se duplique un fichero de cabecera.

Un detalle: Si miras código C/C++ que veas por ahí, puedes ver que en lugar de usar #pragma once, se usa #ifndef:

#ifndef __POINT_H
#define __POINT_H
typedef struct {
	int x;
	int y;
} Point, *PPoint;
#endif

Este código hace que el procesador mire si está definido el valor __POINT_H, y si NO lo está, lo defina y procese el resto del fichero. La segunda vez que el preprocesador procesa el fichero de cabecera, el valor __POINT_H, ya ha sido definido y por lo tanto el preprocesador no incluye nada (el #include correspondiente se sustituye por una línea en blanco).

Usar #ifndef de esta forma tiene el mismo efecto que #pragma once, siempre que te asegures de que cada fichero de cabecera usa un valor distinto (por eso se suele poner el nombre del fichero de cabecera como parte del nombre del valor). Este mecanismo es el que se usaba antes de que existiese #pragma once y lo puedes ver en mucho código antiguo o que requiera compatibilidad con compiladores antiguos. Mi recomendación es que uses #pragma once.

Y con esto, cerramos este primer post dedicado a los ficheros de cabecera. En el siguiente veremos como declarar clases, estructuras y sus diferencias.