Programação Modular - Complementos de Programação
Quando começamos a programar projectos com alguma dimensão, é usual dividir o código do mesmo por diversos ficheiros a que chamamos módulos. Um módulo é uma colecção de funções que realizam tarefas relacionadas. Podemos dividir um módulo em duas partes:
Parte pública do módulo: Definição de estruturas de dados e funções que devem ser acedidas fora do módulo. Estas definições estão no ``header file'' por convenção. Os "header files" têm extensão .h.
Parte privada do módulo: Tudo o que é interno ao módulo (não visivel pelo mundo exterior). Ficheiro com extensão .c.
A ideia é dividir um problema em secções diferentes e escrever um ``header file'' para cada um dos ficheiros C com as definições de todas as funções e variáveis globais desse ficheiro que devem ser usadas por outros módulos.
Esta abordagem tem diversas vantagens:
Estrutura do programa fica mais clara ao agrupar funções e variáveis relacionadas num mesmo ficheiro.
Possibilidade de compilar cada um dos módulos separadamente, poupando tempo.
A reutilização das funções é facilitada.
Divisão de um programa em módulos
Quando dividimos o código de um programa em diversos módulos, levantam-se algumas questões:
Como é que uma função de um módulo acede a uma outra de outro módulo?
Como é que uma função de um módulo acede a uma variável de outro módulo?
Como é que uma macro é partilhada por diversos módulos?
A resposta está na directiva include do pré-processador do C. A imagem seguinte ilustra a inclusão do ficheiro ("header file") mac.h noutros dois:
Aqui já se vê uma vantagem do uso de módulos, as definições comuns aos dois ficheiros estão armazenadas no "header file" mac.h , quaisquer alterações neste são automaticamente propagadas aos ficheiros que o incluem.
No caso dum módulo necessitar de aceder a funções definidas noutro, os protótipos respectivos devem estar no "header file" correspondente. Por exemplo, posso programar um módulo constituído por dois ficheiros, pilha.h e pilha.c, referentes à implementação de uma pilha. O pilha.c tem a implementação das funções de manipulação da pilha e o pilha.h tem os protótipos das funções implementadas em pilha.c. Agora implemento outro módulo, para usar a pilha, neste tenho que incluir os protótipos das funções que vou usar. A imagem seguinte ilustra esta ideia:
Partilhar variáveis é parecido com partilhar funções. As funções são definidas num ficheiro e um protótipo é partilhado por outros módulos. As variáveis também são definidas algures, por exemplo:
e partilhadas por outros módulos com a declaração:
O extern indica que uma variável está defina fora do módulo corrente (não a define!!!). Podemos então distinguir os seguintes casos:
extern variável: definida noutro ficheiro.
"nenhum" variável: pública.
static variável: local ao ficheiro (privada).
Para variáveis/ficheiros declarados globalmente, significa privado para o ficheiro local. No caso de estar declarado localmente significa que a variável está declarada em memória estática em vez de estar na pilha.
O ficheiro main.c inclui funções definidas em escreve_string.c e declaradas no seu ``header file'' escreve_string.h.
#include "escreve_string.h"
char *OutraString = "Ola a todos!";
printf("\n Começo....\n");
escreveString(A_STRING);
printf("\n Fim....\n");
escreve_string.h:
#define A_STRING "Olá mundo"
void escreveString(char *);
O escreve_string.c:
extern char *OutraString;
void escreveString(char *estaString){
printf("\n%s\n",estaString);
printf("\n A variavel global: %s declarada no módulo e definida no main",OutraString);
A compilação dos diferentes ficheiros num só executável faz-se da seguinte forma:
gcc -o mod main.c escreve_string.c
Protecção de "header files"
A inclusão de um mesmo "header file" mais do que uma vez origina problemas. No entanto esta situação não é muito rara. Por exemplo, se f1.h incluir f3.h, f2.h incluir f3.h e prog.c incluir f1.h e f2.h, f3.h será incluído duas vezes o que origina um erro de compilação. A imagem seguinte ilustra esta ideia:
Para resolver este problema recorremos mais uma vez ao pré-processador. Por exemplo, se quisermos proteger o "header file" do exemplo anterior escrevemos o seguinte:
#ifndef ESCREVE_STRING_H
#define ESCREVE_STRING_H
Este "header file" passa a ser identificado por ESCREVE_STRING_H e só será incluído uma vez.
O utilitário make
O make é um utilitário que permite automatizar o processo de compilação de múltiplos ficheiros em simultâneo. Conforme o número de ficheiros aumenta, mais argumentos o programador tem que escrever no momento da compilação. Mudanças num só ficheiro levam à recompilação de todo o projecto e consequente perda de tempo, com o make só os ficheiros alterados e os que dependem deles são recompilados. O ficheiro Makefile inclui uma série de secções onde são definidas macros e regras para a compilação.
Se temos os ficheiros: ola.c, ola.obj e ola.exe e for feita uma alteração no ola.c, o make reconhece essa alteração e cria novamente os outros ficheiros. Se estivermos a tratar de vários ficheiros ``obj'' de diversas fontes que constituem um único executável e for feita uma alteração em algum ficheiro de código fonte, o make trata de compilar somente o ``obj'' correspondente e o executável.
O MakeFile é constituído por regras de dependência e de construção. Uma regra de dependência tem 2 partes:
lado esquerdo (alvo) : lado direito (ficheiros de cujo alvo depende)
Se o alvo está desactualizado no que diz respeito aos seus constituintes então as regras de construção que seguem as regras de dependência são verificadas.
Execução do utilitário make
Quando se executa o make as seguintes tarefas são executadas:
O Makefile é lido. O Makefile indica que objectos e bibliotecas necessitam de ser ``linkados'' e quais ``headers'' e ``sources'' necessitam ser compilados para criar cada objecto.
É verificada a data e hora de cada ficheiro ``obj'' e é comparada com a data e hora de cada ``source'' e ``header'' do qual depende. Recompila caso necessário.
A data e hora de cada objecto é comparada com cada executável. Recompila caso necessário.
Criação de um Makefile
CFLAGS =# -g
mod: main.o escreve_string.o
$(CC) $(CFLAGS) -o mod main.o escreve_string.o
main.o: main.c escreve_string.h
escreve_string.o: escreve_string.c
$(CC) $(CFLAGS) -c escreve_string.c
rm -rf mod main.o escreve_string.o
Um exemplo mais completo envolvendo estes conceitos pode ser obtido aqui.
Apêndice A: Pré-processador
O pré-processador é a primeira fase de compilação de um programa em C. O pré-processador manipula apenas o texto do programa fonte servindo-se de uma linguagem própria bastante poderosa que pode vir a tornar-se muito útil para o programador. Todas as directivas do pré-processador começam com o caractere #.
Usado para definir constantes ou substituições mais poderosas denominadas macros.
Sempre que quisermos redefinir um nome com outra directiva #define , temos que o indefinir primeiro com #undef.
Lê um arquivo de texto e inclui o seu conteúdo no local da directiva. Duas formas possíveis:
#include <nome_arquivo>
#include "nome_arquivo"
O uso de <> indica que o pré-processador deve procurar o ficheiro na directoria de bibliotecas do sistema. O uso de "" indica que o pré-processador deve procurar o ficheiro no directório actual em primeiro lugar. No contexto do programação em módulos, usa-se sempre "" para incluir ficheiros do projecto em causa.
O #if - inclusão condicional
O #if avalia uma expressão inteira e executa uma acção baseada no resultado (interpretado como TRUE ou FALSE). Esta directiva é sempre terminada com a palavra #endif. Entre #if e #endif poderão estar as directivas #else e #elif.