Archivos de encabezado (C++)

Los nombres de elementos de programa, como variables, funciones, clases, etc., deben declararse antes de poder usarse. Por ejemplo, no puede solo escribir x = 42 sin declarar primero "x".

int x; // declaration
x = 42; // use x

La declaración indica al compilador si el elemento es un int, un double, una función, un class o alguna otra cosa. Además, cada nombre debe declararse (directa o indirectamente) en cada archivo .cpp en el que se use. Al compilar un programa, cada archivo .cpp se compila de forma independiente en una unidad de compilación. El compilador no tiene conocimiento de los nombres que se declaran en otras unidades de compilación. Esto significa que si define una clase o función o variable global, debe proporcionar una declaración de ese elemento en cada archivo .cpp adicional que lo use. Cada declaración de ese elemento debe ser exactamente idéntica en todos los archivos. Una ligera incoherencia provocará errores, o un comportamiento no deseado, cuando el enlazador intente combinar todas las unidades de compilación en un único programa.

Para minimizar el potencial de errores, C++ ha adoptado la convención de usar archivos de encabezado para contener declaraciones. Usted realiza las declaraciones en un archivo de encabezado y, a continuación, se usa la directiva #include en cada archivo .cpp u otro archivo de encabezado que requiera esa declaración. La directiva #include inserta una copia del archivo de encabezado directamente en el archivo .cpp antes de la compilación.

Nota:

En Visual Studio 2019, la característica de módulos de C++20 se presenta como una mejora y reemplazo posible de los archivos de encabezado. Para obtener más información, consulte Información general de los módulos en C++.

Ejemplo

El ejemplo siguiente muestra una manera común de declarar una clase y, a continuación, usarla en un archivo de código fuente diferente. Comenzaremos con el archivo de encabezado, my_class.h. Contiene una definición de clase, pero tenga en cuenta que la definición está incompleta; la función miembro do_something no está definida:

// my_class.h
namespace N
{
    class my_class
    {
    public:
        void do_something();
    };

}

A continuación, cree un archivo de implementación (normalmente con una extensión .cpp o similar). Llamaremos al archivo my_class.cpp y proporcionaremos una definición para la declaración de miembro. Agregamos una directiva #include para el archivo "my_class.h" para que la declaración de my_class se inserte en este punto en el archivo .cpp e incluiremos <iostream> para extraer la declaración de std::cout. Tenga en cuenta que las comillas se usan para los archivos de encabezado en el mismo directorio que el archivo de origen y los corchetes angulares se usan para encabezados de biblioteca estándar. Además, muchos encabezados de biblioteca estándar no tienen .h ni ninguna otra extensión de archivo.

En el archivo de implementación, opcionalmente podemos usar una instrucción using para evitar tener que calificar todas las menciones de "my_class" o "cout" con "N::" o "std::". No coloque instrucciones using en los archivos de encabezado.

// my_class.cpp
#include "my_class.h" // header in local directory
#include <iostream> // header in standard library

using namespace N;
using namespace std;

void my_class::do_something()
{
    cout << "Doing something!" << endl;
}

Ahora podemos usar my_class en otro archivo .cpp. Usamos #include en el archivo de encabezado para que el compilador extraiga la declaración. Todo el compilador debe saber que my_class es una clase que tiene una función miembro pública denominada do_something().

// my_program.cpp
#include "my_class.h"

using namespace N;

int main()
{
    my_class mc;
    mc.do_something();
    return 0;
}

Después de que el compilador termine de compilar cada archivo .cpp en archivos .obj, pasa los archivos .obj al enlazador. Cuando el enlazador combina los archivos de objeto, encuentra exactamente una definición para my_class; está en el archivo .obj generado para my_class.cpp y la compilación se realiza correctamente.

Incluir restricciones

Normalmente, los archivos de encabezado tienen una directiva incluir restricciones o #pragma once para asegurarse de que no se insertan varias veces en un único archivo .cpp.

// my_class.h
#ifndef MY_CLASS_H // include guard
#define MY_CLASS_H

namespace N
{
    class my_class
    {
    public:
        void do_something();
    };
}

#endif /* MY_CLASS_H */

Qué colocar en un archivo de encabezado

Dado que un archivo de encabezado podría incluirse en varios archivos, no puede contener definiciones que puedan generar varias definiciones del mismo nombre. No se permite lo siguiente, o se considera una práctica muy desaconsejable:

  • definiciones de tipo integradas en el espacio de nombres o el ámbito global
  • definiciones de funciones no insertadas
  • definiciones de variables no constantes
  • definiciones de agregado
  • espacios de nombres sin nombre
  • Directivas using

El uso de la directiva using no provocará necesariamente un error, pero puede causar un problema porque lleva el espacio de nombres al ámbito en cada archivo .cpp que incluya directa o indirectamente ese encabezado.

Archivo de encabezado de ejemplo

En el ejemplo siguiente se muestran los distintos tipos de declaraciones y definiciones que se permiten en un archivo de encabezado:

// sample.h
#pragma once
#include <vector> // #include directive
#include <string>

namespace N  // namespace declaration
{
    inline namespace P
    {
        //...
    }

    enum class colors : short { red, blue, purple, azure };

    const double PI = 3.14;  // const and constexpr definitions
    constexpr int MeaningOfLife{ 42 };
    constexpr int get_meaning()
    {
        static_assert(MeaningOfLife == 42, "unexpected!"); // static_assert
        return MeaningOfLife;
    }
    using vstr = std::vector<int>;  // type alias
    extern double d; // extern variable

#define LOG   // macro definition

#ifdef LOG   // conditional compilation directive
    void print_to_log();
#endif

    class my_class   // regular class definition,
    {                // but no non-inline function definitions

        friend class other_class;
    public:
        void do_something();   // definition in my_class.cpp
        inline void put_value(int i) { vals.push_back(i); } // inline OK

    private:
        vstr vals;
        int i;
    };

    struct RGB
    {
        short r{ 0 };  // member initialization
        short g{ 0 };
        short b{ 0 };
    };

    template <typename T>  // template definition
    class value_store
    {
    public:
        value_store<T>() = default;
        void write_value(T val)
        {
            //... function definition OK in template
        }
    private:
        std::vector<T> vals;
    };

    template <typename T>  // template declaration
    class value_widget;
}