Tracks
/
C++
C++
/
Temario
/
Cabeceras
Ca

Cabeceras en C++

2 ejercicios

Acerca de Cabeceras

En C++, las declaraciones suelen estar separadas de las definiciones. Las declaraciones se agrupan en los denominados ficheros de cabecera, y las implementaciones correspondientes se colocan en los ficheros fuente. Puedes pensar en los ficheros de cabecera como una API. El fichero de cabecera te dirá qué ofrece una base de código sin entrar en los detalles de cómo.

Cabecera y fuente

La extensión de fichero más habitual para los ficheros de cabecera es .h. Algunos proyectos usan .hpp o prescinden por completo de la extensión.

Las definiciones se encuentran en un fichero .cpp aparte. Para reunir las partes, el fichero fuente empieza incluyendo el fichero de cabecera correspondiente.

Si quieres escribir una biblioteca llamada «quick_math» que ofrece una función «super_root» que quieres usar a menudo, los ficheros tendrían este aspecto:

// A file named quick_math.h
#pragma once

namespace quick_math {

double super_root(double x, int n);

}
// A file named quick_math.cpp
#include <cmath>

#include "quick_math.h"

double quick_math::super_root(double x, int n) {
    while (n) {
        x = std::sqrt(x), --n;
    }
    return x;
}

Si necesitas incluir una cabecera que solo requiere la implementación, la línea #include correspondiente solo es necesaria en el fichero fuente. Todo lo que se incluye en la cabecera también está disponible en el fichero .cpp, como la biblioteca string del ejemplo siguiente. Atención: el ; es necesario después de la declaración en el fichero de cabecera, pero no después de la definición en el fichero fuente.

Note

Muchos ejercicios de C++ en Exercism empiezan con dos ficheros casi vacíos: la cabecera y el fuente. Tienes que consultar el fichero *_test.cpp para ver los nombres y los espacios de nombres de las funciones esperadas y poder resolver el ejercicio.

Clases y cabeceras

Las clases pueden volverse muy complejas, y su relación con la división entre cabecera y fuente puede resultar confusa. Una distribución posible es mantener todos los detalles de implementación en el fichero fuente y todas las declaraciones y variables miembro en la cabecera:

// A file named robot_flower.h
#if !defined(ROBOT_FLOWER_H)
#define ROBOT_FLOWER_H

#include <string>

namespace robots {

class Flower {
   private:
    bool needs_water{};
    int size{};
    std::string name{};

   public:
    Flower(std::string name, int size = 0);
    void give_water();
    std::string get_name();
    int get_size();
    void start_next_day();
};
}  // namespace robots
#endif
// A file named robot_flower.cpp
#include "robot_flower.h"

robots::Flower::Flower(std::string name, int size) {
    this->name = "Robotica " + name;
    this->size = size;
}

void robots::Flower::start_next_day() {
    if (!needs_water) ++size;
    needs_water = true;
}

std::string robots::Flower::get_name() { return name; }

int robots::Flower::get_size() { return size; }

Cuando la cabecera se usa como resumen de la API, es ahí donde uno buscaría información como los valores predeterminados. Por eso, el valor predeterminado del parámetro size del constructor se gestiona en la cabecera y no en la implementación. Las definiciones del fichero fuente van precedidas del espacio de nombres robots y del tipo de clase Flower.

Otra opción de distribución es una biblioteca solo de cabecera, que no tiene ningún fichero .cpp:

// A file named robot_flower.h
#pragma once

#include <string>

namespace robots {

class Flower {
   private:
    bool needs_water{};
    int size{};
    std::string name{};

   public:
    Flower(std::string name, int size = 0) {
        this->name = "Robotica " + name;
        this->size = size;
    }
    void give_water() { needs_water = false; }
    std::string get_name() { return name; }
    int get_size() { return size; }
    void start_next_day() {
        if (!needs_water) ++size;
        needs_water = true;
    }
};
}  // namespace robots

Los proyectos pueden usar combinaciones de estas distribuciones, y hay mucho debate sobre cuál puede ser la mejor para cada caso de uso.

Guardas de inclusión

Es posible que hayas reparado en la línea #pragma once del fichero de cabecera de ejemplo anterior. Esto se llama guarda de inclusión, y garantiza que el contenido del fichero se incluya una sola vez durante la compilación para evitar errores. Existe otra variante más compleja de guarda de inclusión que empieza por #ifndef y termina con #endif, que se detalla a continuación.

Advanced

Declaraciones anticipadas

El código C++ se evalúa secuencialmente. Si quieres usar una función, el compilador debe conocerla en el momento de usarla. A veces no es posible encontrar una forma lineal de ordenar las definiciones en el código fuente. Echa un vistazo al ejemplo siguiente:

int myFunction(int n) {
    if (n < 10) {
        return n;
    } else {
        return myOtherFunction(n / 10);
    }
}

int myOtherFunction(int m) { return myFunction(m / 2); }

Cuando se define myFunction, el compilador todavía no conoce myOtherFunction. Por desgracia, el problema de la referencia circular no se puede resolver cambiando el orden.

C++ ofrece las declaraciones anticipadas para que el compilador conozca myFunction y myOtherFunction antes de que se definan. El compilador da por sentado que la definición aparecerá más adelante, después de la declaración. El ejemplo siguiente muestra cómo se usa una declaración anticipada para funciones.

int myFunction(int n);       // Forward declaration of myFunction
int myOtherFunction(int m);  // Forward declaration of myOtherFunction

// Definition of myFunction
int myFunction(int n) {
    if (n < 0) {
        return 0;
    } else {
        return myOtherFunction(n - 2);
    }
}

// Definition of myOtherFunction
int myOtherFunction(int m) { return myFunction(m / 2); }

Guardas de inclusión con ifndef

No importa si el mismo fichero se incluye varias veces dentro de un proyecto. Los ficheros de cabecera no deberían contener definiciones. El proyecto completo no puede tener la misma definición más de una vez. Esto se conoce como la «regla de la definición única». El compilador se encargará de hacerla cumplir.

Es fácil evitar inclusiones múltiples no deseadas de la misma definición con las guardas de inclusión. Se forman mediante un procedimiento especial durante las etapas de compilación. El objetivo es incluir un fichero solo si cierta variable no se ha definido, y definirla una vez que el fichero se ha incluido. A menudo, la secuencia de esta variable se elige como una variación del nombre del fichero. Otro método es generar un UUID para reducir el riesgo de usar la secuencia dos veces por accidente. La sintaxis puede verse a continuación, con MY_HEADER_FILE_H como variable.

#ifndef MY_HEADER_FILE_H /* any name uniquely mapped to file name */
#define MY_HEADER_FILE_H

// file content

#endif

El problema de #pragma once es que los pragmas no forman parte oficial del lenguaje C++ y su implementación varía de un compilador a otro. Muchos proyectos grandes se han pasado al método de pragma más sencillo, pero unos pocos todavía son cautos.

Editar en GitHub El enlace se abre en una nueva ventana o pestaña

Aprende Cabeceras