Arquivos de cabeçalho (C++)

Os nomes dos elementos do programa, como variáveis, funções, classes etc., devem ser declarados para serem usados. Por exemplo, você não pode simplesmente escrever x = 42 sem primeiro declarar 'x'.

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

A declaração informa ao compilador se o elemento é um int, um double, uma função, uma class ou alguma outra coisa. Além disso, cada nome deve ser declarado (direta ou indiretamente) em cada arquivo .cpp no qual ele é usado. Quando você compila um programa, cada arquivo .cpp é compilado de maneira independente em uma unidade de compilação. O compilador não tem conhecimento de quais nomes são declarados em outras unidades de compilação. Isso significa que, se você definir uma classe ou função ou variável global, deverá fornecer uma declaração dela em cada arquivo .cpp adicional que a usa. Cada declaração dela deve ser exatamente idêntica em todos os arquivos. Uma pequena inconsistência causará erros ou comportamento não intencional quando o vinculador tentar mesclar todas as unidades de compilação em um só programa.

Para minimizar o potencial de erros, o C++ adotou a convenção de usar arquivos de cabeçalho para conter declarações. Você faz as declarações em um arquivo de cabeçalho e depois usa a diretiva #include em cada arquivo .cpp ou outro arquivo de cabeçalho que requer essa declaração. A diretiva #include insere uma cópia do arquivo de cabeçalho diretamente no arquivo .cpp antes da compilação.

Observação

No Visual Studio 2019, o recurso módulos C++20 é introduzido como uma melhoria e eventual substituição para arquivos de cabeçalho. Para obter mais informações, confira Visão Geral de módulos no C++.

Exemplo

O exemplo a seguir mostra um modo comum de declarar uma classe e usá-la em um arquivo de origem diferente. Vamos começar com o arquivo de cabeçalho, my_class.h. Ele contém uma definição de classe, mas observe que a definição está incompleta; a função de membro do_something não está definida:

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

}

Então crie um arquivo de implementação (normalmente com uma extensão .cpp ou semelhante). Chamaremos o arquivo my_class.cpp e forneceremos uma definição para a declaração de membro. Adicionamos uma diretiva #include para o arquivo "my_class.h" para que a declaração my_class seja inserida neste ponto no arquivo .cpp e incluímos <iostream> para efetuar pull da declaração para std::cout. Observe que as aspas são usadas para arquivos de cabeçalho no mesmo diretório que o arquivo de origem e colchetes angulares são usados para cabeçalhos de biblioteca padrão. Além disso, muitos cabeçalhos de biblioteca padrão não têm .h nem nenhuma outra extensão de arquivo.

No arquivo de implementação, opcionalmente, podemos usar uma instrução using para evitar a qualificação de todas as menções de "my_class" ou "cout" com "N::" ou "std::". Não coloque instruções using em seus arquivos de cabeçalho.

// 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;
}

Agora podemos usar my_class em outro arquivo .cpp. Nós incluímos (#include) o arquivo de cabeçalho para que o compilador efetue pull na declaração. Tudo o que o compilador precisa saber é que my_class é uma classe que tem uma função de membro público chamada do_something().

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

using namespace N;

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

Depois que o compilador terminar de compilar cada arquivo .cpp em arquivos .obj, ele passará os arquivos .obj para o vinculador. Quando o vinculador mescla os arquivos de objeto, ele encontra exatamente uma definição para my_class; ela está no arquivo .obj produzido para my_class.cpp e o build é bem-sucedido.

Incluir guardas

Normalmente, os arquivos de cabeçalho têm um proteção de inclusão ou uma diretiva #pragma once para garantir que eles não sejam inseridos várias vezes em um só arquivo .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 */

O que colocar em um arquivo de cabeçalho

Como um arquivo de cabeçalho pode ser incluído por vários arquivos, ele não pode conter definições que possam produzir várias definições de mesmo nome. Os seguintes não são permitidos ou são considerados uma prática muito ruim:

  • definições de tipo internas no namespace ou no escopo global
  • definições de função não embutidas
  • definições de variáveis não const
  • definições de agregação
  • namespaces sem nome
  • Diretivas using

O uso da diretiva using não necessariamente causará um erro, mas pode causar um problema porque ele coloca o namespace no escopo em cada arquivo .cpp que inclui direta ou indiretamente esse cabeçalho.

Arquivo de cabeçalho de exemplo

O seguinte exemplo mostra os vários tipos de declarações e definições permitidas em um arquivo de cabeçalho:

// 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;
}