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.

Você vai aprender
  • 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 comando nm.
  • Por que separar as etapas economiza tempo em projetos grandes.
  • A ler os erros do ligador: referência não definida para e múltiplas definições de (em inglês, undefined reference to e multiple 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.

BicicletaPrograma em C
A especificação do encaixeo .h, com os protótipos
Cada fábricao compilador, trabalhando num .c por vez
Cada peça prontaum arquivo objeto .o (código de máquina, ainda incompleto)
A montadorao ligador (linker, o programa ld)
A bicicleta montadao 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 .c do programa. O .h nã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 .o com 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.

EtapaComandoQuem trabalhaEntraSai
Compilargcc -c main.cpré-processador + compiladorum .c (e os .h que ele inclui)um .o
Ligargcc main.o util.o -o appligador (ld)todos os .o + bibliotecaso executável
As duasgcc main.c util.c -o apptodostodos os .co 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.o oferece linha e total_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.o precisa de linha, total_compra, printf e puts.

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.

Cuidado com o .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 do main.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 main chama linha, e nenhum .o a define. Uma linha para cada chamada (são três chamadas de linha). Em inglês: undefined reference to `linha'.
collect2: error: ld returned 1 exit status
"O ld terminou com o código 1", ou seja, falhou. O collect2 é 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).

No editor do guia, o ligador é outro (o 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

FormaBom paraRuim porque
gcc … main.c util.c -o appProjetos pequenos, testes rápidos, exercíciosRecompila tudo a cada vez
gcc … *.c -o appO mesmo, sem digitar os nomesPega também um .c velho ou de teste que esteja na pasta
gcc -c em cada arquivo + gcc *.o -o appEntender as etapas; projetos médiosVocê tem de lembrar o que mudou e o que depende de cada .h
make (ou CMake)Qualquer projeto que vai crescerPrecisa 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:

  1. O .c (ou o .o) que define a função ficou fora do comando.
  2. O nome está escrito diferente no .h e no .c (maiúscula, letra trocada). Confira com nm arquivo.o.
  3. A função foi declarada no .h, mas nunca escrita.
  4. A função está marcada como static no outro arquivo, e por isso é invisível (aula static e extern).
  5. 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

ComandoO que faz
gcc -std=c23 -Wall -Wextra main.c util.c -o appCompila e liga tudo de uma vez
gcc … *.c -o appO mesmo, com todos os .c da pasta
gcc -std=c23 -Wall -Wextra -c util.cSó compila: gera util.o
gcc -c util.c -o outro.oSó compila, escolhendo o nome do .o
gcc main.o util.o -o appSó liga os .o num executável
nm arquivo.oLista os símbolos: T = definido aqui, U = precisa de outro lugar
file arquivo.oMostra o tipo do arquivo (relocatable = objeto)

Exercícios

1. Só compilar

Complete o comando que gera o estoque.o sem ligar:

gcc -std=c23 -Wall -Wextra estoque.c

2. Só ligar

Complete o comando que junta main.o e estoque.o no executável loja:

gcc -o 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 .c sozinho 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 .h não entra no comando).
  • Separado: gcc -c arquivo.c gera o arquivo.o; gcc main.o util.o -o app liga. Assim, só o que mudou é recompilado. Se um .h mudar, recompile todos que o incluem.
  • nm lista os símbolos de um .o: T (oferece) e U (precisa). O ligador casa cada U com um T.
  • referência não definida para (undefined reference to): faltou a definição. múltiplas definições de (multiple definition of): sobrou uma. A linha collect2: error: ld returned 1 exit status só diz que o ligador falhou.
  • Para não ter de lembrar o que recompilar, use o make.