12. Organização em múltiplos arquivos¶
Este capítulo precisa de um ambiente com múltiplos arquivos
Diferente do resto do curso, os exercícios deste capítulo não têm versão de arquivo único — a ideia inteira é separar declaração de implementação em arquivos diferentes. Use o ambiente local, ou um IDE online que aceite múltiplos arquivos (paiza.IO, Codiva.io, Playcode e OnlineCPP anunciam esse suporte — confirme com seu professor qual está configurado para a turma). O Wokwi, usado a partir da Unidade 7, também aceita múltiplos arquivos nativamente.
Por que separar .h de .cpp¶
Você já vem fazendo isso desde a Unidade 1, dentro do mesmo exercício: um arquivo .h com a declaração (assinatura da função/classe) e um .cpp com a implementação (o corpo). Até agora, isso era "invisível" porque os exercícios juntavam tudo numa versão de arquivo único para caber no compilador online. Agora vale entender por que essa separação existe de verdade.
Um projeto C++ real tem vários arquivos .cpp, cada um compilado separadamente em um arquivo objeto (.o), depois todos ligados (linkados) num executável só. Para o arquivo A usar uma função/classe definida no arquivo B, ele precisa ver a declaração — não a implementação inteira, só a assinatura. É isso que o .h fornece: um "cardápio" do que aquele módulo oferece, sem expor os detalhes de como.
projeto/
├── conta.h <- declaracao da classe ContaBancaria
├── conta.cpp <- implementacao dos metodos de ContaBancaria
├── main.cpp <- usa ContaBancaria (so inclui conta.h)
conta.h:
#ifndef CONTA_H
#define CONTA_H
#include <string>
class ContaBancaria {
private:
std::string titular_;
double saldo_;
public:
ContaBancaria(std::string titular, double saldo_inicial);
void deposita(double valor);
bool saca(double valor);
double saldo() const;
};
#endif
conta.cpp:
#include "conta.h"
ContaBancaria::ContaBancaria(std::string titular, double saldo_inicial)
: titular_(titular), saldo_(saldo_inicial >= 0 ? saldo_inicial : 0.0) {
}
void ContaBancaria::deposita(double valor) {
if (valor > 0) saldo_ += valor;
}
bool ContaBancaria::saca(double valor) {
if (valor > 0 && valor <= saldo_) {
saldo_ -= valor;
return true;
}
return false;
}
double ContaBancaria::saldo() const {
return saldo_;
}
main.cpp:
#include "conta.h"
#include <iostream>
int main() {
ContaBancaria c("Ana", 100.0);
c.deposita(50.0);
std::cout << c.saldo() << "\n";
}
Classe::metodo(...) fora da declaração da classe é a sintaxe para "isto é a implementação de metodo, que pertence a Classe" — você já viu isso nos exercícios de struct com métodos das listas 03, 04 e 07.
Compilando múltiplos arquivos¶
Localmente, você passa todos os .cpp relevantes ao compilador de uma vez:
O compilador compila main.cpp e conta.cpp separadamente (cada um vira um .o internamente) e depois linka os dois num executável só. Isso é o motivo de o erro "undefined reference to..." (abaixo) ser um erro de linkedição, não de compilação — cada arquivo individual pode compilar perfeitamente e o link ainda falhar, se faltar algum .cpp.
O problema da inclusão múltipla, e o #ifndef/#define/#endif¶
Se dois arquivos .cpp diferentes incluírem o mesmo .h, ou se um .h incluir outro que já foi incluído antes (direta ou indiretamente), o conteúdo do .h pode acabar colado mais de uma vez no mesmo arquivo final antes da compilação — e uma classe (ou função) declarada duas vezes no mesmo arquivo é erro de compilação.
O padrão include guard, que você já vem escrevendo desde a Unidade 1 sem talvez ter percebido o motivo completo, resolve isso:
#ifndef CONTA_H // "se CONTA_H ainda nao foi definido..."
#define CONTA_H // "...defina agora"
// conteudo do header aqui
#endif // fim do "se"
Na primeira vez que conta.h é incluído em qualquer lugar, CONTA_H ainda não existe, então o pré-processador entra no bloco, processa o conteúdo, e define CONTA_H. Se o mesmo conta.h for incluído de novo (direta ou indiretamente) no mesmo arquivo final, CONTA_H já existe, e o pré-processador pula o conteúdo inteiro — evitando a declaração duplicada.
Alternativa moderna, suportada por praticamente todo compilador atual: #pragma once no topo do arquivo, em vez do trio #ifndef/#define/#endif. Faz a mesma coisa, com menos código — mas não é 100% padrão da linguagem (é uma extensão amplamente suportada), então este curso usa o #ifndef tradicional, que funciona em qualquer lugar.
Erros comuns¶
**"undefined reference to ContaBancaria::deposita(double)'"** (erro de **linker**, não de compilador) — você esqueceu de incluirconta.cppna linha de compilação (g++ main.cpp -o programa, semconta.cpp). O compilador aceitamain.cppsozinho (ele só precisa da declaração, do.h`), mas o linker não encontra a implementação em lugar nenhum.
"redefinition of class ‘ContaBancaria’" — falta include guard no .h, ou ele foi incluído de um jeito que burlou a proteção (raro, mas acontece com guards mal nomeados, colidindo entre arquivos diferentes que usam o mesmo nome de macro, tipo H_H). Sempre use um nome de macro único, geralmente derivado do nome do arquivo (CONTA_H para conta.h).
Esquecer #include <string> no .h que usa std::string, e só perceber o erro no .cpp que inclui esse .h — cada .h deveria incluir tudo que ele mesmo precisa para compilar sozinho, sem depender de quem o inclui já ter incluído algo antes. Regra prática: um header deve compilar mesmo se for o primeiro include de um arquivo.
Resumo¶
.hdeclara (a "vitrine");.cppimplementa (o "estoque"). Quem usa uma classe/função só precisa incluir o.h.- Vários
.cppsão compilados separadamente e depois linkados — "undefined reference" é erro de link, não de compilação, geralmente por esquecer de incluir um.cppna compilação. - Include guard (
#ifndef/#define/#endif, ou#pragma once) evita declaração duplicada quando o mesmo.hé incluído mais de uma vez. - Cada
.hdeve incluir tudo que precisa para compilar sozinho, independentemente de quem o inclui.