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