C++ Arquivos .h e .cpp
Enquanto o programa tem 50 linhas, um arquivo só dá conta. Mas o programa da lojinha da Ana cresceu: tem funções de cálculo, de estoque, de relatório… e rolar um arquivo de mil linhas para achar uma função é cansativo. Em C++, a solução é dividir o código em vários arquivos, e cada parte ganha dois: um .h, que diz o que existe, e um .cpp, que diz como funciona. Nesta aula você vai montar essa divisão, entender por que ela é assim e conhecer os dois erros que todo mundo encontra no caminho.
- A diferença entre declarar e definir uma função.
- O que vai no arquivo
.h(cabeçalho) e o que vai no.cpp. - Como montar um projeto com
calculos.h,calculos.cppemain.cpp. - Por que um
.hincluído duas vezes dáredefinition, e como os include guards e o#pragma onceresolvem. - Por que uma função definida no
.hdá "múltiplas definições" (multiple definition of), e como oinlineresolve.
Todos os programas desta aula foram compilados com o g++ 15.2 na máquina fictícia da Ana (usuária ana, computador notebook), com o comando padrão do guia (-std=c++23 -Wall -Wextra). Você precisa conhecer Funções e o #include.
A ideia: o cardápio e a cozinha
Num restaurante, o cardápio diz o que existe: "Lasanha — R$ 30". Não diz como a lasanha é feita. A receita fica na cozinha. O cliente só precisa do cardápio para pedir; o cozinheiro só precisa da receita para preparar.
| No restaurante | No C++ | Arquivo | Exemplo |
|---|---|---|---|
| Cardápio: o nome do prato e o preço | declaração: nome, parâmetros e tipo de retorno | calculos.h (header, cabeçalho) | double calcularTotal(double preco, int quantidade); |
| Receita: o passo a passo | definição: a declaração com o corpo { … } | calculos.cpp | double calcularTotal(…) { return preco * quantidade; } |
| Cliente que lê o cardápio | quem chama a função | main.cpp (faz #include "calculos.h") | calcularTotal(2.50, 4) |
O .h vem de header ("cabeçalho"), porque ele é incluído no topo dos outros arquivos. Você também vai ver .hpp (um "h de C++"); funciona igual, é só costume do projeto.
1. Declarar × definir
Você viu em Funções que o compilador lê o arquivo de cima para baixo e só aceita chamar uma função que ele já conhece. Ele não precisa do corpo para isso: basta saber o nome, o que entra e o que sai. Essa linha sem corpo, terminada em ;, é a declaração (também chamada de protótipo):
double calcularTotal(double preco, int quantidade); // declaração: "existe"
double calcularTotal(double preco, int quantidade) { // definição: "é assim"
return preco * quantidade;
}
- declaração
- Termina com
;. Pode ser repetida quantas vezes quiser, em quantos arquivos quiser: é só um aviso de que a função existe em algum lugar. - definição
- Tem o corpo
{ … }. Cada função comum precisa ter exatamente uma definição no programa inteiro. Essa é a regra mais importante desta aula (o nome oficial é regra da definição única, ODR, de One Definition Rule).
Com isso, dá para entender a divisão: o .h guarda as declarações (que podem aparecer em vários arquivos), e o .cpp guarda as definições (que precisam existir uma vez só).
2. Um projeto em três arquivos
A Ana vai tirar as contas da lojinha do main.cpp. Ela cria uma pasta ~/cpp/loja com três arquivos.
O cabeçalho: calculos.h
#pragma once // Declarações: o "cardápio" do que existe em calculos.cpp double calcularTotal(double preco, int quantidade); double aplicarDesconto(double valor, double porcento);
- #pragma once
- "Inclua este arquivo uma vez só". Toda
.hdeve começar com ele (ou com os include guards da seção 3). - as duas linhas de baixo
- Só declarações, com
;e sem corpo. É tudo o que quem usa a lojinha precisa saber.
A implementação: calculos.cpp
#include "calculos.h"
// Definições: o corpo de cada função
double calcularTotal(double preco, int quantidade) {
return preco * quantidade;
}
double aplicarDesconto(double valor, double porcento) {
return valor - valor * porcento / 100;
}
- #include "calculos.h"
- O
.cppinclui o próprio cabeçalho. Assim, se a declaração e a definição ficarem diferentes (umintnum lugar edoubleno outro), o compilador tem a chance de perceber. É costume que essa seja a primeira linha. - as funções
- As mesmas da declaração, agora com o corpo.
Quem usa: main.cpp
#include <iostream>
#include "calculos.h"
int main() {
double total = calcularTotal(2.50, 4);
std::cout << "Total: R$ " << total << '\n';
std::cout << "Com 10% de desconto: R$ " << aplicarDesconto(total, 10) << '\n';
}
O main.cpp não sabe como o total é calculado, e nem precisa: o #include "calculos.h" cola as declarações no topo, e isso basta para o compilador aceitar as chamadas. Na hora de compilar, você passa os dois .cpp para o g++ (o .h não entra no comando, porque ele já é colado pelo #include):
$ cd ~/cpp/loja $ g++ -std=c++23 -Wall -Wextra main.cpp calculos.cpp -o loja $ ./loja Total: R$ 10 Com 10% de desconto: R$ 9
Os detalhes desse comando, e como compilar cada .cpp separado, ficam para a próxima aula, Compilar Vários Arquivos. Aqui o foco é o que vai dentro de cada arquivo.
Para ver o que o compilador recebeu ao compilar o main.cpp, use o raio-x da aula do pré-processador (-E -P) e olhe o final:
$ g++ -std=c++23 -E -P main.cpp | tail -8
double calcularTotal(double preco, int quantidade); « veio do calculos.h
double aplicarDesconto(double valor, double porcento);
int main() {
double total = calcularTotal(2.50, 4);
std::cout << "Total: R$ " << total << '\n';
std::cout << "Com 10% de desconto: R$ " << aplicarDesconto(total, 10) << '\n';
}
Só as declarações chegaram; os corpos ficaram no calculos.cpp, que é compilado à parte.
A Ana esqueceu o #include "calculos.h" num arquivo novo:
#include <iostream>
int main() {
double total = calcularTotal(2.50, 4);
std::cout << "Total: R$ " << total << '\n';
}
$ g++ -std=c++23 -Wall -Wextra sem-include.cpp calculos.cpp -o loja2
sem-include.cpp: In function ‘int main()’:
sem-include.cpp:4:20: error: ‘calcularTotal’ was not declared in this scope
4 | double total = calcularTotal(2.50, 4);
| ^~~~~~~~~~~~~
O que significa: "‘calcularTotal’ não foi declarada neste escopo". O compilador lê cada .cpp sozinho, sem olhar os outros. Passar o calculos.cpp no mesmo comando não ajuda: ao compilar o sem-include.cpp, ninguém contou a ele que a função existe. (No editor do guia, o Clang diz use of undeclared identifier 'calcularTotal', "uso de nome não declarado".)
Como resolver: acrescente #include "calculos.h" no topo do arquivo que chama a função.
Experimente: as abas abaixo são os três arquivos da lojinha, e o editor compila todos os .cpp juntos. Rode. Depois acrescente uma função double calcularTroco(double pago, double total): a declaração no calculos.h, a definição no calculos.cpp e uma chamada no main.cpp, por exemplo calcularTroco(20, total). Teste também o que acontece se você esquecer a declaração no .h.
#include <iostream>
#include "calculos.h"
int main() {
double total = calcularTotal(2.50, 4);
std::cout << "Total: R$ " << total << '\n';
std::cout << "Com 10% de desconto: R$ " << aplicarDesconto(total, 10) << '\n';
}
#pragma once double calcularTotal(double preco, int quantidade); double aplicarDesconto(double valor, double porcento);
#include "calculos.h"
double calcularTotal(double preco, int quantidade) {
return preco * quantidade;
}
double aplicarDesconto(double valor, double porcento) {
return valor - valor * porcento / 100;
}
3. O mesmo .h incluído duas vezes
Num projeto de verdade, um cabeçalho inclui outro. A Ana criou um produto.h com o tipo Produto (uma struct) e um estoque.h que também precisa dele. Repare: os dois arquivos estão sem #pragma once.
#include <string>
struct Produto {
std::string nome;
double preco;
};
#include "produto.h" int contarEmFalta(const Produto produtos[], int quantos);
#include <iostream>
#include "produto.h"
#include "estoque.h"
int main() {
Produto p{"Pão", 0.75};
std::cout << p.nome << ": R$ " << p.preco << '\n';
}
$ g++ -std=c++23 -Wall -Wextra main.cpp -o main
In file included from estoque.h:1,
from main.cpp:3:
produto.h:3:8: error: redefinition of ‘struct Produto’
3 | struct Produto {
| ^~~~~~~
In file included from main.cpp:2:
produto.h:3:8: note: previous definition of ‘struct Produto’
3 | struct Produto {
| ^~~~~~~
Leia a mensagem de cima para baixo:
- In file included from estoque.h:1, from main.cpp:3:
- "No arquivo incluído a partir de…": o caminho da colagem. O
main.cpp(linha 3) incluiu oestoque.h, que (linha 1) incluiu oproduto.h. - error: redefinition of ‘struct Produto’
- "Redefinição da struct Produto": é a segunda vez que ela aparece.
- note: previous definition
- A primeira vez foi quando o
main.cpp(linha 2) incluiu oproduto.hdiretamente.
O produto.h foi colado duas vezes no main.cpp, e uma struct só pode ser definida uma vez em cada .cpp. Ninguém fez nada de errado: é natural o main.cpp incluir o que usa. Quem precisa se proteger é o cabeçalho.
No editor do guia, o Clang diz redefinition of 'Produto' e ainda dá a dica: unguarded header; consider using #ifdef guards or #pragma once ("cabeçalho sem guarda; considere usar guardas com #ifdef ou #pragma once"). É exatamente o conserto.
Proteção 1: include guards
A proteção clássica usa o #ifndef que você viu em O Pré-processador. O nome em inglês é include guard, "guarda do include":
#ifndef PRODUTO_H
#define PRODUTO_H
#include <string>
struct Produto {
std::string nome;
double preco;
};
#endif
- #ifndef PRODUTO_H
- "Se
PRODUTO_Hainda não foi definido…". Na primeira colagem, não foi: o conteúdo entra. - #define PRODUTO_H
- …então defina agora. É uma marca de "já passei por aqui".
- o meio
- O conteúdo de verdade do cabeçalho.
- #endif
- Fim do bloco. Na segunda colagem,
PRODUTO_Hjá existe, e o pré-processador pula tudo até aqui.
$ g++ -std=c++23 -Wall -Wextra main.cpp -o main && ./main Pão: R$ 0.75
O nome da marca é livre, mas precisa ser único no projeto: o costume é o nome do arquivo em maiúsculas, com _ no lugar do ponto (PRODUTO_H, CALCULOS_H). Se dois arquivos usarem a mesma marca, o segundo some sem aviso nenhum.
Proteção 2: #pragma once
A forma curta é uma linha só no topo:
#pragma once
#include <string>
struct Produto {
std::string nome;
double preco;
};
| Include guards | #pragma once | |
|---|---|---|
| Linhas | três (#ifndef, #define, #endif) | uma |
| Está no padrão do C++? | sim, funciona em qualquer compilador | não oficialmente, mas g++, Clang e Visual C++ aceitam todos |
| Risco | dois arquivos com a mesma marca (copiar e colar um .h e esquecer de trocar) | casos raros com o mesmo arquivo em dois caminhos (cópias, links) |
| Onde você vai ver | código antigo, bibliotecas que rodam em todo lugar | a maioria dos projetos novos |
Qual usar: num projeto seu, #pragma once. Se o projeto já usa include guards, siga o projeto. O importante é que todo .h tenha uma das duas.
4. O que não pode ir no .h
A Ana pensou: "para que dois arquivos? Vou pôr a função inteira no .h". E fez assim, com #pragma once e tudo:
#pragma once
double calcularTotal(double preco, int quantidade) {
return preco * quantidade;
}
Enquanto só o main.cpp incluía o calculos.h, funcionou. Aí ela criou um relatorio.cpp que também usa a função:
#include <iostream>
#include "calculos.h"
void relatorio() {
std::cout << "Relatório: R$ " << calcularTotal(3.0, 2) << '\n';
}
#include <iostream>
#include "calculos.h"
void relatorio();
int main() {
std::cout << "Total: R$ " << calcularTotal(2.50, 4) << '\n';
relatorio();
}
$ g++ -std=c++23 -Wall -Wextra main.cpp relatorio.cpp -o loja /usr/bin/x86_64-linux-gnu-ld.bfd: /tmp/cc4cmEdH.o: na função "calcularTotal(double, int)": relatorio.cpp:(.text+0x0): múltiplas definições de "calcularTotal(double, int)"; /tmp/ccXMYQZb.o:main.cpp:(.text+0x0): definido primeiro aqui collect2: error: ld returned 1 exit status $ echo $? 1
Esse erro é diferente de todos os que você viu até agora. Ele não tem arquivo:linha:coluna, porque não vem do compilador: os dois .cpp compilaram sem problema. Quem reclamou foi o ligador (linker), o programa ld, que junta as partes no final:
- /usr/bin/x86_64-linux-gnu-ld.bfd
- O ligador do Linux (o
ld). Quando a primeira palavra de uma mensagem é ele, o problema está em juntar as partes, não no código de um arquivo. - /tmp/cc4cmEdH.o
- Um arquivo temporário: o
relatorio.cppjá compilado (código de máquina). O nome muda a cada compilação. - múltiplas definições de "calcularTotal(double, int)"
- A função tem corpo em dois lugares: no
relatorio.cppe nomain.cpp(definido primeiro aqui). Isso quebra a regra da definição única. - collect2: error: ld returned 1 exit status
- "O ld terminou com o código 1", ou seja, falhou. O
collect2é o ajudante do g++ que chama o ligador.
O ld vem de outro pacote (o binutils), que tem tradução instalada no Ubuntu, e o sistema da Ana está em português. As mensagens do próprio g++ continuam em inglês. Em tutoriais e fóruns você vai encontrar a versão em inglês, que é esta (com LANG=C na frente, o comando roda em inglês):
$ LANG=C g++ -std=c++23 -Wall -Wextra main.cpp relatorio.cpp -o loja /usr/bin/x86_64-linux-gnu-ld.bfd: /tmp/ccg015ih.o: in function `calcularTotal(double, int)': relatorio.cpp:(.text+0x0): multiple definition of `calcularTotal(double, int)'; /tmp/cc5RroGG.o:main.cpp:(.text+0x0): first defined here collect2: error: ld returned 1 exit status
No editor do guia, que usa o Clang e o ligador wasm-ld, a mesma situação aparece como duplicate symbol ("símbolo duplicado").
"Mas eu pus o #pragma once!"
Essa é a parte que confunde. O #pragma once e os include guards protegem contra colar o .h duas vezes dentro do mesmo .cpp. Cada .cpp é compilado sozinho, do zero, sem lembrar dos outros:
| Arquivo compilado | Colou o calculos.h? | Resultado |
|---|---|---|
main.cpp | sim, uma vez | uma definição de calcularTotal no main.o |
relatorio.cpp | sim, uma vez (para ele, é a primeira) | outra definição no relatorio.o |
| Ligação | duas definições: erro |
Solução 1: declaração no .h, definição no .cpp
É a divisão da seção 2: o calculos.h fica só com double calcularTotal(double preco, int quantidade);, e o corpo vai para um calculos.cpp. Cada .cpp que inclui o cabeçalho recebe só a declaração, e o corpo existe uma vez. É o jeito normal para a maioria das funções.
Solução 2: inline
Para funções pequenas, que você quer deixar inteiras no .h, escreva inline na frente:
#pragma once
inline double calcularTotal(double preco, int quantidade) {
return preco * quantidade;
}
$ g++ -std=c++23 -Wall -Wextra main.cpp relatorio.cpp -o loja && ./loja Total: R$ 10 Relatório: R$ 6
inline quer dizer, em C++ moderno: "esta definição vai aparecer em vários .cpp; são todas iguais, guarde uma só". O ligador então aceita as cópias. (O nome vem de uma ideia antiga, "copie o corpo no lugar da chamada", que hoje o compilador decide sozinho, com ou sem a palavra.)
.h têm o mesmo problema
Um int visitas = 0; num cabeçalho incluído por dois .cpp dá o mesmo erro do ligador:
$ g++ -std=c++23 -Wall -Wextra main.cpp a.cpp -o visitas /usr/bin/x86_64-linux-gnu-ld.bfd: /tmp/cc4flNA3.o:(.bss+0x0): múltiplas definições de "visitas"; /tmp/cczPoZPn.o:(.bss+0x0): definido primeiro aqui collect2: error: ld returned 1 exit status
A solução é a mesma: inline int visitas = 0; C++17. Melhor ainda é evitar variáveis globais que mudam de valor; passe os dados por parâmetros. Constantes como constexpr double TAXA = 0.1; podem ir no .h sem problema.
5. O que vai em cada arquivo
| Coisa | No .h | No .cpp |
|---|---|---|
Declaração de função (double f(int);) | sim | pode, mas costuma vir do .h |
Definição de função comum (double f(int x) { … }) | não (múltiplas definições) | sim, em um só |
Função inline ou constexpr com corpo | sim | se só aquele arquivo usa |
struct, class, enum class | sim (protegido por #pragma once) | se só aquele arquivo usa |
Constantes (constexpr, const) | sim | sim |
Variável global comum (int x = 0;) | não (use inline) | sim |
| Templates | sim (o corpo inteiro) | raramente |
using namespace std; | não (vazaria para todos que incluem; veja Namespaces) | se quiser, depois dos #include |
Boas práticas
- Todo
.hcom#pragma once(ou include guards) na primeira linha. - Um par por assunto:
calculos.h+calculos.cpp,estoque.h+estoque.cpp. O nome igual ajuda a achar as coisas. - O
.cppinclui o próprio.hprimeiro. Se o cabeçalho esquecer algum#includede que precisa (como o<string>doproduto.h), o erro aparece logo. - Cada
.hinclui o que ele mesmo usa. Oproduto.husastd::string, então inclui<string>, sem contar que quem o incluir já tenha feito isso. - Nunca inclua um
.cpp(#include "calculos.cpp"). Funciona por acaso com um arquivo só e dá "múltiplas definições" assim que o projeto cresce.
Referência
| Mensagem | Quem diz | Causa | Conserto |
|---|---|---|---|
‘f’ was not declared in this scope | compilador | o .cpp chama f sem incluir o .h que a declara | #include "f.h" |
redefinition of ‘struct X’ | compilador | o mesmo .h colado duas vezes no mesmo .cpp | #pragma once ou include guards |
múltiplas definições de / multiple definition of | ligador (ld) | corpo de função (ou variável global) num .h incluído por vários .cpp | mover o corpo para o .cpp ou usar inline |
referência não definida para / undefined reference to | ligador | a declaração existe, mas o .cpp com o corpo não foi compilado junto | veja Compilar Vários Arquivos |
Exercícios
1. A declaração
Complete a declaração (para o .h) da função double media(double a, double b):
2. A guarda
Complete a primeira linha do include guard do arquivo estoque.h:
#define ESTOQUE_H
…
#endif
3. Um .h com #pragma once tem o corpo de uma função comum (sem inline) e é incluído por main.cpp e loja.cpp. O que acontece ao compilar os dois juntos?
4. Qual destas linhas não deve ir num .h incluído por vários .cpp?
Missão: conserte a lojinha que não liga
Missão: a Ana pôs o corpo de calcularTotal no calculos.h, e os dois .cpp o incluem. Rode e veja o erro do ligador. Conserte de um dos dois jeitos da aula (o inline, ou mover o corpo para um arquivo novo… mas aqui só existem estas abas, então o caminho mais curto é o inline). A saída deve ser:
Total: R$ 10 Relatório: R$ 6
#include <iostream>
#include "calculos.h"
void relatorio();
int main() {
std::cout << "Total: R$ " << calcularTotal(2.50, 4) << '\n';
relatorio();
}
#pragma once
double calcularTotal(double preco, int quantidade) {
return preco * quantidade;
}
#include <iostream>
#include "calculos.h"
void relatorio() {
std::cout << "Relatório: R$ " << calcularTotal(3.0, 2) << '\n';
}
#pragma once
inline double calcularTotal(double preco, int quantidade) {
return preco * quantidade;
}
Missão final: proteja o cabeçalho
Missão: o produto.h está sem proteção e chega duas vezes ao main.cpp (uma direta, outra pelo estoque.h). Rode e leia o redefinition. Depois proteja o produto.h com include guards (#ifndef/#define/#endif), sem mexer nas outras abas. A saída deve ser:
Pão: R$ 0.75 Em falta: 1
#include <iostream>
#include "produto.h"
#include "estoque.h"
int main() {
Produto p{"Pão", 0.75, 10};
std::cout << p.nome << ": R$ " << p.preco << '\n';
Produto lista[] = {{"Leite", 4.5, 0}, {"Café", 12.9, 3}};
std::cout << "Em falta: " << contarEmFalta(lista, 2) << '\n';
}
#include <string>
struct Produto {
std::string nome;
double preco;
int quantidade;
};
#pragma once #include "produto.h" int contarEmFalta(const Produto produtos[], int quantos);
#include "estoque.h"
int contarEmFalta(const Produto produtos[], int quantos) {
int falta = 0;
for (int i = 0; i < quantos; ++i) {
if (produtos[i].quantidade == 0) {
++falta;
}
}
return falta;
}
#ifndef PRODUTO_H
#define PRODUTO_H
#include <string>
struct Produto {
std::string nome;
double preco;
int quantidade;
};
#endif
Resumo
- Declaração (sem corpo, com
;) diz que algo existe e pode se repetir. Definição (com corpo) diz como funciona e deve existir uma vez no programa. - O
.hé o cardápio (declarações, structs, constantes); o.cppé a cozinha (as definições). Quem usa faz#include "x.h". - Compile todos os
.cppjuntos; o.hnão entra no comando. - Todo
.hcomeça com#pragma onceou com include guards, senão um.hcolado duas vezes no mesmo.cppdáredefinition. - Essas proteções valem dentro de um
.cpp. Corpo de função comum no.h, incluído por vários.cpp, dá "múltiplas definições" (multiple definition of) no ligador. Mova o corpo para o.cppou useinline.