Esta é provavelmente uma resposta mais detalhada do que você queria, mas acho que uma explicação decente é justificada.
Em C e C ++, um arquivo de origem é definido como uma unidade de conversão . Por convenção, os arquivos de cabeçalho contêm declarações de função, definições de tipo e definições de classe. As implementações de funções reais residem em unidades de conversão, como arquivos .cpp.
A idéia por trás disso é que funções e funções de membro de classe / estrutura são compiladas e montadas uma vez; outras funções podem chamar esse código de um local sem criar duplicatas. Suas funções são declaradas como "externas" implicitamente.
/* Function declaration, usually found in headers. */
/* Implicitly 'extern', i.e the symbol is visible everywhere, not just locally.*/
int add(int, int);
/* function body, or function definition. */
int add(int a, int b)
{
return a + b;
}
Se você deseja que uma função seja local para uma unidade de conversão, defina-a como 'estática'. O que isto significa? Isso significa que, se você incluir arquivos de origem com funções externas, obterá erros de redefinição, porque o compilador encontra a mesma implementação mais de uma vez. Portanto, você deseja que todas as suas unidades de tradução vejam a declaração da função, mas não o corpo da função .
Então, como tudo se mistura no final? Esse é o trabalho do vinculador. Um vinculador lê todos os arquivos de objeto gerados pelo estágio do assembler e resolve os símbolos. Como eu disse anteriormente, um símbolo é apenas um nome. Por exemplo, o nome de uma variável ou função. Quando as unidades de conversão que chamam funções ou declaram tipos não sabem a implementação dessas funções ou tipos, diz-se que esses símbolos não foram resolvidos. O vinculador resolve o símbolo não resolvido conectando a unidade de conversão que mantém o símbolo indefinido junto com o que contém a implementação. Ufa. Isso é válido para todos os símbolos visíveis externamente, sejam eles implementados no seu código ou fornecidos por uma biblioteca adicional. Uma biblioteca é realmente apenas um arquivo com código reutilizável.
Existem duas exceções notáveis. Primeiro, se você tiver uma função pequena, poderá integrá-la. Isso significa que o código de máquina gerado não gera uma chamada de função externa, mas é literalmente concatenado no local. Como geralmente são pequenas, o tamanho da sobrecarga não importa. Você pode imaginar que eles sejam estáticos no modo como funcionam. Portanto, é seguro implementar funções embutidas nos cabeçalhos. As implementações de funções dentro de uma definição de classe ou estrutura também são frequentemente incorporadas automaticamente pelo compilador.
A outra exceção são os modelos. Como o compilador precisa ver toda a definição do tipo de modelo ao instancia-los, não é possível dissociar a implementação da definição como nas funções independentes ou nas classes normais. Bem, talvez isso seja possível agora, mas obter amplo suporte do compilador para a palavra-chave "export" levou muito, muito tempo. Portanto, sem suporte para 'exportação', as unidades de tradução obtêm suas próprias cópias locais de tipos e funções instanciados de modelo, semelhante à maneira como as funções embutidas funcionam. Com suporte para 'exportação', este não é o caso.
Para as duas exceções, algumas pessoas acham "melhor" colocar as implementações de funções embutidas, funções e tipos de modelo nos arquivos .cpp e #incluir o arquivo .cpp. Se isso é um cabeçalho ou um arquivo de origem, não importa; o pré-processador não se importa e é apenas uma convenção.
Um resumo rápido de todo o processo, do código C ++ (vários arquivos) e até o executável final:
- O pré - processador é executado, que analisa todas as diretivas que começam com um '#'. A diretiva #include concatena o arquivo incluído com inferior, por exemplo. Também faz substituição de macro e colagem de token.
- O compilador real é executado no arquivo de texto intermediário após o estágio do pré-processador e emite o código do assembler.
- O montador é executado no arquivo de montagem e emite código de máquina, geralmente chamado de arquivo de objeto e segue o formato executável binário do sistema operacional em questão. Por exemplo, o Windows usa o PE (formato executável portátil), enquanto o Linux usa o formato Unix System V ELF, com extensões GNU. Nesta fase, os símbolos ainda estão marcados como indefinidos.
- Finalmente, o vinculador é executado. Todos os estágios anteriores foram executados em cada unidade de tradução em ordem. No entanto, o estágio do vinculador funciona em todos os arquivos de objetos gerados que foram gerados pelo assembler. O vinculador resolve símbolos e faz muita mágica, como criar seções e segmentos, que depende da plataforma de destino e do formato binário. Os programadores não precisam saber disso em geral, mas certamente ajuda em alguns casos.
Novamente, isso foi definitivamente mais do que você pediu, mas espero que os detalhes minuciosos ajudem você a ver a imagem maior.