|
A compilação de um programa C/C++ envolve três passos principais: pré-processador, compilação e linkagem, neste artigo vamos ver o trabalho que cada um desses passos realiza.
Quando invocamos a compilação na nossa IDE favorita ou usando um makefile para cada arquivo C/C++ (arquivos .c, cpp, cc, etc) é executado o pré-processador (que executa as diretivas #ifdef, #include, substituições de macros, etc), o resultado disso é um arquivo temporário com o código fonte pré-processado Em seguida é invocado o compilador que processa este arquivo, caso não ocorram erros é gerado o arquivo objeto (geralmente extensão obj ou .o). Após a geração de todos arquivos objetos é invocado o linker, que faz a linkagem de todos os arquivos objeto gerando finalmente o executável (exe no caso do windows) ou biblioteca dinâmica (dlls no Windows).
Pré Processador
O pré-processador é a primeira etapa da compilação, pode ser ou não um programa separado do compilador, mas o que importa mesmo é que ele é o primeiro a “tocar” o código e cuida de algumas tarefas como:
- Junta linhas que foram separadas por sequências de escape.
- Remove comentários e os substitui por espaços em branco
- Expande macros
- Processa diretivas de pré-processamento
O comando de pré-processamento que provavelmente é mais pelos desenvolvedores é o include, que é resolvido pelo pré-processador. O include consiste em apenas abrir o arquivo indicado, copiar o seu conteúdo e colar em cima do comando include, sendo que o include pode ter duas formas:
- #include <stdio.h>
- #include “objeto.h”
Na primeira forma, usando-se < e > o pré-processador procura pelo arquivo indicado apenas nos diretórios de include que o compilador estiver configurado, já na segunda opção com aspas, o pré-processador procura pelo arquivo no diretório dos fontes do programa.
Vale lembrar que arquivos de header (extensões h, hpp, etc) não são compilados diretamente pelo compilador, mas apenas quando são incluídos em algum arquivo de código (c, cpp, cc, etc).
Sobre a compilação condicional é quando usamos comandos como #ifdef, #if, etc. Estes comandos são bem uteis quando queremos compilar certas partes do código em apenas algumas situações, exemplo:
FILE *abre_arquivo(const char *fileName)
{
#ifdef DEBUG
printf("Abrindo %s\n", fileName);
#endif
return fopen(fileName, "rb");
}
No código acima o printf é executado apenas quando DEBUG é definido, que pode ser feito usando-se “#define DEBUG” em algum ponto do código ou configurando o compilador para fazer isso automaticamente, no caso do visual, clique com botão direito no projeto (na aba “Solution Explorer”), secione Propriedades, depois C/C++ –> Pre-Processor e finalmente coloque na caixa de texto “Preprocecssor Definitions” o nome do seu macro.
O comando #ifdef é útil também na hora da portabilidade, podemos forçar a inclusão de determinados arquivos em diferentes plataformas, ligar e desligar testes extras em compilações para debug, etc.
Um detalhe do pré processador que passa despercebido pela maioria no inicio é que os macros não são nada além de substituição de texto, exemplo:
#define TAM 100 int meuArray[TAM];
O código acima após o pré-processamento deve ficar idêntico código abaixo:
int meuArray[100];
Sendo assim, este é o código que o pré-processador vai gerar, ou seja, para o compilador C/C++ TAM nunca existiu, pois quando o compilador foi processar o código fonte TAM e os demais macros já vão ter sido removidos e substituídos onde tiver sido necessário.
No caso do C++ é recomendável usar "const" ao invés de macros para definir constantes, pois estas minimizam a chance de erros, principalmente pelo fato que vão ser compiladas pelo compilador C++ e este poderá fazer verificações, como, por exemplo, se os tipos estão corretos
Compilador C/C++
Agora que já temos uma noção melhor do trabalho do pré-processador vamos entender o papel do compilador. O compilador processa o resultado do pré-processador e para cada unidade compilação gera um arquivo objeto.
O compilador C trata cada arquivo fonte como sendo uma unidade de compilação, isso quer dizer que ele compila cada um de maneira independente, ignorando a existência de qualquer outro arquivo. A única forma do compilador considerar um outro arquivo é através da diretiva include, que já foi processada no passo anterior pelo pré-processador.
Resumindo: se seu código em um arquivo a.c tenta acessar uma função que tenha sido declarada apenas em b.c isso gera um erro de compilação no arquivo a.c, este erro pode ser evitado declarando-se (note que declarar não é definir) novamente a função.
Com base no que foi explicado até o momento já é possível ver que seria possível programar em C/C++ sem usar includes, apenas declarando-se funções e o que mais for preciso em cada arquivo fonte, mas isso daria um trabalhão e não seria nada produtivo.
O pré-processador permite que o construtor do compilador possa focar seus esforços na principal tarefa deste, que consiste em verificar se o código esta bem construído segundo a gramática da linguagem. Após verificar que não existe qualquer erro sintático (como, por exemplo, um ; faltando) ou semântico (como, por exemplo, uma variável definida duas vezes) ele já pode então gerar o código em linguagem de maquina (note que essa é uma explicação extremamente simples das tarefas de um compilador) que resulta no arquivo objeto.
No arquivo objeto, quando o compilador precisa inserir uma chamada para uma função x definida em outro arquivo, este coloca apenas uma anotação que ali a função deve ser chamada, pois como ele não compilou esta função (tendo visto apenas uma declaração desta) ele não sabe dizer em qual endereço ela esta. Dessa forma as chamadas de funções e acesso a variáveis globais em arquivos objetos são apenas temporários.
Linker
Finalmente, após pré-processar, compilar cada arquivo fonte e gerar todos os arquivos objetos podemos invocar o linker. O linker ou programa de ligações é responsável por juntar todos os arquivos objetos e gerar o arquivo executável (pode ser usado também para gerar bibliotecas dinâmicas, como dlls).
Dessa forma a tarefa dele é substituir todos as chamadas de funções e acessos a variáveis em arquivos objeto pelo endereço real de onde esse item se encontra, vemos também que ele fica responsável por organizar cada função, variável dentro do espaço de memória do executável.
Perceba então que o linker é o responsável pelo erro que persegue muitos iniciantes, vamos ver um exemplo:
void proc();
int main(int argc, char* argv[])
{
proc();
return 0;
}
Compilando esse código com o visual temos a seguinte mensagem de erro:
- error LNK2019: unresolved external symbol _proc referenced in function _main
O erro acima (do visual) indica que houve um erro de linkagem, pois o simbolo “_proc” que é referenciado em “_main” não foi encontrado. Detalhe que a compilação foi bem sucedida, pois para o compilador não interessa se a função “proc” realmente existe ou não, o que importa é que ela foi declarada. As funções compiladas pelo C são geradas com um “_” devido ao name mangling, que é usado para evitar conflitos de nomes.
Mas o linker não conseguiu encontrar a definição desta função, então temos o erro. É bem comum termos esse erro quando usamos alguma biblioteca externa e esquecemos de linkar os arquivos lib (ou .a) com o projeto.
Alias, falando em bibliotecas, é tarefa do linker juntar no executável final qualquer biblioteca extra que for especificada, mas como ele não possui super-poderes é preciso dizer a ele quais bibliotecas usar (e cada compilador e/ou IDE costumar ter a sua forma).
Por fim, se o linker conseguir encontrar todos os simbolos ele vai finalmente gerar o arquivo executavel e o programa poderá ser usado!
Notas Finais
Vimos aqui um resumo (não muito pequeno) de como funciona o processo de compilação de um programa C/C++. Infelizmente não é possível cobrir todos os tópicos em um único artigo (sem que este fique muito extenso), então para se aprofundar um pouco mais no assunto sugiro a leitura:
-
17/02/2011 22:43:48 |200.165.239.xxx| Carolina Fantini

Gostei muito do artigo Bruno, aprendi umas coisinhas básicas que eu não sabia. ^^
-
28/04/2011 18:00:18 |187.13.178.xxx| Marcelo Pereira Diniz. - Linguagem de programação.

Quero me tornar um xper na programação de computadores. Quais são os melhores livros pois eu acho as informações na internet um pouco fraca.
-
28/04/2011 18:06:46 | Bruno Crivelari Sanches

Tem bastante material bom, mas tem que garimpar e ir vasculhando, muitas vezes tem que se ir coletando informações de diversos lugares.
Se seu interesse é C++, de uma olhada no nosso roadmap: http://www.pontov.com.br/site/index.php/cpp/46-conceit os-basicos/88-roadmap-c











E aí guri! Ta na área né, como andam as coisas, depois da Ignis não nos comunicamos mais! To trabalhando num studio de outsourcing www.playlore.com dá uma olhada! abraços e boa sorte!