A compilação de um programa C ++ envolve três etapas:
Pré-processamento: o pré-processador pega um arquivo de código-fonte C ++ e lida com as diretivas #include
s, se #define
outras diretivas de pré-processador. A saída desta etapa é um arquivo C ++ "puro" sem diretivas de pré-processador.
Compilação: o compilador pega a saída do pré-processador e produz um arquivo de objeto a partir dele.
Vinculação: o vinculador pega os arquivos de objeto produzidos pelo compilador e produz uma biblioteca ou um arquivo executável.
Pré-processando
O pré-processador lida com as diretivas do pré - processador , como #include
e #define
. É independente da sintaxe do C ++, motivo pelo qual deve ser usado com cuidado.
Ele funciona em um arquivo de origem C ++ em um momento substituindo #include
directivas com o conteúdo dos respectivos arquivos (que normalmente é apenas declarações), fazendo a substituição de macros ( #define
), e selecionar diferentes partes do texto dependendo de #if
, #ifdef
e #ifndef
directivas.
O pré-processador trabalha em um fluxo de tokens de pré-processamento. A substituição de macro é definida como a substituição de tokens por outros tokens (o operador ##
permite mesclar dois tokens quando faz sentido).
Depois de tudo isso, o pré-processador produz uma única saída que é um fluxo de tokens resultantes das transformações descritas acima. Ele também adiciona alguns marcadores especiais que informam ao compilador de onde cada linha veio, para que ele possa ser usado para produzir mensagens de erro sensíveis.
Alguns erros podem ser produzidos nesta fase com o uso inteligente das diretivas #if
e #error
.
Compilação
A etapa de compilação é realizada em cada saída do pré-processador. O compilador analisa o código-fonte C ++ puro (agora sem diretivas de pré-processador) e o converte em código de montagem. Em seguida, chama o back-end subjacente (assembler no conjunto de ferramentas) que reúne esse código no código da máquina produzindo o arquivo binário real em algum formato (ELF, COFF, a.out, ...). Este arquivo de objeto contém o código compilado (em formato binário) dos símbolos definidos na entrada. Os símbolos nos arquivos de objetos são referidos pelo nome.
Arquivos de objeto podem se referir a símbolos que não estão definidos. Este é o caso quando você usa uma declaração e não fornece uma definição para ela. O compilador não se importa com isso e, felizmente, produzirá o arquivo de objeto, desde que o código-fonte esteja bem formado.
Os compiladores geralmente permitem que você pare a compilação neste momento. Isso é muito útil, pois com ele você pode compilar cada arquivo de código-fonte separadamente. A vantagem que isso oferece é que você não precisa recompilar tudo se alterar apenas um único arquivo.
Os arquivos de objetos produzidos podem ser colocados em arquivos especiais chamados bibliotecas estáticas, para facilitar a reutilização posteriormente.
É nesse estágio que são relatados erros "regulares" do compilador, como erros de sintaxe ou erros de resolução de sobrecarga com falha.
Linking
O vinculador é o que produz a saída final da compilação a partir dos arquivos de objeto que o compilador produziu. Essa saída pode ser uma biblioteca compartilhada (ou dinâmica) (e, embora o nome seja semelhante, eles não têm muito em comum com as bibliotecas estáticas mencionadas anteriormente) ou um executável.
Ele vincula todos os arquivos de objeto substituindo as referências a símbolos indefinidos pelos endereços corretos. Cada um desses símbolos pode ser definido em outros arquivos de objeto ou em bibliotecas. Se eles estiverem definidos em bibliotecas que não sejam a biblioteca padrão, você precisará informar o vinculador sobre elas.
Nesse estágio, os erros mais comuns estão em falta de definições ou definições duplicadas. O primeiro significa que as definições não existem (ou seja, não foram gravadas) ou que os arquivos ou bibliotecas de objetos em que residem não foram fornecidos ao vinculador. O último é óbvio: o mesmo símbolo foi definido em dois arquivos ou bibliotecas de objetos diferentes.