Tracks
/
C++
C++
/
Temario
/
Encabezados
En

Encabezados en C++

2 ejercicios

Acerca de Encabezados

En C++, las declaraciones a menudo se separan de las definiciones. Las declaraciones se agrupan en los llamados archivos de cabecera, y las implementaciones correspondientes se colocan en archivos fuente. Puedes pensar en los archivos de cabecera como una API. El archivo de cabecera te dice qué ofrece una base de código sin entrar en los detalles de cómo.

Cabecera y fuente

La extensión de archivo más común para los archivos de cabecera es .h. Algunos proyectos usan .hpp o prescinden por completo de la extensión.

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

Si quieres escribir una biblioteca llamada «quick_math» que ofrece una función «super_root» que quieres usar con frecuencia, los archivos se verían así:

// 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 archivo fuente. Todo lo que se incluye en la cabecera también está disponible en el archivo .cpp, como la biblioteca string del ejemplo siguiente. Atención: el ; es necesario después de la declaración en el archivo de cabecera, pero no después de la definición en el archivo fuente.

Note

Muchos ejercicios de C++ en Exercism comienzan con dos archivos casi vacíos: cabecera y fuente. Tienes que revisar el archivo *_test.cpp para ver los nombres y los namespaces de las funciones esperadas y así 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 la implementación en el archivo 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 un 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 maneja en la cabecera y no en la implementación. Las definiciones del archivo fuente llevan como prefijo el namespace robots y el tipo de clase Flower.

Otra opción de distribución es una biblioteca solo de cabecera, que no tiene ningún archivo .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 podría ser la más adecuada para cada caso de uso.

Guardas de inclusión

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

Advanced

Declaraciones anticipadas

El código C++ se evalúa de forma procedimental. Si quieres usar una función, el compilador debe conocerla en el momento en que la usas. 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. Lamentablemente, 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 asume que la definición aparecerá en algún punto posterior a la declaración. El siguiente ejemplo 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 mediante ifndef

No importa si el mismo archivo se incluye varias veces dentro de un proyecto. Los archivos 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 múltiples inclusiones accidentales 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 archivo solo si cierta variable no se ha establecido, y luego establecerla una vez que el archivo se incluye. A menudo, la secuencia de esta variable se elige como una variación del nombre del archivo. Otro método es la generación de un UUID para reducir el riesgo de usar la secuencia dos veces por accidente. La sintaxis se puede ver 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 son parte oficial del lenguaje C++ y su implementación varía de un compilador a otro. Muchos proyectos grandes se han cambiado al método más simple del pragma, pero unos pocos todavía son cautelosos.

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

Aprende Encabezados