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.

Você vai aprender
  • 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.cpp e main.cpp.
  • Por que um .h incluído duas vezes dá redefinition, e como os include guards e o #pragma once resolvem.
  • Por que uma função definida no .h dá "múltiplas definições" (multiple definition of), e como o inline resolve.

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 restauranteNo C++ArquivoExemplo
Cardápio: o nome do prato e o preçodeclaração: nome, parâmetros e tipo de retornocalculos.h (header, cabeçalho)double calcularTotal(double preco, int quantidade);
Receita: o passo a passodefinição: a declaração com o corpo { … }calculos.cppdouble calcularTotal(…) { return preco * quantidade; }
Cliente que lê o cardápioquem chama a funçãomain.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 .h deve 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 .cpp inclui o próprio cabeçalho. Assim, se a declaração e a definição ficarem diferentes (um int num lugar e double no 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.

"‘calcularTotal’ was not declared in this scope"

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 o estoque.h, que (linha 1) incluiu o produto.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 o produto.h diretamente.

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_H ainda 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_H já 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
Linhastrês (#ifndef, #define, #endif)uma
Está no padrão do C++?sim, funciona em qualquer compiladornão oficialmente, mas g++, Clang e Visual C++ aceitam todos
Riscodois 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 vercódigo antigo, bibliotecas que rodam em todo lugara 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.cpp já 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.cpp e no main.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.
Por que esta mensagem está em português?

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 compiladoColou o calculos.h?Resultado
main.cppsim, uma vezuma definição de calcularTotal no main.o
relatorio.cppsim, uma vez (para ele, é a primeira)outra definição no relatorio.o
Ligaçãoduas 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.)

Variáveis globais no .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

CoisaNo .hNo .cpp
Declaração de função (double f(int);)simpode, 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 corposimse só aquele arquivo usa
struct, class, enum classsim (protegido por #pragma once)se só aquele arquivo usa
Constantes (constexpr, const)simsim
Variável global comum (int x = 0;)não (use inline)sim
Templatessim (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 .h com #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 .cpp inclui o próprio .h primeiro. Se o cabeçalho esquecer algum #include de que precisa (como o <string> do produto.h), o erro aparece logo.
  • Cada .h inclui o que ele mesmo usa. O produto.h usa std::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

MensagemQuem dizCausaConserto
‘f’ was not declared in this scopecompiladoro .cpp chama f sem incluir o .h que a declara#include "f.h"
redefinition of ‘struct X’compiladoro mesmo .h colado duas vezes no mesmo .cpp#pragma once ou include guards
múltiplas definições de / multiple definition ofligador (ld)corpo de função (ou variável global) num .h incluído por vários .cppmover o corpo para o .cpp ou usar inline
referência não definida para / undefined reference toligadora declaração existe, mas o .cpp com o corpo não foi compilado juntoveja 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):

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 .cpp juntos; o .h não entra no comando.
  • Todo .h começa com #pragma once ou com include guards, senão um .h colado duas vezes no mesmo .cpp dá 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 .cpp ou use inline.