C++ Compilar Vários Arquivos

Na aula anterior, a Ana dividiu a lojinha em calculos.h, calculos.cpp e main.cpp. Agora vem a pergunta prática: como transformar vários arquivos em um programa só? Nesta aula você vai compilar tudo de uma vez, depois compilar cada parte separada e juntar no final, e vai entender por que projetos grandes trabalham assim. No caminho aparece o erro de ligação mais comum do C++, o undefined reference to, e você vai aprender a investigá-lo.

Você vai aprender
  • Como compilar vários .cpp num comando só.
  • O que são as duas fases, compilação e ligação, e o arquivo .o que fica no meio.
  • Como compilar cada arquivo separado com g++ -c e ligar depois.
  • Por que isso deixa projetos grandes muito mais rápidos de recompilar.
  • Como ler e consertar undefined reference to (em português, "referência não definida para").
  • Se a ordem dos arquivos no comando importa.

Todos os comandos desta aula foram executados na máquina fictícia da Ana (usuária ana, computador notebook), com o g++ 15.2, na pasta ~/cpp/loja com os três arquivos da aula Arquivos .h e .cpp. Para relembrar, o calculos.h declara duas funções:

#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);

O calculos.cpp tem o corpo das duas, e o main.cpp as usa para mostrar o total de 4 itens de R$ 2,50 e o total com 10% de desconto.


A ideia: a fábrica de móveis

Uma fábrica de móveis não monta um guarda-roupa inteiro numa bancada só. Uma equipe faz as portas, outra faz as gavetas, outra faz a estrutura. Cada equipe trabalha sozinha, só precisando saber as medidas combinadas (onde vai cada parafuso). No fim, a montagem junta as peças.

Na fábricaNo C++
As medidas combinadaso .h: as declarações
Cada equipe fazendo a sua peçaa compilação: cada .cpp vira um .o, sozinho
A peça pronta, esperando a montagemo arquivo .o (objeto)
A montagem finala ligação (linking): o ligador junta os .o num programa
Se a porta mudar, só a equipe da porta trabalha de novose só o calculos.cpp mudar, só ele é recompilado

1. O jeito simples: todos os .cpp num comando

$ g++ -std=c++23 -Wall -Wextra main.cpp calculos.cpp -o loja
$ ./loja
Total: R$ 10
Com 10% de desconto: R$ 9
main.cpp calculos.cpp
Todos os arquivos .cpp do programa, separados por espaço. O g++ compila cada um e junta no final.
(sem o calculos.h)
O .h não vai no comando: ele já é colado dentro dos .cpp pelo #include.
-o loja
O nome do programa final, como sempre.

A ordem não importa

Para arquivos .cpp (e .o), tanto faz quem vem primeiro. O main não precisa estar no primeiro arquivo:

$ g++ -std=c++23 -Wall -Wextra calculos.cpp main.cpp -o loja && ./loja
Total: R$ 10
Com 10% de desconto: R$ 9

O ligador lê todas as peças antes de montar. (Com bibliotecas, passadas com -l, a ordem importa; isso fica para Usar Bibliotecas Externas.)

Todos os .cpp da pasta: *.cpp

O curinga *.cpp do terminal vira a lista de todos os arquivos terminados em .cpp da pasta:

$ g++ -std=c++23 -Wall -Wextra *.cpp -o loja && ./loja
Total: R$ 10
Com 10% de desconto: R$ 9

É prático, mas pega tudo, inclusive um arquivo de teste esquecido na pasta. Veja o que aconteceu quando a Ana tinha deixado lá um teste.cpp com outro main:

$ g++ -std=c++23 -Wall -Wextra *.cpp -o loja
/usr/bin/x86_64-linux-gnu-ld.bfd: /tmp/ccnTvS9K.o: na função "main":
teste.cpp:(.text+0x0): múltiplas definições de "main"; /tmp/ccY9WJOj.o:main.cpp:(.text+0x0): definido primeiro aqui
collect2: error: ld returned 1 exit status

Um programa tem um main, e agora havia dois (no teste.cpp e no main.cpp). É o mesmo erro "múltiplas definições" da aula anterior. Ela apagou o teste.cpp e tudo voltou a funcionar.

Experimente: o editor do guia faz o mesmo que g++ *.cpp: compila todas as abas .cpp juntas. Rode. Depois acrescente uma terceira função, double calcularFrete(double total), que devolve 0 se o total passar de 50 e 8.9 se não passar: declare no calculos.h, defina no calculos.cpp e mostre o frete no 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';
}
#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;
}

2. As duas fases, uma de cada vez

Quando você passa vários .cpp ao g++, ele faz por trás:

  1. Compila cada .cpp separado, sem olhar os outros, e gera um arquivo objeto (.o) com o código de máquina daquele arquivo. Nesse momento, as chamadas para funções de outros arquivos ficam "em aberto".
  2. Liga os .o: o ligador (ld) junta as peças, preenche cada chamada em aberto com o endereço da função certa e grava o programa.

Dá para fazer as duas fases à mão. A opção -c (de compile, "só compile") para depois da fase 1:

$ g++ -std=c++23 -Wall -Wextra -c calculos.cpp
$ g++ -std=c++23 -Wall -Wextra -c main.cpp
$ ls -l *.o
-rw-rw-r-- 1 ana ana 1608 out 16 21:48 calculos.o
-rw-rw-r-- 1 ana ana 2336 out 16 21:48 main.o
-c
Compila o arquivo e para, sem ligar. Não precisa de -o: o resultado ganha o mesmo nome com .o no lugar de .cpp.
calculos.o, main.o
Arquivos objeto: código de máquina, mas ainda não um programa. Não dá para rodar um .o.
-std=c++23 -Wall -Wextra
As opções de linguagem e de avisos valem na compilação, então vão em cada -c. Use as mesmas em todos os arquivos do projeto.

O comando file confirma o que é um .o:

$ file calculos.o
calculos.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped

ELF é o formato de programas do Linux; relocatable ("realocável") quer dizer "peça para montagem", com os endereços ainda por decidir. Agora a fase 2, a ligação: passe os .o ao g++, sem -c:

$ g++ main.o calculos.o -o loja
$ ./loja
Total: R$ 10
Com 10% de desconto: R$ 9

Para guardar os .o em outra pasta e não misturar com o código, use -o junto com -c:

$ mkdir -p build
$ g++ -std=c++23 -Wall -Wextra -c calculos.cpp -o build/calculos.o

3. Por que separar acelera projetos grandes

A Ana mediu o tempo de cada etapa com o comando time (que mostra quanto um comando demorou; olhe a linha real):

$ time g++ -std=c++23 -Wall -Wextra main.cpp calculos.cpp -o loja

real	0m1,342s           « tudo de uma vez: 1,3 segundo
user	0m1,218s
sys	0m0,123s
$ time g++ -std=c++23 -Wall -Wextra -c main.cpp

real	0m1,189s           « o main.cpp sozinho: quase tudo
user	0m1,103s
sys	0m0,086s
$ time g++ -std=c++23 -Wall -Wextra -c calculos.cpp

real	0m0,026s           « o calculos.cpp sozinho: 26 milésimos
user	0m0,018s
sys	0m0,008s
$ time g++ main.o calculos.o -o loja

real	0m0,084s           « a ligação: 84 milésimos
user	0m0,063s
sys	0m0,021s

O main.cpp demora porque inclui o <iostream> (lembra das 68 mil linhas da aula O Pré-processador?). Se a Ana mudar só o desconto, no calculos.cpp, ela não precisa compilar o main.cpp de novo: o main.o antigo continua valendo.

Mudou só o calculos.cppComandosTempo
Recompilar tudog++ … main.cpp calculos.cpp -o lojacerca de 1,3 s
Recompilar só o que mudoug++ … -c calculos.cpp + g++ main.o calculos.o -o lojacerca de 0,1 s

Com dois arquivos, a diferença é de um segundo. Um projeto profissional tem centenas ou milhares de .cpp, e recompilar tudo pode levar minutos ou horas. Por isso ninguém recompila tudo a cada mudança: compila-se só o que mudou e liga-se de novo.

Se o .h mudar, todos que o incluem precisam ser recompilados

O .h é colado dentro dos .cpp. Se a Ana mudar o calculos.h, tanto o main.o quanto o calculos.o ficam velhos. Lembrar disso à mão é chato e fácil de errar; é exatamente o trabalho que o Make faz por você, e é o assunto da aula seguinte.


4. undefined reference to: a peça que faltou na montagem

A Ana compilou o main.cpp sozinho, esquecendo o calculos.cpp:

$ g++ -std=c++23 -Wall -Wextra main.cpp -o loja
/usr/bin/x86_64-linux-gnu-ld.bfd: /tmp/ccnBpmNV.o: na função "main":
main.cpp:(.text+0x1f): referência não definida para "calcularTotal(double, int)"
/usr/bin/x86_64-linux-gnu-ld.bfd: main.cpp:(.text+0x98): referência não definida para "aplicarDesconto(double, double)"
collect2: error: ld returned 1 exit status
$ echo $?
1
/usr/bin/x86_64-linux-gnu-ld.bfd
Quem fala é o ligador. O main.cpp compilou sem erro: o calculos.h prometeu que as funções existem, e o compilador acreditou.
/tmp/ccnBpmNV.o: na função "main"
O .o temporário do main.cpp; o problema está dentro da função main.
main.cpp:(.text+0x1f)
Um endereço dentro do código de máquina do main.cpp, não um número de linha. Para você, basta saber que é no main.cpp.
referência não definida para "calcularTotal(double, int)"
O main chama calcularTotal(double, int), mas nenhuma peça entregue ao ligador tem o corpo dela. A promessa do .h não foi cumprida.
collect2: error: ld returned 1 exit status
A ligação falhou, e nenhum programa foi gravado.

Como na aula anterior, a mensagem está em português porque vem do ligador, que tem tradução no Ubuntu. É a mensagem que você mais vai encontrar em inglês nos fóruns, então vale conhecer a versão original:

$ LANG=C g++ -std=c++23 -Wall -Wextra main.cpp -o loja
/usr/bin/x86_64-linux-gnu-ld.bfd: /tmp/ccUwwVgg.o: in function `main':
main.cpp:(.text+0x1f): undefined reference to `calcularTotal(double, int)'
/usr/bin/x86_64-linux-gnu-ld.bfd: main.cpp:(.text+0x98): undefined reference to `aplicarDesconto(double, double)'
collect2: error: ld returned 1 exit status

No editor do guia (Clang com o ligador wasm-ld), o mesmo problema aparece como undefined symbol ("símbolo indefinido").

Investigando com nm

O comando nm lista os símbolos (nomes de funções e variáveis) de um .o. A opção -C mostra os nomes do jeito que você escreveu (sem ela, aparecem nomes codificados como _Z13calcularTotaldi):

$ nm -C calculos.o
0000000000000000 T calcularTotal(double, int)
0000000000000020 T aplicarDesconto(double, double)
$ nm -C main.o | grep -i calcular
                 U calcularTotal(double, int)
LetraQuer dizerNa fábrica
To corpo da função está aqui (text, a parte do código)"eu fabrico esta peça"
Uusado aqui, mas não definido (undefined); alguém tem que fornecer"eu preciso desta peça"

Ligar é casar cada U com um T do mesmo nome. Se um U fica sem par, é undefined reference. Ligando só o main.o, o erro é o mesmo:

$ g++ main.o -o loja
/usr/bin/x86_64-linux-gnu-ld.bfd: main.o: na função "main":
main.cpp:(.text+0x1f): referência não definida para "calcularTotal(double, int)"
/usr/bin/x86_64-linux-gnu-ld.bfd: main.cpp:(.text+0x98): referência não definida para "aplicarDesconto(double, double)"
collect2: error: ld returned 1 exit status
"Referência não definida", mas o .cpp está no comando!

Na pasta ~/cpp/errada, a Ana passou os dois arquivos e mesmo assim:

$ g++ -std=c++23 -Wall -Wextra main.cpp calculos.cpp -o loja
/usr/bin/x86_64-linux-gnu-ld.bfd: /tmp/ccT46yI7.o: na função "main":
main.cpp:(.text+0x1f): referência não definida para "calcularTotal(double, int)"
collect2: error: ld returned 1 exit status

O que aconteceu: olhe o calculos.cpp dela:

#include "calculos.h"

double calcularTotal(double preco, double quantidade) {
    return preco * quantidade;
}
…

O .h promete calcularTotal(double, int), mas o .cpp define calcularTotal(double, double). Para o C++, são duas funções diferentes (é a sobrecarga): o main pede a com int, e só existe a com double. O ligador procura pelo nome e pelos tipos dos parâmetros, que aparecem na mensagem: "calcularTotal(double, int)".

Como resolver: deixe a declaração e a definição idênticas. Compare a mensagem com o .cpp, letra por letra.

Lista de suspeitos do undefined reference

SuspeitoComo conferir
Esqueceu um .cpp no comandoo arquivo com o corpo da função está na linha do g++?
Declaração e definição com tipos diferentescompare os tipos da mensagem com a definição
Nome diferente (calcularTotal × calculaTotal)nm -C arquivo.o mostra o nome que foi gerado
A função foi declarada, mas ninguém escreveu o corpoprocure a definição (com {) no projeto
Falta uma biblioteca (-l…)veja Usar Bibliotecas Externas
ErroFaseTem arquivo:linha?Significa
‘x’ was not declared in this scopecompilaçãosimfaltou a declaração (o #include)
undefined reference to / referência não definida paraligaçãonãofaltou a definição (o corpo)
multiple definition of / múltiplas definições deligaçãonãotem definição demais

Formas de compilar: qual usar

FormaExemploQuando usar
Tudo num comandog++ … main.cpp calculos.cpp -o lojaprojetos pequenos, testes rápidos
Curingag++ … *.cpp -o lojauma pasta que só tem o programa (cuidado com sobras)
Separado: -c e depois ligarg++ … -c x.cpp + g++ *.o -o lojaentender o processo; base para o Make
Makemakeprojetos com vários arquivos: recompila só o que mudou, sozinho
CMakecmake --build buildprojetos de verdade, em vários sistemas e IDEs

Referência

ComandoO que faz
g++ a.cpp b.cpp -o progcompila e liga tudo
g++ -c a.cppsó compila: gera a.o
g++ -c a.cpp -o build/a.osó compila, com outro nome ou pasta
g++ a.o b.o -o progsó liga
nm -C a.olista os símbolos (T = definido aqui, U = precisa de fora)
file a.odiz o tipo do arquivo
LANG=C comandoroda o comando com as mensagens em inglês

Exercícios

1. Só compilar

Complete para gerar o estoque.o, sem ligar:

g++ -std=c++23 -Wall -Wextra estoque.cpp

2. Só ligar

Complete para ligar main.o e estoque.o num programa chamado loja:

g++ -o loja

3. Em que fase aparece undefined reference to `calcularTotal(double, int)'?

4. Num projeto com 200 arquivos .cpp, a Ana mudou só o frete.cpp. O que precisa ser feito?

5. O que o U quer dizer em U calcularTotal(double, int), na saída do nm -C main.o?

Missão: provoque o erro de ligação

Missão: no editor não existe "esquecer um arquivo no comando", porque todas as abas .cpp entram. Simule o esquecimento: apague a função aplicarDesconto inteira do calculos.cpp (deixe a declaração no .h) e rode. Repare que o erro não aponta uma linha: é a ligação reclamando da peça que faltou.

#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;
}
#include "calculos.h"

double calcularTotal(double preco, int quantidade) {
    return preco * quantidade;
}

Missão final: cumpra a promessa do .h

Missão: o calculos.h promete duas funções, mas o projeto não liga. Leia a mensagem: qual função falta e com que tipos? Uma delas foi escrita com o tipo errado no calculos.cpp, e a outra nem foi escrita. Conserte só o calculos.cpp (o troco é pago - total). A saída deve ser:

Total: R$ 30
Troco: R$ 20
#include <iostream>
#include "calculos.h"

int main() {
    double total = calcularTotal(7.50, 4);
    std::cout << "Total: R$ " << total << '\n';
    std::cout << "Troco: R$ " << calcularTroco(50, total) << '\n';
}
#pragma once

double calcularTotal(double preco, int quantidade);
double calcularTroco(double pago, double total);
#include "calculos.h"

double calcularTotal(double preco, double quantidade) {
    return preco * quantidade;
}
#include "calculos.h"

double calcularTotal(double preco, int quantidade) {
    return preco * quantidade;
}

double calcularTroco(double pago, double total) {
    return pago - total;
}

Resumo

  • Para um programa de vários arquivos, passe todos os .cpp ao g++ (nunca o .h). A ordem dos .cpp não importa.
  • Por trás, são duas fases: compilação (cada .cpp vira um .o, sozinho) e ligação (o ld junta os .o).
  • g++ -c x.cpp só compila; g++ a.o b.o -o prog só liga. Assim, quando um arquivo muda, só ele é recompilado.
  • undefined reference to ("referência não definida para") é do ligador: uma função foi declarada e usada, mas o corpo não chegou. Suspeitos: .cpp esquecido, tipos diferentes entre .h e .cpp, nome diferente, biblioteca faltando.
  • nm -C arquivo.o mostra o que cada peça fornece (T) e o que ela precisa (U).