Percursos
/
C++
C++
/
Programa
/
Cabeçalhos
Ca

Cabeçalhos em C++

2 exercícios

Sobre Cabeçalhos

Em C++, as declarações são frequentemente separadas das definições. As declarações são agrupadas em ficheiros de cabeçalho, com as respetivas implementações colocadas em ficheiros de código-fonte. Podes pensar nos ficheiros de cabeçalho como uma API. O ficheiro de cabeçalho diz-te o que uma base de código tem para oferecer, sem entrar nos detalhes de como.

Cabeçalho e código-fonte

A extensão de ficheiro mais comum para ficheiros de cabeçalho é .h. Alguns projetos usam .hpp ou dispensam completamente a extensão.

As definições ficam num ficheiro .cpp separado. Para reunir as partes, o ficheiro de código-fonte começa por incluir o respetivo ficheiro de cabeçalho.

Se quiseres escrever uma biblioteca chamada "quick_math" que oferece uma função "super_root" que queres usar frequentemente, os ficheiros 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 precisares de incluir um cabeçalho que só é necessário na implementação, a respetiva linha #include só é precisa no ficheiro de código-fonte. Tudo o que é incluído no cabeçalho também está disponível no ficheiro .cpp, como a biblioteca string no exemplo abaixo. Atenção: o ; é preciso depois da declaração no ficheiro de cabeçalho, mas não depois da definição no ficheiro de código-fonte.

Note

Muitos exercícios de C++ no Exercism começam com dois ficheiros quase vazios: cabeçalho e código-fonte. Tens de verificar o ficheiro *_test.cpp para veres os nomes e os namespaces das funções esperadas, para conseguires resolver o exercício.

Classes e cabeçalhos

As classes podem tornar-se muito complexas e a sua relação com a divisão cabeçalho / código-fonte pode ser confusa. Uma organização possível é manter todos os detalhes de implementação no ficheiro de código-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, é aí que alguém procuraria informações como valores predefinidos. O valor predefinido do parâmetro size do construtor é, por isso, tratado no cabeçalho e não na implementação. As definições no ficheiro de código-fonte são precedidas pelo namespace robots e pelo tipo de classe Flower.

Outra opção de organização é uma biblioteca só com cabeçalho, que não tem sequer um ficheiro .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 destas organizações e há muita discussão sobre qual será a mais adequada para cada caso de uso.

Guardas de inclusão

Talvez tenhas reparado na linha #pragma once no ficheiro de cabeçalho de exemplo acima. Isto chama-se uma guarda de inclusão e garante que o conteúdo do ficheiro é incluído apenas uma vez durante a compilação, para evitar erros. Existe outra variação, mais complexa, de uma guarda de inclusão que começa com #ifndef e termina com #endif, descrita abaixo.

Advanced

Declarações antecipadas

O código C++ é avaliado de forma procedural. Se quiseres usar uma função, ela tem de ser conhecida do compilador no momento da utilização. Por vezes, não é possível encontrar uma forma linear de organizar as definições no código-fonte. Dá uma vista de olhos ao 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 da referência circular não se resolve trocando a ordem.

O C++ oferece declarações antecipadas para que o compilador conheça myFunction e myOtherFunction antes de serem definidas. O compilador assume que a definição virá mais tarde, depois da declaração. O exemplo seguinte mostra como se usa uma declaração antecipada 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 ficheiro é incluído várias vezes dentro de um projeto. Os ficheiros de cabeçalho não devem conter definições. O projeto completo não pode ter a mesma definição mais do que uma vez. Isto chama-se a "regra da definição única". Será imposta pelo compilador.

É fácil evitar múltiplas inclusões involuntárias da mesma definição com guardas de inclusão. São formadas por um procedimento especial durante as fases de compilação. O objetivo é incluir um ficheiro apenas se uma determinada variável ainda não estiver definida e, depois, defini-la quando o ficheiro for incluído. Muitas vezes, a sequência desta variável é escolhida como uma variação do nome do ficheiro. 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 mudaram para o método mais simples com o pragma, mas alguns ainda são cautelosos.

Editar via GitHub A ligação abre numa nova janela ou separador

Aprende Cabeçalhos