C Vários Arquivos
Na aula Arquivos .h e .c, a Ana dividiu um programa em vários arquivos e compilou tudo com um comando só. Agora você vai olhar por dentro desse comando: o gcc compila cada .c separadamente, gera um pedaço de programa para cada um (um arquivo .o) e, no fim, um outro programa, o ligador, junta os pedaços. Você vai fazer essas etapas uma de cada vez, espiar dentro de um .o e aprender a ler os erros do ligador, que são diferentes dos erros do compilador e assustam muita gente.
- Como compilar vários arquivos juntos:
gcc main.c util.c -o app. - Como compilar cada arquivo separado com
gcc -c, gerando arquivos.o, e depois ligar os.o. - O que há dentro de um
.o, com o comandonm. - Por que separar as etapas economiza tempo em projetos grandes.
- A ler os erros do ligador:
referência não definida paraemúltiplas definições de(em inglês,undefined reference toemultiple definition of).
Todos os comandos desta aula rodaram na máquina fictícia da Ana (usuária ana, computador notebook), na pasta ~/c/loja, com o gcc 15.2. O sistema da Ana está em português, e por isso as mensagens do ligador saem em português; as do compilador continuam em inglês.
A ideia: a montadora de bicicletas
Uma bicicleta não é feita inteira num lugar só. Uma fábrica faz as rodas, outra faz o quadro, outra faz os freios. Cada fábrica trabalha sozinha, sem ver as outras: ela só sabe que a roda tem de ter um encaixe de tal tamanho. No fim, a montadora recebe as peças e as encaixa. Se faltar uma peça, ou se chegarem dois guidões, quem percebe é a montadora, e não as fábricas.
| Bicicleta | Programa em C |
|---|---|
| A especificação do encaixe | o .h, com os protótipos |
| Cada fábrica | o compilador, trabalhando num .c por vez |
| Cada peça pronta | um arquivo objeto .o (código de máquina, ainda incompleto) |
| A montadora | o ligador (linker, o programa ld) |
| A bicicleta montada | o executável, o programa que roda |
Por isso existem dois tipos de erro. O compilador reclama do que está errado dentro de um .c (um ; faltando, um tipo errado). O ligador reclama do que está errado no conjunto (uma função que ninguém definiu, ou que dois definiram).
1. O jeito simples: todos os .c num comando
O projeto da Ana tem três arquivos. O util.h promete duas funções:
#ifndef UTIL_H #define UTIL_H int total_compra(int preco, int quantidade); void linha(void); #endif
O util.c as define:
#include <stdio.h>
#include "util.h"
int total_compra(int preco, int quantidade) {
return preco * quantidade;
}
void linha(void) {
printf("--------------------\n");
}
E o main.c as usa:
#include <stdio.h>
#include "util.h"
int main(void) {
linha();
printf("Lojinha da Ana\n");
linha();
printf("3 cadernos: R$ %d\n", total_compra(12, 3));
printf("5 lápis: R$ %d\n", total_compra(2, 5));
linha();
return 0;
}
$ ls main.c util.c util.h $ gcc -std=c23 -Wall -Wextra main.c util.c -o app $ ./app -------------------- Lojinha da Ana -------------------- 3 cadernos: R$ 36 5 lápis: R$ 10 --------------------
- main.c util.c
- Todos os
.cdo programa. O.hnão entra no comando: ele chega pelo#include. - -o app
- O nome do executável. Pode ser qualquer um.
A ordem dos arquivos não importa: gcc … util.c main.c -o app dá o mesmo programa. E quando todos os .c da pasta fazem parte do programa, o Bash monta a lista para você com *.c (o asterisco quer dizer "todos os arquivos terminados em .c"):
$ gcc -std=c23 -Wall -Wextra *.c -o app
Experimente: o editor do guia faz o mesmo que o comando acima: compila todas as abas .c juntas. Rode. Depois acrescente ao util uma função int com_desconto(int valor), que devolve o valor menos 10%: protótipo no util.h, definição no util.c, e use-a no main.c.
#include <stdio.h>
#include "util.h"
int main(void) {
linha();
printf("Lojinha da Ana\n");
linha();
printf("3 cadernos: R$ %d\n", total_compra(12, 3));
printf("5 lápis: R$ %d\n", total_compra(2, 5));
linha();
return 0;
}
#ifndef UTIL_H #define UTIL_H int total_compra(int preco, int quantidade); void linha(void); #endif
#include <stdio.h>
#include "util.h"
int total_compra(int preco, int quantidade) {
return preco * quantidade;
}
void linha(void) {
printf("--------------------\n");
}
2. As duas etapas, uma de cada vez
Por trás do comando único, o gcc fez três coisas: compilou o main.c num arquivo temporário, compilou o util.c em outro, e chamou o ligador para juntar os dois. Os arquivos temporários foram apagados no fim. Você pode fazer cada etapa à mão.
Etapa 1: compilar, com -c
$ gcc -std=c23 -Wall -Wextra -c main.c $ gcc -std=c23 -Wall -Wextra -c util.c $ ls main.c main.o util.c util.h util.o
- -c
- "Só compile" (compile): pare antes de ligar. Para cada
.c, sai um.ocom o mesmo nome. - main.o
- Um arquivo objeto (object file): o código de máquina do
main.c, mas ainda com "buracos" onde ele chama funções de outros arquivos.
Também dá para compilar vários de uma vez (gcc -c main.c util.c gera os dois .o) ou escolher o nome do .o com -o (gcc -c util.c -o ferramentas.o). As opções de aviso (-Wall -Wextra) e de padrão (-std=c23) vão nesta etapa, porque é aqui que o código C é lido.
O .o ainda não é um programa. O comando file diz o que ele é, e tentar rodá-lo não funciona:
$ file util.o util.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped $ ./util.o bash: ./util.o: Permissão negada
"relocatable" quer dizer "relocável": um pedaço de código que ainda vai ser encaixado num lugar definitivo. Um executável pronto apareceria como ELF 64-bit LSB pie executable.
Etapa 2: ligar os .o
$ gcc main.o util.o -o app $ ./app | tail -3 3 cadernos: R$ 36 5 lápis: R$ 10 --------------------
Quando você passa arquivos .o, o gcc não compila nada: ele só chama o ligador. O ligador encaixa as peças, junta a biblioteca C (onde moram o printf e companhia) e grava o executável app.
| Etapa | Comando | Quem trabalha | Entra | Sai |
|---|---|---|---|---|
| Compilar | gcc -c main.c | pré-processador + compilador | um .c (e os .h que ele inclui) | um .o |
| Ligar | gcc main.o util.o -o app | ligador (ld) | todos os .o + bibliotecas | o executável |
| As duas | gcc main.c util.c -o app | todos | todos os .c | o executável |
Dentro de um .o: nm
O comando nm (de names, "nomes") lista os símbolos de um .o: os nomes de funções e variáveis que ele oferece e os que ele precisa:
$ nm util.o
0000000000000017 T linha
U puts
0000000000000000 T total_compra
$ nm main.o
U linha
0000000000000000 T main
U printf
U puts
U total_compra
- T
- "Está aqui" (de text, o nome da parte do arquivo onde fica o código). O
util.ooferecelinhaetotal_compra. O número à esquerda é a posição dentro do arquivo. - U
- "Indefinido" (undefined): o arquivo usa esse nome, mas ele está em outro lugar. O
main.oprecisa delinha,total_compra,printfeputs.
O trabalho do ligador é casar cada U com um T: o linha que o main.o precisa está no util.o; o printf e o puts estão na biblioteca C. (E de onde veio o puts, se ninguém o escreveu? O gcc percebeu que printf("--------------------\n") não tem nenhum % e o trocou pelo puts, que só escreve um texto e é mais rápido. O resultado na tela é o mesmo.)
3. Por que separar as etapas?
Num projeto de dois arquivos, tanto faz. Mas um programa de verdade pode ter centenas de .c, e compilar todos leva minutos. Se a Ana mudar só o util.c, o main.o continua valendo: o código do main.c não mudou. Basta recompilar o que mudou e ligar de novo:
$ gcc -std=c23 -Wall -Wextra -c util.c « só o arquivo que mudou $ gcc main.o util.o -o app « ligar é rápido
Ligar é muito mais rápido que compilar. Com 200 arquivos, recompilar um só e ligar leva segundos, contra minutos para recompilar tudo.
.h. Se o util.h mudar, todos os .c que o incluem têm de ser recompilados (aqui, o main.c e o util.c), porque o .h foi colado dentro deles. Esquecer um deixa um .o velho, compilado com a promessa antiga, e o resultado pode ser um programa que liga mas faz besteira.
Lembrar quais arquivos mudaram e quais dependem de qual .h é chato e fácil de errar. Por isso ninguém faz isso à mão: o programa make lê uma receita (o Makefile) e recompila exatamente o necessário. É o assunto da aula Makefile.
4. Os erros do ligador
Os erros do ligador não têm número de linha nem a setinha ^ embaixo do código, porque o ligador não lê o seu código C: ele só vê os .o. Mas eles são fáceis de entender quando você sabe ler as partes.
"Referência não definida": a peça que faltou
A Ana ligou só o main.o, esquecendo o util.o:
$ gcc main.o -o app /usr/bin/x86_64-linux-gnu-ld.bfd: main.o: na função "main": main.c:(.text+0x9): referência não definida para "linha" /usr/bin/x86_64-linux-gnu-ld.bfd: main.c:(.text+0x1d): referência não definida para "linha" /usr/bin/x86_64-linux-gnu-ld.bfd: main.c:(.text+0x2c): referência não definida para "total_compra" /usr/bin/x86_64-linux-gnu-ld.bfd: main.c:(.text+0x53): referência não definida para "total_compra" /usr/bin/x86_64-linux-gnu-ld.bfd: main.c:(.text+0x70): referência não definida para "linha" collect2: error: ld returned 1 exit status
- /usr/bin/x86_64-linux-gnu-ld.bfd
- Quem está falando: o ligador (
ld), na versão para computadores x86 de 64 bits. - main.o: na função "main"
- Onde está o problema: dentro da função
main, que veio domain.o. - main.c:(.text+0x9)
- Uma posição dentro do código de máquina do
main.c. Não é número de linha; pode ignorar. - referência não definida para "linha"
- A mensagem: o
mainchamalinha, e nenhum.oa define. Uma linha para cada chamada (são três chamadas delinha). Em inglês:undefined reference to `linha'. - collect2: error: ld returned 1 exit status
- "O
ldterminou com o código 1", ou seja, falhou. Ocollect2é um ajudante do gcc que chama o ligador. Essa linha aparece em todo erro de ligação; a informação útil está nas linhas de cima.
Repare que o main.o compilou sem reclamar: o util.h prometeu as funções, e o compilador acreditou. Quem descobre que a promessa não foi cumprida é o ligador. Quando você compila tudo junto (gcc main.c -o app, sem o util.c), a mensagem é a mesma, com nomes de arquivos temporários como /tmp/ccU9TVcP.o no lugar do main.o.
Outra causa clássica: o nome no .h é diferente do nome no .c. No projeto da pasta nome, o util.c define Linha, com L maiúsculo:
$ gcc -std=c23 -Wall -Wextra main.c util.c -o app /usr/bin/x86_64-linux-gnu-ld.bfd: /tmp/ccU9TVcP.o: na função "main": main.c:(.text+0x9): referência não definida para "linha" /usr/bin/x86_64-linux-gnu-ld.bfd: main.c:(.text+0x1d): referência não definida para "linha" /usr/bin/x86_64-linux-gnu-ld.bfd: main.c:(.text+0x70): referência não definida para "linha" collect2: error: ld returned 1 exit status
Para o C, linha e Linha são nomes diferentes. O nm util.o mostraria T Linha, e o main.o precisa de U linha. O compilador não tem como perceber: o util.c definir uma função a mais, chamada Linha, é permitido. Só o ligador, olhando o conjunto, nota que a linha prometida nunca chegou.
Sem main
Todo programa precisa de exatamente um main. Ligar só o util.o:
$ gcc util.o -o app /usr/bin/x86_64-linux-gnu-ld.bfd: /usr/lib/gcc/x86_64-linux-gnu/15/../../../x86_64-linux-gnu/Scrt1.o: na função "_start": (.text+0x1b): referência não definida para "main" collect2: error: ld returned 1 exit status
O Scrt1.o é um pedacinho de código do sistema que todo programa C recebe. A função _start dele é o verdadeiro começo do programa: ela prepara tudo e chama o seu main. Sem main, a referência fica sem dono.
"Múltiplas definições": duas peças iguais
Agora o contrário. Na pasta dupla, a Ana escreveu uma linha no main.c (com = em vez de -), esquecendo que o util.c já tem uma:
$ gcc -std=c23 -Wall -Wextra main.c util.c -o app /usr/bin/x86_64-linux-gnu-ld.bfd: /tmp/cccvbgyH.o: na função "linha": util.c:(.text+0x17): múltiplas definições de "linha"; /tmp/ccXm2qlQ.o:main.c:(.text+0x0): definido primeiro aqui collect2: error: ld returned 1 exit status
"Múltiplas definições de linha" no util.c; "definido primeiro aqui", no main.c. Em inglês: multiple definition of `linha'; … first defined here. Cada arquivo, sozinho, estava certo; juntos, há duas funções com o mesmo nome, e o ligador não escolhe uma por você. O conserto é deixar uma só, ou, se as duas devem existir, dar nomes diferentes ou tornar uma delas particular do seu arquivo com static (aula static e extern).
wasm-ld, para WebAssembly) e fala inglês com outras palavras: undefined symbol: linha ("símbolo indefinido") no lugar de "referência não definida", e duplicate symbol: linha ("símbolo duplicado") no lugar de "múltiplas definições". No fim, ele diz linker command failed, "o comando do ligador falhou".
Missão: no editor não dá para "esquecer um arquivo no comando", porque todas as abas .c entram. Simule o esquecimento: na aba util.c, apague a função total_compra inteira (deixe o protótipo no util.h) e rode. O compilador aceita, e o ligador reclama.
#include <stdio.h>
#include "util.h"
int main(void) {
linha();
printf("3 cadernos: R$ %d\n", total_compra(12, 3));
linha();
return 0;
}
#ifndef UTIL_H #define UTIL_H int total_compra(int preco, int quantidade); void linha(void); #endif
#include <stdio.h>
#include "util.h"
int total_compra(int preco, int quantidade) {
return preco * quantidade;
}
void linha(void) {
printf("--------------------\n");
}
#include <stdio.h>
#include "util.h"
void linha(void) {
printf("--------------------\n");
}
Formas de compilar: qual usar
| Forma | Bom para | Ruim porque |
|---|---|---|
gcc … main.c util.c -o app | Projetos pequenos, testes rápidos, exercícios | Recompila tudo a cada vez |
gcc … *.c -o app | O mesmo, sem digitar os nomes | Pega também um .c velho ou de teste que esteja na pasta |
gcc -c em cada arquivo + gcc *.o -o app | Entender as etapas; projetos médios | Você tem de lembrar o que mudou e o que depende de cada .h |
make (ou CMake) | Qualquer projeto que vai crescer | Precisa escrever a receita uma vez |
Qual usar? Para aprender e para programas de dois ou três arquivos, o comando único. Assim que o projeto começar a crescer, ou demorar para compilar, escreva um Makefile: ele faz as duas etapas por você, recompilando só o necessário.
Quando dá errado
referência não definida para "nome" (undefined reference to `nome')
O que significa: alguém chama nome, mas nenhum .o o define.
Como consertar: passe por esta lista de suspeitos:
- O
.c(ou o.o) que define a função ficou fora do comando. - O nome está escrito diferente no
.he no.c(maiúscula, letra trocada). Confira comnm arquivo.o. - A função foi declarada no
.h, mas nunca escrita. - A função está marcada como
staticno outro arquivo, e por isso é invisível (aula static e extern). - Ela está numa biblioteca que não foi pedida com
-l(aula Bibliotecas).
referência não definida para "main" na função _start
O que significa: nenhum dos arquivos tem a função main: ou o main.c ficou fora do comando, ou o nome está errado (Main, mian).
Como consertar: confira se o arquivo com o main está no comando. Se você quer só gerar o .o de um arquivo sem main, faltou o -c.
múltiplas definições de "nome" (multiple definition of `nome')
O que significa: dois .o definem a mesma função ou variável global. Causas comuns: o corpo de uma função (ou uma variável com valor) num .h incluído por vários .c; a mesma função copiada em dois arquivos; o mesmo .c passado duas vezes (gcc main.c util.c util.c); um #include "util.c".
Como consertar: cada função e cada variável global com uma definição, num .c só. Se duas funções auxiliares têm o mesmo nome em arquivos diferentes, marque-as com static.
bash: ./util.o: Permissão negada
O que significa: você tentou rodar um arquivo objeto. Ele não é um programa completo, e nem tem permissão para rodar.
Como consertar: ligue os .o num executável (gcc main.o util.o -o app) e rode o ./app.
Referência
| Comando | O que faz |
|---|---|
gcc -std=c23 -Wall -Wextra main.c util.c -o app | Compila e liga tudo de uma vez |
gcc … *.c -o app | O mesmo, com todos os .c da pasta |
gcc -std=c23 -Wall -Wextra -c util.c | Só compila: gera util.o |
gcc -c util.c -o outro.o | Só compila, escolhendo o nome do .o |
gcc main.o util.o -o app | Só liga os .o num executável |
nm arquivo.o | Lista os símbolos: T = definido aqui, U = precisa de outro lugar |
file arquivo.o | Mostra o tipo do arquivo (relocatable = objeto) |
Exercícios
1. Só compilar
Complete o comando que gera o estoque.o sem ligar:
2. Só ligar
Complete o comando que junta main.o e estoque.o no executável loja:
3. A Ana mudou só o estoque.c. O estoque.h não mudou. O que ela precisa refazer?
4. Na saída de nm main.o aparece U total_compra. O que isso quer dizer?
5. Quem mostra a mensagem múltiplas definições de "linha"?
Missão final: cumpra a promessa do .h
Missão: o projeto compila, mas não liga. Leia a mensagem do ligador: quais nomes ele não acha? Um deles foi escrito com outro nome no util.c; o outro nem foi escrito. Conserte só o util.c. A função com_desconto devolve o valor menos 10% (valor - valor / 10, com inteiros).
-------------------- 3 cadernos: R$ 36 Com desconto: R$ 33 --------------------
#include <stdio.h>
#include "util.h"
int main(void) {
int total = total_compra(12, 3);
linha();
printf("3 cadernos: R$ %d\n", total);
printf("Com desconto: R$ %d\n", com_desconto(total));
linha();
return 0;
}
#ifndef UTIL_H #define UTIL_H int total_compra(int preco, int quantidade); int com_desconto(int valor); void linha(void); #endif
#include <stdio.h>
#include "util.h"
int total_compra(int preco, int quantidade) {
return preco * quantidade;
}
void Linha(void) {
printf("--------------------\n");
}
#include <stdio.h>
#include "util.h"
int total_compra(int preco, int quantidade) {
return preco * quantidade;
}
int com_desconto(int valor) {
return valor - valor / 10;
}
void linha(void) {
printf("--------------------\n");
}
Resumo
- O gcc compila cada
.csozinho e o ligador junta as peças. O compilador vê um arquivo de cada vez; só o ligador vê o conjunto. - Tudo junto:
gcc main.c util.c -o app(a ordem não importa; o.hnão entra no comando). - Separado:
gcc -c arquivo.cgera oarquivo.o;gcc main.o util.o -o appliga. Assim, só o que mudou é recompilado. Se um.hmudar, recompile todos que o incluem. nmlista os símbolos de um.o:T(oferece) eU(precisa). O ligador casa cadaUcom umT.referência não definida para(undefined reference to): faltou a definição.múltiplas definições de(multiple definition of): sobrou uma. A linhacollect2: error: ld returned 1 exit statussó diz que o ligador falhou.- Para não ter de lembrar o que recompilar, use o make.