paulo1205
(usa Ubuntu)
Enviado em 21/04/2018 - 23:30h
ljparaujo escreveu:
E quanto à compilação eu compilei todos os aquivos com o comando: gcc -Wall *c -o principal
Creio que assim que é feita a compilação de todos ao mesmo tento ?
Sim. Resolveu?
Só que, fazendo assim, você mata a principal vantagem da compilação em separado, que é economizar tempo de não ter de recompilar código que não foi alterado desde a última compilação.
A maneira tradicional de fazer pode ser entendida através de um exemplo: suponha que você tem um programa que implementa uma um minissistema de notas fiscais, que, além de interagir com o usuário no console, também se comunica por rede usando um protocolo proprietário e grava dados numa base de persistência com um formato também criado pela sua empresa.
Possivelmente você dividiria esse programa em quatro grandes blocos de códigos correlatos: um para implementar o protocolo de comunicação por rede, um para as operações sobre o repositório de dados, um para a interface com o usuário, e um quarto bloco que conteria a inteligência do seu sistema, recorrendo eventualmente a operações providas pelos três primeiros.
Além dessa divisão macro, cada bloco seria dividido em duas partes: uma que especifica a interface que ficará visível para outros módulos que porventura usem as operações que ele provê, e outra que contém a implementação de tais operações. No caso do C e do C++, a interface geralmente fica contida em arquivos de cabeçalhos (em Inglês,
headers , de onde vem o sufixo “.h” usualmente empregado nos nomes de tais arquivos), que contém apenas as declarações de símbolos globais, macros e definições de tipos de dados, e não provocam geração de código executável. Essas definições são geralmente suficientes para que o compilador conheça os tipos de cada símbolo, e faça as verificações necessárias para garantir que, quando o símbolo for usado, o será de uma maneira válida e consistentes. Já a implementação, que contém a definição dos símbolos (variáveis e funções), é usado para produzir código executável. Tal código não precisa ser visível para outras partes durante a etapa de compilação, mas somente no momento final da produção do executável, chamada de
ligação u
linking , quando todos os nomes de símbolos são finalmente trocados por um endereço bem determinado dentro do programa executável.
Assim dividido, seu programa conteria os seguintes arquivos (os nomes são arbitrários):
• para o protocolo de comunicação,
netcomm.h (interface) e
netcomm.c (implementação);
• para a interface com o usuário,
userui.h (interface) e
userui.c (implementação);
• para a persistência de dados,
companydb.h (interface) e
companydb.c (implementação);
• para a inteligência do sistema, integração das partes, e programa principal,
invoice.c (não precisa de interface, porque as operações que ele traz não serão usadas por outros módulos).
Para produzir o executável (chamado, digamos,
invoice.bin ), você teria que usar o código objeto produzido pelos quatro arquivos “.c”, mas a transformação de cada arquivo “.c” em código objeto poderia ser feita independentemente dos demais. Uma vez que as interfaces de cada módulo tenham atingido a maturidade (isto é, você não vai mais mudar a forma de nenhuma função ou símbolo exportado pela interface visível de nenhum dos módulos), mudanças que tenham de ser feitas na implementação de um módulo (tais como, por exemplo, correções de
bugs ou pequenas evoluções de versão) não impactarão o código objeto dos demais módulos, de modo que, para gerar a nova versão do executável, basta recompilar o módulo alterado e refazer o
linking dos códigos objetos dos módulos que já estavam prontos com aquele que acabou de ser recompilado.
Esse processo é relativamente simples e fácil de automatizar. Programas como o
make ou a configuração de “projetos” oferecidas por ambientes de desenvolvimento integrados permitem definir regras de composição de cada executável a partir de arquivos de objeto e bibliotecas, bem como para a formação de tais objetos e bibliotecas a partir de código fonte em C e C++. Se a data de um arquivo qualquer na cadeia de regras de composição for posterior à do executável já existente, a ferramenta de automatização automaticamente dispara a compilação das partes necessárias ao longo da cadeia, até gerar o(s) executável(is) finais.
No nosso exemplo, um possível conteúdo de arquivo
Makefile (que serve como definição de regras para o
make ) seria algo como o seguinte.
# Variável contendo a lista de arquivos objetos (esses são os produtos
# da compilação de cada arquivo fonte com sufixo “.c”).
OBJECTS=invoice.o companydb.o netcomm.o userui.o
# Além dos objetos produzidos por você pode haver outros objetos ou bibliotecas
# do sistema dos quais seu programa possa depender; Vou supor que netcomm.o
# depende da libresolv, e que userui.c depende da libcurses.
LIBS=-lresolv -lcurses
# Regra genérica para transformar arquivos fontes com sufixo “.c” em arquivos
# objeto com sufixo “.o”. O compilador é chamado com a opção “-c”, que serve
# para apenas compilar os arquivos fontes, sem invocar o linker. O nome do arquivo
# fonte é inferido pelo comando make, e é colocado na linha de comando através
# da variável “$<”.
.c.o:
gcc -Wall -Werror -O2 -pedantic-errors -c $<
# Regra default (que é executada quando se digita apenas “make”), que é a primeira
# regra não-genérica do arquivo, é compilar e ligar todos os executáveis (no nosso
# caso, temos um executável só, então dará na mesma usar “make”, “make all” ou
# “make invoice.bin”.
all: invoice.bin
# Regra específica para gerar “invoice.bin” a partir dos objetos (aqui, o comando gcc
# é usado na função de front-end para o linker, porque o linker padrão do sistema (ld)
# tem uma sintaxe muito mais complexa, que não convém explicar aqui. “$>” significa
# todos os membros da lista de dependências (no caso, é idêntico a $(OBJECTS)), e
# “$@” é o nome do alvo da regra (no caso, “invoice.bin”).
invoice.bin: $(OBJECTS)
gcc -Wall -Werror $> $(LIBS) -o $@