He

Headers em C++

2 exercícios

Sobre Headers

Em C++, as declarações geralmente são separadas das definições. As declarações são agrupadas em arquivos chamados de cabeçalho, e as respectivas implementações ficam em arquivos fonte. Você pode pensar nos arquivos de cabeçalho como uma API. O arquivo de cabeçalho diz o que uma base de código tem a oferecer, sem entrar nos detalhes de como.

Cabeçalho e fonte

A extensão de arquivo mais comum para arquivos de cabeçalho é .h. Alguns projetos usam .hpp ou dispensam a extensão por completo.

As definições ficam em um arquivo .cpp separado. Para reunir as partes, o arquivo fonte começa incluindo o respectivo arquivo de cabeçalho.

Se você quiser escrever uma biblioteca chamada "quick_math" que oferece uma função "super_root" que você quer usar com frequência, os arquivos ficariam assim:

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

Se você precisar incluir um cabeçalho que só é necessário para a implementação, a respectiva linha #include só é necessária no arquivo fonte. Tudo o que é incluído no cabeçalho também fica disponível no arquivo .cpp, como a biblioteca string no exemplo abaixo. Atenção: o ; é necessário depois da declaração no arquivo de cabeçalho, mas não depois da definição no arquivo fonte.

Note

Muitos exercícios de C++ no Exercism começam com dois arquivos quase vazios: cabeçalho e fonte. Você precisa verificar o arquivo *_test.cpp para ver os nomes e os namespaces das funções esperadas, a fim de resolver o exercício.

Classes e cabeçalhos

As classes podem ficar muito complexas, e a relação delas com a divisão entre cabeçalho e fonte pode ser confusa. Uma organização possível é manter todos os detalhes de implementação no arquivo fonte e todas as declarações e variáveis membro no cabeçalho:

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

Quando o cabeçalho é usado como uma visão geral da API, é ali que a pessoa procuraria informações como valores padrão. O valor padrão do parâmetro size do construtor é, portanto, definido no cabeçalho, e não na implementação. As definições no arquivo fonte são prefixadas com o namespace robots e o tipo de classe Flower.

Outra opção de organização é uma biblioteca header only, que não tem nenhum arquivo .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

Os projetos podem usar combinações dessas organizações, e há muita discussão sobre qual seria a melhor opção para cada caso de uso.

Guardas de inclusão

Você deve ter notado a linha #pragma once no arquivo de cabeçalho de exemplo acima. Isso é chamado de guarda de inclusão, e ela garante que o conteúdo do arquivo seja incluído apenas uma vez durante a compilação, para evitar erros. Existe outra variação, mais complexa, de guarda de inclusão, que começa com #ifndef e termina com #endif, detalhada abaixo.

Advanced

Declarações antecipadas

O código C++ é avaliado de forma procedural. Se você quiser usar uma função, ela precisa ser conhecida pelo compilador no momento do uso. Às vezes não é possível encontrar uma ordem linear para organizar as definições no código fonte. Dê uma olhada no exemplo abaixo:

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

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

Quando myFunction é definida, o compilador ainda não conhece myOtherFunction. Infelizmente, o problema de referência circular não pode ser resolvido trocando a ordem.

C++ oferece declarações antecipadas para informar o compilador sobre myFunction e myOtherFunction antes de elas serem definidas. O compilador presume que a definição virá em algum momento posterior à declaração. O próximo exemplo mostra como uma declaração antecipada é usada para funções.

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 inclusão com ifndef

Não importa se o mesmo arquivo é incluído várias vezes dentro de um projeto. Arquivos de cabeçalho não devem conter definições. O projeto inteiro não pode ter a mesma definição mais de uma vez. Isso é chamado de "regra da definição única". Ela será imposta pelo compilador.

É fácil evitar múltiplas inclusões não intencionais da mesma definição com as guardas de inclusão. Elas são formadas por um procedimento especial durante as etapas de compilação. O objetivo é incluir um arquivo apenas se uma determinada variável ainda não tiver sido definida, e então defini-la assim que o arquivo for incluído. Muitas vezes, a sequência dessa variável é escolhida como uma variação do nome do arquivo. Outro método é a geração de um UUID para reduzir o risco de usar a mesma sequência duas vezes por acidente. A sintaxe pode ser vista abaixo, com MY_HEADER_FILE_H como variável.

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

// file content

#endif

O problema com #pragma once é que os pragmas não fazem parte oficial da linguagem C++, e a implementação varia de compilador para compilador. Muitos projetos grandes migraram para o método mais simples com pragma, mas alguns ainda são cautelosos.

Editar via GitHub O link abre em uma nova janela ou aba

Aprenda Headers