C++ Como Funciona
Você escreve um texto num arquivo ola.cpp, digita um comando, e aparece um programa que roda. O que acontece no meio? Nesta aula você vai abrir a "caixa-preta" do compilador e ver, de verdade, as quatro etapas que transformam o seu texto num programa executável. Você vai olhar o arquivo de cada etapa, entender por que um programa C++ roda sem precisar do compilador, e por que toda mudança no código pede uma nova compilação.
- O caminho completo: pré-processador → compilador → montador → ligador.
- Como parar o
g++no meio do caminho para ver cada etapa:-E,-Se-c. - O que são o arquivo
.ii, o.s(assembly), o.o(objeto) e o executável. - A diferença prática entre linguagem compilada e interpretada.
- Por que o programa pronto roda sem o
g++, e por que é preciso compilar de novo depois de cada mudança. - Como reconhecer de qual etapa veio uma mensagem de erro.
Os comandos desta aula foram executados na máquina fictícia da Ana (usuária ana, computador notebook), na pasta ~/cpp, com o g++ 15.2. Não se preocupe em decorar as opções -E, -S e -c: no dia a dia você vai usar um único comando, que faz as quatro etapas de uma vez. Aqui elas servem para você enxergar o caminho.
A ideia: publicar um livro traduzido
Imagine que a Ana escreveu um livro em português e quer vendê-lo num país que só lê japonês. O livro passa por quatro pessoas, uma depois da outra:
- A organizadora junta tudo num manuscrito só. Onde a Ana escreveu "inserir aqui o capítulo de receitas", ela cola o capítulo inteiro. Ela não traduz nada: só junta.
- O tradutor lê o manuscrito completo, confere se as frases fazem sentido (se não fizerem, devolve com um bilhete apontando a linha) e escreve a versão em japonês.
- A gráfica transforma o texto traduzido em páginas impressas, prontas, mas ainda soltas. Onde o livro diz "veja o mapa no apêndice", fica um espaço em branco: o mapa vem de outro lugar.
- O encadernador junta as páginas da Ana com as partes que vêm de fora (o mapa, o índice padrão da editora), preenche os espaços e entrega o livro pronto.
Compilar um programa C++ é exatamente isso. Cada pessoa é uma ferramenta, e cada uma produz um arquivo:
| Etapa | Ferramenta | Recebe | Produz | Para parar aqui |
|---|---|---|---|---|
| 1. Juntar | Pré-processador | ola.cpp (o seu código) | ola.ii: o código com os #include já colados | g++ -E |
| 2. Traduzir | Compilador (propriamente dito) | o código pré-processado | ola.s: assembly, as instruções do processador escritas em texto | g++ -S |
| 3. Montar | Montador (assembler) | ola.s | ola.o: arquivo objeto, código de máquina com "espaços em branco" | g++ -c |
| 4. Ligar | Ligador (linker) | ola.o + a biblioteca padrão do C++ | ola: o executável, pronto para rodar | (é o final) |
O g++ é, na verdade, um "gerente": ele chama uma ferramenta de cada vez e passa o resultado de uma para a outra. Quando você digita g++ ola.cpp -o ola, as quatro etapas acontecem em sequência, e os arquivos intermediários são criados numa pasta temporária e apagados no fim. Vamos fazer uma por uma.
O ponto de partida: o código-fonte
O arquivo que você escreve se chama código-fonte (source code). É um texto comum, que qualquer editor abre. Este é o programa mais simples que existe:
#include <iostream>
int main() {
std::cout << "Olá, mundo!\n";
}
$ ls -l ola.cpp -rw-rw-r-- 1 ana ana 71 out 16 21:47 ola.cpp « 71 bytes: só texto
São 71 bytes, ou seja, 71 caracteres (o á conta como 2, porque no formato UTF-8 ele ocupa dois bytes). Guarde esse número: vamos ver o que acontece com ele em cada etapa.
Etapa 1: o pré-processador (g++ -E)
O pré-processador é a "organizadora" do livro. Ele procura as linhas que começam com #, as chamadas diretivas, e as executa. A diretiva #include <iostream> quer dizer: "cole aqui o conteúdo do arquivo iostream". Esse arquivo vem com o compilador e contém tudo o que o C++ precisa saber sobre o std::cout.
A opção -E manda o g++ parar logo depois do pré-processador. O -o ola.ii diz em que arquivo guardar o resultado (-o = output, saída; .ii é a extensão tradicional de código C++ pré-processado):
$ g++ -E ola.cpp -o ola.ii
$ wc -l ola.cpp ola.ii « conta as linhas de cada arquivo
5 ola.cpp
36937 ola.ii « de 5 linhas para quase 37 mil!
36942 total
As 5 linhas viraram 36.937. Isso porque o <iostream> inclui outros arquivos, que incluem outros, e o pré-processador colou todos eles. Ninguém lê 37 mil linhas: os comandos head e tail mostram só o começo e o fim do arquivo:
$ head -5 ola.ii
# 0 "ola.cpp"
# 0 "<built-in>"
# 0 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 0 "<command-line>" 2
$ tail -8 ola.ii
}
# 2 "ola.cpp" 2
# 3 "ola.cpp"
int main() { « o seu código está lá no final
std::cout << "Olá, mundo!\n";
}
As linhas que começam com # e um número são marcadores: anotações que o pré-processador deixa para o compilador saber de que arquivo e de que linha veio cada trecho. É graças a elas que, quando há um erro, a mensagem diz ola.cpp:4 e não "linha 36936 do arquivo colado".
E onde está o cout? O comando grep procura um texto num arquivo, e o sed -n mostra um intervalo de linhas:
$ grep -n "extern ostream cout" ola.ii 36916: extern ostream cout; « linha 36916: veio do arquivo iostream $ sed -n "36914,36918p" ola.ii # 64 "/usr/include/c++/15/iostream" 3 extern istream cin; extern ostream cout; extern ostream cerr; extern ostream clog;
Repare na primeira linha: o trecho veio de /usr/include/c++/15/iostream, o arquivo de verdade que o g++ 15 traz. A linha extern ostream cout; é uma declaração: ela diz ao compilador "existe uma coisa chamada cout, do tipo ostream, mas ela está guardada em outro lugar". Lembre-se desse "outro lugar": ele volta na etapa 4.
Ele só mexe em texto: cola arquivos, troca palavras (#define) e liga ou desliga trechos (#if). Ele não sabe se o seu código está certo. A aula O Pré-processador mostra todas as diretivas.
Etapa 2: o compilador (g++ -S)
Agora vem o "tradutor", a etapa mais difícil e a que dá nome a todo o processo. O compilador lê o código pré-processado, confere se ele segue as regras do C++ (a sintaxe, os tipos) e traduz cada instrução para assembly (lê-se "assêmbli"): a língua do processador, escrita em texto, com uma instrução simples por linha.
A opção -S manda o g++ parar depois desta etapa:
$ g++ -S ola.cpp -o ola.s $ wc -l ola.s 50 ola.s « 50 linhas de assembly
As 36.937 linhas viraram 50. Toda aquela biblioteca colada era só declarações ("existe um cout"); o compilador só gera instruções para o que o seu programa faz. Veja os trechos que correspondem ao seu programa. Para caber na tela, cortamos algumas linhas técnicas; cada … marca um corte e não faz parte da saída real:
$ cat ola.s
…
.LC0:
.string "Ol\303\241, mundo!\n" « o seu texto (o á virou dois bytes: \303\241)
…
main: « aqui começa a função main
…
leaq .LC0(%rip), %rdx « pega o endereço do texto
leaq _ZSt4cout(%rip), %rax « pega o endereço do std::cout
movq %rdx, %rsi
movq %rax, %rdi
call _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@PLT « chama o <<
movl $0, %eax « o main devolve 0 ("deu tudo certo")
popq %rbp
…
ret « fim do main
…
.ident "GCC: (Ubuntu 15.2.0-16ubuntu1) 15.2.0"
Você não precisa ler assembly para programar em C++. Mas dá para reconhecer a ideia: a linha std::cout << "Olá, mundo!\n"; virou "pegue o endereço do texto, pegue o endereço do cout, chame a função do <<". O nome esquisito _ZSt4cout é o std::cout "codificado" num formato que o ligador entende (isso se chama name mangling, "embaralhar nomes"). E o movl $0, %eax é o 0 que o main devolve sozinho quando chega ao fim: é o código de saída que você vê com echo $?.
É nesta etapa que aparecem quase todos os erros de compilação: um ; esquecido, uma variável com nome errado, um texto onde deveria ir um número. Se o compilador encontra um erro, ele para e não produz nada. A aula Lendo Erros do Compilador ensina a ler essas mensagens.
Este assembly é de um processador x86-64 (Intel e AMD, a maioria dos PCs). Num Mac com chip Apple ou num celular (processadores ARM), as instruções são outras. O seu ola.cpp é o mesmo em todos; quem muda é o que o compilador gera. Por isso um programa compilado para Linux num PC não roda num Mac: é preciso compilar de novo lá.
Etapa 3: o montador (g++ -c)
O montador (assembler, o programa as) transforma o assembly, que ainda é texto, em código de máquina: os números que o processador executa. O resultado é um arquivo objeto, com extensão .o.
A opção -c (de compile, "só compile, não ligue") faz as etapas 1, 2 e 3 e para antes da 4:
$ g++ -c ola.cpp -o ola.o $ file ola.o « o comando file diz que tipo de arquivo é ola.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
O file diz que o ola.o é um arquivo ELF (o formato de programas do Linux), de 64 bits, para x86-64, e relocatable ("relocável"): um pedaço de programa que ainda vai ser encaixado em outro lugar. Ele não é texto. Se você tentar ver os primeiros bytes com o xxd (que mostra um arquivo como números), aparece a "assinatura" do ELF e muitos zeros:
$ xxd ola.o | head -2 00000000: 7f45 4c46 0201 0100 0000 0000 0000 0000 .ELF............ « todo ELF começa com 7f E L F 00000010: 0100 3e00 0100 0000 0000 0000 0000 0000 ..>.............
O arquivo objeto ainda não roda. Veja o que acontece se a Ana tentar:
$ ./ola.o bash: ./ola.o: Permissão negada « não é um programa: nem permissão de execução ele tem
Por que não está pronto? Porque ele tem buracos, os "espaços em branco" da gráfica. O comando nm lista os nomes que um arquivo objeto define e os que ele usa mas não tem; a opção -C mostra os nomes em C++ legível:
$ nm -C ola.o
0000000000000000 T main « T: o main está aqui dentro
U std::ios_base_library_init() « U: usado, mas está em outro lugar
U std::cout « o cout que o iostream prometeu
U std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)
O T (de text, o nome tradicional da área de código) marca o que o arquivo tem: o seu main. O U (undefined, "não definido") marca o que ele usa mas não tem: o std::cout e o operador << (aquele nome enorme é o << que escreve um texto). Eles foram só declarados pelo iostream, lembra? O código deles de verdade está na biblioteca padrão do C++, que já vem compilada no sistema.
Etapa 4: o ligador
O ligador (linker, o programa ld; também se diz "linkeditor") é o "encadernador". Ele junta o seu arquivo objeto com as bibliotecas de que ele precisa, liga cada U ao endereço certo e escreve o executável. Basta passar o .o para o g++:
$ g++ ola.o -o ola $ file ola ola: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=4857c0895c68bf346d8c08c2d87be4904a5be0c5, for GNU/Linux 3.2.0, not stripped $ ./ola Olá, mundo! « finalmente!
Agora o file diz executable, "executável". Ele também diz dynamically linked ("ligado dinamicamente"): em vez de copiar a biblioteca padrão inteira para dentro do ola, o ligador anotou "este programa precisa da biblioteca tal", e o sistema a carrega na hora em que o programa abre. O comando ldd mostra essas bibliotecas:
$ ldd ./ola linux-vdso.so.1 (0x0000708dcce9e000) libstdc++.so.6 => /usr/lib/x86_64-linux-gnu/libstdc++.so.6 (0x0000708dcca00000) « a biblioteca padrão do C++: aqui mora o cout libc.so.6 => /usr/lib/x86_64-linux-gnu/libc.so.6 (0x0000708dcc600000) « a biblioteca do C libm.so.6 => /usr/lib/x86_64-linux-gnu/libm.so.6 (0x0000708dccd50000) « a de matemática /lib64/ld-linux-x86-64.so.2 (0x0000708dccea0000) libgcc_s.so.1 => /usr/lib/x86_64-linux-gnu/libgcc_s.so.1 (0x0000708dccd22000)
A libstdc++ (standard C++ library, a biblioteca padrão do C++ do GCC) é onde o std::cout de verdade mora. Os números entre parênteses são endereços de memória e mudam a cada execução.
Todos os arquivos, lado a lado
$ ls -l total 932 -rwxrwxr-x 1 ana ana 16120 out 16 21:47 ola « o executável: 16 KB (o x = pode ser executado) -rw-rw-r-- 1 ana ana 71 out 16 21:47 ola.cpp « o seu código: 71 bytes -rw-rw-r-- 1 ana ana 924092 out 16 21:47 ola.ii « pré-processado: 924 KB de texto -rw-rw-r-- 1 ana ana 1664 out 16 21:47 ola.o « objeto: 1,6 KB, com buracos -rw-rw-r-- 1 ana ana 820 out 16 21:47 ola.s « assembly: 820 bytes de texto
Só o ola tem o x nas permissões (-rwxrwxr-x): ele é o único que pode ser executado. Os outros três são passos intermediários, que você normalmente nunca vê.
Tudo de uma vez
No dia a dia, ninguém faz as etapas separadas. Um único comando faz tudo:
$ g++ ola.cpp -o ola $ ./ola Olá, mundo!
Para ver o g++ trabalhando como "gerente", a opção -v (verbose, "falante") mostra cada ferramenta que ele chama. A saída é enorme; filtrando só as três linhas importantes (e cortando o fim de cada uma):
$ g++ -v ola.cpp -o ola 2>&1 | grep -E "cc1plus|-as |collect2" | cut -c1-90 /usr/libexec/gcc/x86_64-linux-gnu/15/cc1plus -quiet -v -imultiarch x86_64-linux-gnu -D_GN « etapas 1 e 2: o compilador /usr/bin/x86_64-linux-gnu-as -v --64 -o /tmp/ccgIgiFF.o /tmp/ccwf5DRv.s « etapa 3: o montador (as) /usr/libexec/gcc/x86_64-linux-gnu/15/collect2 -plugin /usr/libexec/gcc/x86_64-linux-gnu/1 « etapa 4: o ligador (collect2 chama o ld)
O cc1plus é o compilador de C++ propriamente dito (ele faz o pré-processamento e a compilação juntos, por eficiência). Repare nos arquivos /tmp/cc….s e /tmp/cc….o: são o assembly e o objeto, criados na pasta temporária e apagados no fim. Se quiser guardá-los, a opção -save-temps deixa todos na pasta atual:
$ g++ -save-temps ola.cpp -o ola $ ls -l total 932 -rwxrwxr-x 1 ana ana 16120 out 16 21:47 ola -rw-rw-r-- 1 ana ana 71 out 16 21:47 ola.cpp -rw-rw-r-- 1 ana ana 924092 out 16 21:47 ola.ii -rw-rw-r-- 1 ana ana 1664 out 16 21:47 ola.o -rw-rw-r-- 1 ana ana 820 out 16 21:47 ola.s
No editor do guia
As caixas Experimente fazem o mesmo caminho, só que dentro do navegador e com o compilador Clang: pré-processar, compilar, montar e ligar, e depois rodar. A caixa abaixo tem um programa que compila, mas não liga: a função dobro foi declarada ("existe uma função dobro"), mas ninguém escreveu o que ela faz. É o mesmo tipo de buraco do U que você viu no nm. Clique em Rodar e procure a palavra undefined ("não definido") na mensagem: ela vem do ligador.
Missão: rode e veja o erro do ligador. Depois tape o buraco: troque a linha int dobro(int x); por int dobro(int x) { return x * 2; }, que diz o que a função faz. Rode de novo: deve aparecer 42. (Funções têm a sua própria aula, Funções; aqui basta copiar.)
#include <iostream>
int dobro(int x);
int main() {
std::cout << dobro(21) << "\n";
}
#include <iostream>
int dobro(int x) { return x * 2; }
int main() {
std::cout << dobro(21) << "\n";
}
Compilado × interpretado, na prática
Na aula O que é C++ você viu a diferença em teoria. Agora dá para ver na prática. Num programa Python, o arquivo .py é o próprio programa: toda vez que ele roda, o interpretador python3 lê o texto e executa. No C++, o arquivo .cpp só é lido uma vez, na compilação; quem roda depois é o executável.
| C++ (compilado) | Python (interpretado) | |
|---|---|---|
| Para rodar | g++ ola.cpp -o ola uma vez, depois ./ola quantas vezes quiser | python3 ola.py, sempre |
| Quem lê o código-fonte | Só o compilador, na hora de compilar | O interpretador, a cada execução |
| O que você entrega para outra pessoa | O executável (ela não precisa do compilador nem do código) | O código-fonte (ela precisa do Python instalado) |
| Erros de digitação | Aparecem antes de rodar: sem compilar, não há programa | Muitos só aparecem quando a linha com erro é executada |
| Mudou o código? | Precisa compilar de novo | É só rodar de novo |
| Velocidade | Muito alta: a tradução já foi feita | Mais baixa: a tradução acontece enquanto roda |
O executável roda sem o g++
Depois de compilado, o programa não precisa mais do compilador. Para provar, a Ana roda o ola com o PATH (a lista de pastas onde o terminal procura programas) apontando para uma pasta que nem existe. Assim, o terminal não consegue achar g++ nenhum. E ainda copia o programa para outra pasta, longe do ola.cpp:
$ env PATH=/pasta-vazia ./ola « nenhum programa do sistema ao alcance Olá, mundo! $ cp ola /tmp/ && cd /tmp && ./ola && cd ~/cpp « longe do código-fonte Olá, mundo!
Nos dois casos o programa rodou. O executável já contém as instruções de máquina; ele só precisa das bibliotecas do sistema que o ldd mostrou (a libstdc++ e a libc), que vêm instaladas em praticamente todo Linux. É assim que os programas que você baixa funcionam: o Chrome, por exemplo, chega pronto, e você não precisa de um compilador para usá-lo.
Mudou o código? Compile de novo
O outro lado da moeda: o executável é uma "fotografia" do código no momento em que foi compilado. A Ana troca mundo por Ana no ola.cpp (o comando sed faz a troca sem abrir um editor) e roda o programa:
$ sed -i 's/mundo/Ana/' ola.cpp
$ cat ola.cpp
#include <iostream>
int main() {
std::cout << "Olá, Ana!\n";
}
$ ./ola
Olá, mundo! « o programa velho: o .cpp mudou, o executável não
$ g++ ola.cpp -o ola « compila de novo
$ ./ola
Olá, Ana! « agora sim
Esquecer de recompilar é uma das confusões mais comuns de quem começa: "mudei o código e nada mudou!". Lembre-se: editou, compilou, rodou. As caixas Experimente fazem isso por você: o botão Rodar sempre compila antes de rodar. Em projetos grandes, compilar tudo de novo demora; por isso existem ferramentas como o Make, que recompilam só os arquivos que mudaram.
Quando dá errado
Cada etapa tem os seus erros, e saber de qual etapa veio a mensagem já é metade do conserto. O segredo está no começo da mensagem.
fatal error: …: Arquivo ou diretório inexistente
#include <iostrem>
int main() {
std::cout << "Olá, mundo!\n";
}
$ g++ erro1.cpp -o erro1
erro1.cpp:1:10: fatal error: iostrem: Arquivo ou diretório inexistente
1 | #include <iostrem>
| ^~~~~~~~~
compilation terminated.
O que significa: "erro fatal: iostrem: arquivo ou diretório inexistente". O pré-processador tentou colar o arquivo iostrem (faltou o a) e não o achou. "Fatal" quer dizer que ele desistiu na hora; compilation terminated = "compilação encerrada". O 1:10 é linha 1, coluna 10, e o ^~~~ aponta o nome errado. Essa mensagem mistura inglês (do g++) e português (o "Arquivo ou diretório inexistente" vem do sistema, que está em português).
Como consertar: corrija o nome: #include <iostream>.
error: expected ‘;’ before ‘}’ token
Quando a mensagem tem arquivo:linha:coluna: error: e fala de regras da linguagem (um ; esperado, um nome desconhecido, tipos que não combinam), ela veio da etapa 2. Foi o que você viu na página inicial ao apagar o ;. Essas mensagens são as mais comuns, e ganham uma aula inteira: Lendo Erros do Compilador.
referência não definida para (undefined reference to)
#include <iostream>
int dobro(int x);
int main() {
std::cout << dobro(21) << "\n";
}
$ g++ soma.cpp -o soma /usr/bin/x86_64-linux-gnu-ld.bfd: /tmp/cch0zKcp.o: na função "main": soma.cpp:(.text+0xe): referência não definida para "dobro(int)" collect2: error: ld returned 1 exit status
Como reconhecer: a mensagem começa com o nome do ligador (ld), cita um arquivo .o temporário em vez de uma linha do seu código, e termina com collect2: error: ld returned 1 exit status ("o ld terminou com o código 1", ou seja, falhou). No Linux em português, o meio vem traduzido; num sistema em inglês aparece undefined reference to `dobro(int)'.
O que significa: o compilador aceitou o código (a declaração int dobro(int x); prometeu que a função existe), mas na hora de ligar ninguém tinha o código da função. É o mesmo U que você viu no nm, só que dessa vez nenhuma biblioteca tinha a peça que faltava.
Como consertar: escreva a função (o "corpo" entre chaves), ou, se ela estiver em outro arquivo .cpp, compile os dois juntos (aula Compilar Vários Arquivos).
referência não definida para "main"
#include <iostream>
int mian() {
std::cout << "Olá, mundo!\n";
}
$ g++ semmain.cpp -o semmain
semmain.cpp: In function ‘int mian()’:
semmain.cpp:5:1: warning: no return statement in function returning non-void [-Wreturn-type]
5 | }
| ^
/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 que significa: a Ana digitou mian em vez de main. Para o compilador, mian é só uma função qualquer (ele até avisa, com um warning, que ela não devolve nada, porque só o main pode terminar sem return). Mas todo programa precisa de um main: é por ele que o programa começa. O ligador junta o seu código com um pedacinho de partida do sistema (o Scrt1.o, com a função _start), que chama o main, e não achou nenhum.
Como consertar: escreva main certinho, em minúsculas.
gcc em vez de g++ e veio uma chuva de "referência não definida"
$ gcc ola.cpp -o ola2 /usr/bin/x86_64-linux-gnu-ld.bfd: /tmp/ccHHq7l3.o: aviso: relocalização contra "_ZSt4cout" em secção só de leitura ".text" /usr/bin/x86_64-linux-gnu-ld.bfd: /tmp/ccHHq7l3.o: na função "main": ola.cpp:(.text+0x12): referência não definida para "std::cout" /usr/bin/x86_64-linux-gnu-ld.bfd: ola.cpp:(.text+0x1d): referência não definida para "std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)" /usr/bin/x86_64-linux-gnu-ld.bfd: aviso: a criar DT_TEXTREL num PIE. collect2: error: ld returned 1 exit status
O que significa: o gcc viu a extensão .cpp e compilou como C++ (etapas 1 a 3 deram certo). Mas, na etapa 4, ele liga o programa como se fosse C, sem a biblioteca padrão do C++. Resultado: o std::cout e o << ficaram sem dono, exatamente os dois U do nm.
Como consertar: use g++ para C++. (gcc e g++ são da mesma família, a GCC, GNU Compiler Collection; o g++ é o "modo C++", que já liga a libstdc++.)
Permissão negada, comando não encontrado ou Arquivo ou diretório inexistente
$ ./ola.o bash: ./ola.o: Permissão negada « tentou rodar o objeto, não o executável $ ola bash: ola: comando não encontrado « faltou o ./ antes do nome $ ./programa bash: ./programa: Arquivo ou diretório inexistente « esse executável não existe (o nome foi outro, ou a compilação falhou)
O que significa: esses erros não são do compilador, são do terminal (o bash). O arquivo .o não é executável. Sem o ./, o terminal procura o programa só nas pastas do sistema, não na pasta atual (o ./ quer dizer "aqui nesta pasta"). E, se a compilação falhou, nenhum executável foi criado.
Como consertar: rode o nome que você deu depois do -o, com ./ na frente. Se a compilação mostrou um error, conserte e compile de novo antes de rodar.
Referência
| Comando | Faz até a etapa | Resultado |
|---|---|---|
g++ -E ola.cpp -o ola.ii | 1. pré-processador | código com os #include colados (texto) |
g++ -S ola.cpp -o ola.s | 2. compilador | assembly (texto) |
g++ -c ola.cpp -o ola.o | 3. montador | arquivo objeto (binário, com buracos) |
g++ ola.o -o ola | 4. ligador (a partir do .o) | executável |
g++ ola.cpp -o ola | todas | executável (os intermediários são apagados) |
g++ -save-temps ola.cpp -o ola | todas | executável, guardando .ii, .s e .o |
g++ -v … | todas | mostra cada ferramenta chamada |
| Extensão | O que é | Dá para ler? |
|---|---|---|
.cpp (.cc, .cxx) | Código-fonte C++, escrito por você | Sim, é texto |
.h, .hpp | Arquivo de cabeçalho, feito para ser incluído com #include (aula Arquivos .h e .cpp) | Sim |
.ii | Código pré-processado | Sim, mas é enorme |
.s | Assembly | Sim, se você souber assembly |
.o | Arquivo objeto | Não, é binário (use nm ou objdump) |
(sem extensão, no Linux e no macOS); .exe no Windows | Executável | Não, é binário; você roda |
| A mensagem começa com… | Veio de |
|---|---|
fatal error: xxx: Arquivo ou diretório inexistente num #include | Pré-processador |
arquivo.cpp:linha:coluna: error: | Compilador |
…ld…, referência não definida / undefined reference, collect2: error | Ligador |
bash: … | O terminal, na hora de rodar |
Exercícios
1. Qual é a ordem das etapas da compilação?
2. O que o pré-processador faz com #include <iostream>?
3. A mensagem termina com collect2: error: ld returned 1 exit status. De que etapa veio o erro?
4. A Ana mudou o texto no ola.cpp, rodou ./ola e a mensagem antiga apareceu. Por quê?
5. Parar antes de ligar
Complete o comando que gera só o arquivo objeto ola.o, sem ligar:
6. Ver o assembly
Complete o comando que para depois da compilação e gera o assembly:
Missão final: provoque e conserte um erro de cada etapa
Missão: este programa tem dois problemas: um nome de arquivo errado no #include (o pré-processador vai reclamar) e um main escrito errado (o ligador vai reclamar). Rode, leia a primeira mensagem, conserte, rode de novo, leia a segunda, conserte. No fim, a saída deve ser Compilou, ligou e rodou!
#include <iostram>
int Main() {
std::cout << "Compilou, ligou e rodou!\n";
return 0;
}
#include <iostream>
int main() {
std::cout << "Compilou, ligou e rodou!\n";
return 0;
}
Repare que você só viu o segundo erro depois de consertar o primeiro: com o #include errado, o processo parou na etapa 1 e nem chegou ao ligador. E Main, com M maiúsculo, não é main: o C++ diferencia maiúsculas de minúsculas.
Resumo
- Quatro etapas: pré-processador (cola os
#include) → compilador (confere as regras e traduz para assembly) → montador (gera o arquivo objeto.o) → ligador (junta com as bibliotecas e gera o executável). g++ -E,-Se-cparam depois das etapas 1, 2 e 3;g++ ola.cpp -o olafaz tudo;-save-tempsguarda os intermediários.- O
.otem "buracos" (Unonm) que o ligador preenche com a biblioteca padrão (libstdc++). - O executável roda sozinho, sem o
g++e sem o.cpp. - Mudou o código? Compile de novo. Editou, compilou, rodou.
- Reconheça a etapa pelo começo da mensagem:
fatal errornum#include(pré-processador),arquivo:linha:coluna: error(compilador),ld/referência não definida/collect2(ligador). - Próximo passo: as Versões do C++, e por que o guia sempre compila com
-std=c++23.