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.

Você vai aprender
  • O caminho completo: pré-processador → compilador → montador → ligador.
  • Como parar o g++ no meio do caminho para ver cada etapa: -E, -S e -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:

  1. 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.
  2. 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.
  3. 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.
  4. 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:

EtapaFerramentaRecebeProduzPara parar aqui
1. JuntarPré-processadorola.cpp (o seu código)ola.ii: o código com os #include já coladosg++ -E
2. TraduzirCompilador (propriamente dito)o código pré-processadoola.s: assembly, as instruções do processador escritas em textog++ -S
3. MontarMontador (assembler)ola.sola.o: arquivo objeto, código de máquina com "espaços em branco"g++ -c
4. LigarLigador (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.

O pré-processador não entende C++

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.

Assembly depende do processador

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 rodarg++ ola.cpp -o ola uma vez, depois ./ola quantas vezes quiserpython3 ola.py, sempre
Quem lê o código-fonteSó o compilador, na hora de compilarO interpretador, a cada execução
O que você entrega para outra pessoaO executável (ela não precisa do compilador nem do código)O código-fonte (ela precisa do Python instalado)
Erros de digitaçãoAparecem antes de rodar: sem compilar, não há programaMuitos só aparecem quando a linha com erro é executada
Mudou o código?Precisa compilar de novoÉ só rodar de novo
VelocidadeMuito alta: a tradução já foi feitaMais 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.

Erro do pré-processador: 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>.

Erro do compilador: 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.

Erro do ligador: 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).

Erro do ligador: 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.

Usei 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++.)

Na hora de rodar: 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

ComandoFaz até a etapaResultado
g++ -E ola.cpp -o ola.ii1. pré-processadorcódigo com os #include colados (texto)
g++ -S ola.cpp -o ola.s2. compiladorassembly (texto)
g++ -c ola.cpp -o ola.o3. montadorarquivo objeto (binário, com buracos)
g++ ola.o -o ola4. ligador (a partir do .o)executável
g++ ola.cpp -o olatodasexecutável (os intermediários são apagados)
g++ -save-temps ola.cpp -o olatodasexecutável, guardando .ii, .s e .o
g++ -v …todasmostra cada ferramenta chamada
ExtensãoO que éDá para ler?
.cpp (.cc, .cxx)Código-fonte C++, escrito por vocêSim, é texto
.h, .hppArquivo de cabeçalho, feito para ser incluído com #include (aula Arquivos .h e .cpp)Sim
.iiCódigo pré-processadoSim, mas é enorme
.sAssemblySim, se você souber assembly
.oArquivo objetoNão, é binário (use nm ou objdump)
(sem extensão, no Linux e no macOS); .exe no WindowsExecutávelNão, é binário; você roda
A mensagem começa com…Veio de
fatal error: xxx: Arquivo ou diretório inexistente num #includePré-processador
arquivo.cpp:linha:coluna: error:Compilador
…ld…, referência não definida / undefined reference, collect2: errorLigador
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:

g++ ola.cpp -o ola.o

6. Ver o assembly

Complete o comando que para depois da compilação e gera o assembly:

g++ ola.cpp -o ola.s

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, -S e -c param depois das etapas 1, 2 e 3; g++ ola.cpp -o ola faz tudo; -save-temps guarda os intermediários.
  • O .o tem "buracos" (U no nm) 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 error num #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.