cpp-ficheros-h-y-cpp

Ficheros .h y .cpp en C++

Una de las cosas peculiares y que más cuesta de entender al trabajar con C++ es que los ficheros de código se dividen en dos tipos de ficheros.

Por un lado tenemos:

  • Archivos de encabezado (.h) que contienen las declaraciones (qué clases, funciones o constantes existen)
  • Archivos de implementación (.cpp) que contienen las contienen las definiciones (cómo funcionan esas clases y funciones)

Esto es puede resultar chocante (además de un engorro), ya que la mayoría de lenguajes modernos declaraciones y definiciones en un solo archivo.

Sin embargo, cuando los ordenadores eran mucho menos potentes esta división ayudaba a reducir los tiempos de compilacion ⌛, sobre todo en proyectos grandes (en la actualidad ya no tienen mucho sentido).

Esto es una herencia directa de C, donde se ya se hacía esta separación de código en cabeceras (.h) y fuentes (.c)

Aunque no siempre son estrictamente necesarios, los ficheros .h y .cpp siguen siendo el día a día en cualquier proyecto de C++ (vamos, que vais a tener que lidiar con ellos sí o sí)

Los módulos son una alternativa más moderna para gestionar proyectos.

Introducción a los Ficheros .h y .cpp

Como decía, generalmente en un proyecto de C++ tendremos ficheros .h y .cpp.

El .h es como el contrato o la promesa: “Te digo qué hay disponible para usar.” El .cpp es la implementación: “Te digó cómo funciona.”

Vamos a verlos un poco más en detalle 👇

Los archivos de encabezado .h (header) contienen las definiciones de las funciones y clases que se usarán en un programa.

  • Declaraciones de Clases: Contiene la definición de clases, incluyendo miembros de datos y métodos.
  • Declaraciones de Funciones: Define las funciones que se implementan en los archivos .cpp.
  • Macros y Constantes: Declara constantes, macros y otros elementos globales.

Los archivos de implementación, con la extensión .cpp, contienen la implementación real del código. Aquí es donde se escriben las definiciones de las funciones y métodos declarados en los archivos .h.

  • Definiciones de Métodos: Implementa los métodos y funciones que se han declarado en los archivos .h.
  • Código de Lógica: Contiene el código que realiza la lógica específica del programa.

Estructura típica de un proyecto

En un proyecto C++ tradicional, se sigue una convención de separar las declaraciones y las definiciones en diferentes archivos.

Lógicamente, cada uno organiza su proyecto como quiere (sobre todo los grandes). Pero, en un ejemplo sencillo, típicamente un proyecto tendría esta pinta.

/proyecto
    |-- src/
        |-- main.cpp
        |-- mi_clase.cpp
    |-- include/
        |-- mi_clase.h

Es decir, tendríamos separados en carpetas los distintos elementos.

  • src/: Meteríamos los archivos de implementación (.cpp).
  • include/: Tendríamos los archivos de encabezado (.h).

Inclusión de archivos

La directiva #include se utiliza para incluir el contenido de un archivo en otro archivo.

#include "mi_clase.h"

En los archivos .cpp, incluiremos los archivos de encabezado necesarios para acceder a las declaraciones de clases y funciones.

Ejemplo de código

Vamos a verlo mejor todo junto en un ejemplo. Supongamos que tenemos una clase llamada MiClase.

Por un lado tendríamos el archivo de encabezado (mi_clase.h).

#ifndef MI_CLASE_H
#define MI_CLASE_H

class MiClase {
public:
    MiClase();                    // Constructor
    void miMetodo();              // Método público
private:
    int miVariable;               // Variable privada
};

#endif // MI_CLASE_H

Por otro lado tendríamos el fichero de archivo de implementación mi_clase.cpp)

#include "mi_clase.h"
#include <iostream>

MiClase::MiClase() : miVariable(0) {
    // Constructor: Inicializa miVariable a 0
}

void MiClase::miMetodo() {
    std::cout << "Mi variable es: " << miVariable << std::endl;
}

Aquí tenemos la definición (el código) de las funciones que teníamos declarado en el .h.

Finalmente, así lo usaríamos en nuestro archivo principal main.cpp.

#include "mi_clase.h"

int main() {
    MiClase objeto;
    objeto.miMetodo();
    return 0;
}

Aquí incluiríamos el fichero mi_clase.h. Con eso el main tiene disponible puede usar las declaraciones *(en este caso de MiClase).

Posteriormente, el compilador juntará todo, añadiendo las implementaciones del fichero .cpp.

Salvaguarda en C++

Una salvaguarda es un mecanismo que se utiliza para que un archivo de encabezado no se incluya más de una vez en un proyecto.

Básicamente son directivas #ifndef, #define, #endif que se insertan en los archivos de cabecera .h, que hacen que el compilador incluya el código una única vez. Así,

#ifndef MI_CLASE_H
#define MI_CLASE_H

// Contenido del archivo de encabezado

#endif // MI_CLASE_H

Aquí,

  • #ifndef: Verifica si la macro no está definida.
  • #define: Define la macro si no está ya definida.
  • #endif: Finaliza la guarda de inclusión

Sin salvaguardas, los mismos bloques de código podrían ser incluidos múltiples veces en un solo archivo fuente, lo que nos daría un error al compilar.

Directiva pragma once

En lugar de usar las tradicionales guardas de inclusión , una forma más moderna es usar una directiva #pragma once.

Esta directiva es más concisa y fácil de leer, e igualmente asegura que un archivo se incluya solo una vez durante la compilación (pero sin necesidad de definir macros manualmente).

#pragma once

class MiClase {
public:
    void miMetodo();
};

Como vemos, #pragma once es mucho más cómoda, ya que no hace falta envolver todo nuestro código en directivas de salvaguarda. Simplemente ponemos una única línea al principio del fichero.

Para poder usarlo necesitamos que sea soportada por el compilador. Es compatible con la mayoría de los compiladores modernos (como GCC, Clang y MSVC).