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.
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.
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.
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.
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.
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); }
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.