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.
- Como compilar vários
.cppnum comando só. - O que são as duas fases, compilação e ligação, e o arquivo
.oque fica no meio. - Como compilar cada arquivo separado com
g++ -ce 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ábrica | No C++ |
|---|---|
| As medidas combinadas | o .h: as declarações |
| Cada equipe fazendo a sua peça | a compilação: cada .cpp vira um .o, sozinho |
| A peça pronta, esperando a montagem | o arquivo .o (objeto) |
| A montagem final | a ligação (linking): o ligador junta os .o num programa |
| Se a porta mudar, só a equipe da porta trabalha de novo | se 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
.cppdo programa, separados por espaço. O g++ compila cada um e junta no final. - (sem o calculos.h)
- O
.hnão vai no comando: ele já é colado dentro dos.cpppelo#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:
- Compila cada
.cppseparado, 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". - 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.ono 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.cpp | Comandos | Tempo |
|---|---|---|
| Recompilar tudo | g++ … main.cpp calculos.cpp -o loja | cerca de 1,3 s |
| Recompilar só o que mudou | g++ … -c calculos.cpp + g++ main.o calculos.o -o loja | cerca 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.
.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.cppcompilou sem erro: ocalculos.hprometeu que as funções existem, e o compilador acreditou. - /tmp/ccnBpmNV.o: na função "main"
- O
.otemporário domain.cpp; o problema está dentro da funçãomain. - 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 é nomain.cpp. - referência não definida para "calcularTotal(double, int)"
- O
mainchamacalcularTotal(double, int), mas nenhuma peça entregue ao ligador tem o corpo dela. A promessa do.hnã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)
| Letra | Quer dizer | Na fábrica |
|---|---|---|
T | o corpo da função está aqui (text, a parte do código) | "eu fabrico esta peça" |
U | usado 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
.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
| Suspeito | Como conferir |
|---|---|
Esqueceu um .cpp no comando | o arquivo com o corpo da função está na linha do g++? |
| Declaração e definição com tipos diferentes | compare 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 corpo | procure a definição (com {) no projeto |
Falta uma biblioteca (-l…) | veja Usar Bibliotecas Externas |
| Erro | Fase | Tem arquivo:linha? | Significa |
|---|---|---|---|
‘x’ was not declared in this scope | compilação | sim | faltou a declaração (o #include) |
undefined reference to / referência não definida para | ligação | não | faltou a definição (o corpo) |
multiple definition of / múltiplas definições de | ligação | não | tem definição demais |
Formas de compilar: qual usar
| Forma | Exemplo | Quando usar |
|---|---|---|
| Tudo num comando | g++ … main.cpp calculos.cpp -o loja | projetos pequenos, testes rápidos |
| Curinga | g++ … *.cpp -o loja | uma pasta que só tem o programa (cuidado com sobras) |
Separado: -c e depois ligar | g++ … -c x.cpp + g++ *.o -o loja | entender o processo; base para o Make |
| Make | make | projetos com vários arquivos: recompila só o que mudou, sozinho |
| CMake | cmake --build build | projetos de verdade, em vários sistemas e IDEs |
Referência
| Comando | O que faz |
|---|---|
g++ a.cpp b.cpp -o prog | compila e liga tudo |
g++ -c a.cpp | só compila: gera a.o |
g++ -c a.cpp -o build/a.o | só compila, com outro nome ou pasta |
g++ a.o b.o -o prog | só liga |
nm -C a.o | lista os símbolos (T = definido aqui, U = precisa de fora) |
file a.o | diz o tipo do arquivo |
LANG=C comando | roda o comando com as mensagens em inglês |
Exercícios
1. Só compilar
Complete para gerar o estoque.o, sem ligar:
2. Só ligar
Complete para ligar main.o e estoque.o num programa chamado 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
.cppao g++ (nunca o.h). A ordem dos.cppnão importa. - Por trás, são duas fases: compilação (cada
.cppvira um.o, sozinho) e ligação (oldjunta os.o). g++ -c x.cppsó compila;g++ a.o b.o -o progsó 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:.cppesquecido, tipos diferentes entre.he.cpp, nome diferente, biblioteca faltando.nm -C arquivo.omostra o que cada peça fornece (T) e o que ela precisa (U).