Bibliotecas estáticas e dinâmicas
Bibliotecas são blocos de código que são reutilizáveis em vários programas. Usá-las economiza tempo, eliminando a necessidade de reescrever o código várias vezes.
Bibliotecas estáticas (geralmente, em Windows, arquivos com extensão .lib
), embora reutilizáveis em vários programas, são bloqueadas em um programa no build. Por outro lado, bibliotecas dinâmicas ou compartilhadas (em Windows, extensão .dll
) existem como arquivos separados fora do arquivo executável.
Neste apêndice, apresento alguns fundamentos do processo de build do C++, fundamentais, para que você consiga ser “efetivo” em menos tempo, com menos chances de “atirar no próprio pé”.
Um péssimo trabalhador culpa suas ferramentas.
Provérbios Islandenses |
Compilação e linking
Comecemos simples. O build em C++ inclui duas etapas principais: compilação e linking.
O processo de compilação processa todos os arquivos contendo código-fonte individualmente e de maneira isolada. Ou seja, para o compilador, quando um arquivo está sendo processado é como se fosse o único, devendo dispor, de alguma meneira, todas as informações necessárias para sua validação.
Obviamente, nenhum “arquivo com código-fonte” é uma ilha. A razão de um sistema ter vários arquivos é, exatamente, dividir o código, segundo algum critério, para facilitar manutenção e evolução, de forma que é natural que código de um arquivo “chame” código (funções e tipos de usuário) em outros arquivos. A “ligação” entre os programas de diversos arquivos com código-fonte é realizada através do processo linking.
O compilador “entende” o código “de cima para baixo”
Como já foi dito, durante a compilação, cada arquivo é processado individualmente e de maneira isolada. Mais do que isso, na medida em que o compilador “lê” um código em um arquivo, precisa conhecer qualquer referência utilizada de maneira a determinar sua validade e correção.
No código que segue, por exemplo, o compilador consegue “entender” o código porque, não há, em momento algum, informação faltando para validações.
// main.cpp int doSomething() { return 0; } int main(){ auto a = doSomething(); // ... }
Já o código abaixo não compila! No momento em que acontece chamada para a função doSomething
ela ainda não é “conhecida”.
// main.cpp int main(){ auto a = doSomething(); // THIS CODE DOES NOT COMPILE! // ... } int doSomething() { return 0; }
Para que não nos preocupemos com a ordem em que as funções aparecem no código, a saída é incluir no início “definições” das implementações que aparecerão a seguir. No exemplo que segue, definimos a função doSomething
antes de qualquer evocação. Dessa forma, o compilador “sabe” que há uma função com esse nome e, também, consegue determinar corretamente o tipo da variável a
.
// main.cpp int doSomething(); int main(){ auto a = doSomething(); // ... } int doSomething() { return 0; }
Essa medida, aliás, autoriza “levar” a implementação concreta de doSomething
para outro arquivo com código fonte (func.cpp
).
// func.cpp int doSomething() { return 0; }
#include
explicado
Incluir no início de um arquivo de código-fonte uma relação exaustiva de declarações de funções e tipos (classes e estruturas) torna o código mais difícil de entender, manter e evoluir. Afinal, caso ocorram breaking changes nas implementações concretas, elas só serão percebidas durante o linking.
O “estilo C++” para lidar com declarações é, para cada arquivo de código-fonte, criar um arquivo de “cabeçalho” relacionando as definições de funções e tipos que ele devem ficar expostas.
// func.h #ifndef FUNC_H #define FUNC_H int doSomething(); #endif /* FUNC_H */
Os arquivos de cabeçalho são, então, “incluídos” em arquivos de código-fonte que serão consumidores.
// main.cpp #include "func.h" int main(){ auto a = doSomething(); // ... }
A inclusão, é executada pela diretiva de pré-processamento #include
, antes do trabalho do compilador, por um “pré-processador” que, literalmente, deixa o código pronto para a compilação.
Pré-processador
Pré-processamento é uma espécie de “etapa prévia” executada antes da compilação e é executado por um “motor” designado como “pré-processador”.
O pré-processador procura quaisquer diretivas de pré-processamento (linhas de código começando com um #
) e altera o código de alguma forma, geralmente adicionando ou removendo linhas.
A diretiva #include
, por exemplo, literalmente insere o conteúdo do arquivo especificado na posição em que aparece.
As diretivas #ifndef
, #define
e #endif
ajudam o pré-processador a não “incluir acidentalmente” um mesmo conjunto de definições mais de uma vez.
#pragma once
As diretivas #ifndef
, #define
e #endif
para impedir duplicidade de inclusão de declarações é um pattern consolidado, aderente a especificação do C++. Entretanto, é uma solução “verbosa” para um problema recorrente.
Atualmente, todos os compiladores do mercado suportam a diretiva #pragma once
que atende ao mesmo objetivo do pattern.
// func.h #pragma once int doSomething();
Macros
O pré-processador do C++ é tremendamente poderoso e perigoso. Ele “altera” o código fonte, expandindo trechos conforme definições, aparentando “enganosamente” uma função.
#define MAX(a,b) (a > b ? a : b) #include <iostream> using namespace std ; int main() { int x = 10, y = 20; cout << "Macro Max(x,y) = " << MAX(x,y) << endl; return 0; }
No exemplo acima, a macro MAX
, quando interpretada pelo pré-processador altera o código-fonte fazendo substituição conforme indicado no modelo. Após o pré-processador, o código resultante será como segue:
// ... conteudo de iostream ... using namespace std ; int main() { int x = 10, y = 20; cout << "Macro Max(x,y) = " << (x > y ? x : y) << endl; return 0; }
Macros são “entregues” pelo pré-processador que não valida, de forma alguma, a correção do código. O exemplo abaixo, por exemplo, não acusa falta de um parêntese na macro. O erro será indicado, só mais tarde, pelo compillador, na linha onde aconteceu a substituição.
#define MAX(a,b) (a > b ? a : b #include <iostream> using namespace std ; int main() { int x = 10, y = 20; cout << "Macro Max(x,y) = " << MAX(x,y) << endl; return 0; }
No passado, macros eram utilizadas em demasia e dificultavam consideravelmente a identificação de erros de compilação. Modernamente, os benefícios do inlining proporcionado pelas macros é resolvido de maneira mais eficiente com o modificador inline
em funções.
Há bem mais…
Essa é apenas uma breve introdução a como funciona o build do C++. Há muitos detalhes e recursos adicionais que podem e precisam ser explorados.
Excelente, Elemar! Conciso e didático
Bom dia, Elemar Jr.
Gostei muito do artigo.
Apesar de não ser programador C++, já estudei na faculdade.
Todo conhecimento é bem vindo.
Há poucas semanas, li o outro artigo sobre C++ moderno, onde falava do tipo ‘auto’.
Tenho acompanhado o canal no YouTube, eximiaCo e aprendido sobre DDD e Arquitetura.
Abraços