Mesmo que o objectivo não seja a reutilização de código, a modularização física através da colocação de diferentes ferramentas (funções, procedimentos, classes, etc.) em ficheiros separados é muito útil em qualquer aplicação minimamente realista. Um módulo físico de um programa é constituído (normalmente) por dois ficheiros: o ficheiro de cabeçalho (interface ou header), com extensão .h, e o ficheiro fonte (de implementação ou source), com extensão .cpp (outros sistemas usam também as extensões .C e .CC).
Para cada módulo deve tipicamente existir um ficheiro com extensão .h, que contém a interface das funções, procedimentos e classes definidas pelo módulo. I.e., os ficheiros de cabeçalho contêm a declaração de funções e procedimentos disponibilizados pelo módulo respectivo, bem como a definição das classes disponibilizadas pelo módulo. Cada definição de classe contém a declaração das respectivas funções e procedimentos membro, a definição das variáveis membro de instância, a declaração das variáveis membro de classe, e a declaração das funções e procedimentos amigos da classe.
Por exemplo, suponha que existe uma classe MinhaClasse com o operador == definido, e que se pretendia disponibilizá-la para utilizações futuras. Nesse caso, deveria existir um ficheiro de cabeçalho minhaclasse.h com o seguinte aspecto:
// As duas primeiras linhas são instruções dadas ao pré-processador para evitar
// inclusões múltiplas deste ficheiro de cabeçalho:
#ifndef MINHACLASSE_H
#define MINHACLASSE_H#include <iostream>
class MinhaClasse {
int x;
int y;
public:
int f1(int, int);
int f2();friend std::ostream& operator << (std::ostream&, MinhaClasse);
};inline int MinhaClasse::f2() {
// Definição da função f2().
}bool operator == (MinhaClasse, MinhaClasse);
#endif // MINHACLASSE_H
Note-se que a função membro f2(), sendo inline,
é definida no ficheiro de cabeçalho.
O correspondente ficheiro fonte, minhaclasse.cpp, deveria ter o seguinte aspecto:
// Inclusão da interface das ferramentas matemáticas utilizadas.
// O pré-processador copia para este ponto o conteúdo integral do ficheiro cmath
// (que está no directório */Vc/include/) e que contém definições e declarações de
// classes e/ou funções matemáticas.
#include <cmath>// O pré-processador copia para este ponto o conteúdo integral do ficheiro
// minhaclasse.h (que está, presume-se, no directório corrente, daí as aspas
// "" em vez de <>). Isto evita ter de repetir as definições e declarações
// constantes no ficheiro de cabeçalho. As inclusões com <> reservam-se
// normalmente para utilização de bibliotecas pré-definidas.
#include "minhaclasse.h"int MinhaClasse::f1(int i, int j)
{
// Definição da função f1().
}// A função f2(), sendo inline, não se redefine aqui.
std::ostream& operator << (std::ostream& saída, MinhaClasse m)
{
// Definição do operador de escrita para variáveis da classe MinhaClasse.
}bool operator == (MinhaClasse a, MinhaClasse b)
{
return a.x == b.x;
}
Para criar um programa executável a partir de diferentes ficheiros é necessário apenas incluir cada um dos ficheiros fonte (extensão .cpp) num projecto e "construir" (fazer Build) esse projecto. O processo que de facto ocorre durante a construção é descrito brevemente abaixo.
Inicialmente o pré-processor age sobre cada ficheiro fonte, incluindo neles todos os ficheiros de cabeçalho especificados por directivas #include e executando todas as directivas de pré-processamento (que correspondem a linhas precedidas de um #).
Depois, é feita a compilação propriamente dita. Esta divide-se em vários passos:
Por vezes um conjunto de módulos relacionados são unidos numa única biblioteca. I.e., os ficheiros objecto respectivos são transformados num único ficheiro de extensão .lib (ou .a em Unix) a que se chama um ficheiro biblioteca. Assim, a uma única biblioteca correspondem usualmente vários ficheiros de cabeçalho, um por cada módulo na biblioteca.
Em muitos casos reais apenas estão disponíveis para o utilizador os ficheiros .h e o respectivo .obj (ou .lib), o que permite esconder parcialmente o código de implementação do módulo ou biblioteca, sem impossibilitar a sua utilização noutros projectos. É o que se passa com a biblioteca padrão do C++, a que correspondem inúmeros ficheiros de cabeçalho (iostream, cmath, cstdlib, string, etc.).
Finalmente, depois de todos os módulos terem sido compilados, e criados os ficheiros objecto correspondentes, procede-se à fusão (linkagem) de todos os ficheiros objecto e biblioteca utilizados num único ficheiro executável. Esta é a tarefa do chamado linker.
Cada ficheiro fonte dá origem a um ficheiro objecto que contem o código máquina correspondente às funções e procedimentos definidos pelo módulo, uma tabela das funções e procedimentos disponibilizados pelo módulo e uma tabela de funções e procedimentos usados pelo módulo mas que deverão estar definidos noutro lado. O processo de fusão integra todos os ficheiros objecto num único executável, verificando se todas as funções e procedimentos necessários estão de facto definidas em algum dos módulos e verificando se não existem repetições. Note-se que o Visual C++, durante o processo de fusão, funde também automaticamente o ficheiro biblioteca da biblioteca padrão. Isso significa que, embora não tivesse dado conta, já utilizou o conceito de compilação separada muitas vezes!
Estes assuntos serão pormenorizados na disciplina de Sistemas Operativos.
// Ficheiro ContaDoManel.h contendo a declaração da classe Conta feita pelo Manel.namespace Manel {
// Tudo o que está dentro deste bloco faz parte do espaço nominativo Manel, e o
// seu nome completo é Manel::* . Por exemplo, Manel::Conta.class Conta {
...
};
}
// Ficheiro ContaDoJaquim.h contendo a declaração da classe Conta feita pelo Jaquim.namespace Jaquim {
// Tudo o que está dentro deste bloco faz parte do espaço nominativo Jaquim, e o
// seu nome completo é Jaquim::* . Por exemplo, Jaquim::Conta.class Conta {
...
};
}
// Programa que utiliza ambas as contas:ou, alternativamente,
#include "ContaDoManel.h"
#include "ContaDoJaquim.h"...
int main()
{
Manel::Conta A; // A é uma variável do tipo conta definida no espaço Manel.
Jaquim::Conta B; // B é uma variável do tipo conta definida no espaço Jaquim....
}
// Programa que utiliza ambas as contas:A directiva de utilização using namespace nome; como que transfere todo o conteúdo do espaço nominativo nome para o espaço nominativo corrente. Isto é usualmente indesejável. Pode-se transferir apenas um determinado nome usando uma declaração de utilização:
#include "ContaDoManel.h"
#include "ContaDoJaquim.h"...
// Esta instrução faz com que seja possível prescindir da utilização do prefixo Manel:: para as definições existentes no espaço Manel:
using namespace Manel;...
int main()
{
Conta A; // A é uma variável do tipo conta definida no espaço Manel.
Jaquim::Conta B; // B é uma variável do tipo conta definida no espaço Jaquim.// Note-se que neste caso continua a ser obrigatório o uso de Jaquim:: cada
// vez que se pretende usar uma definição existente no espaço Jaquim....
}
// Programa que utiliza ambas as contas:
#include "ContaDoManel.h"
#include "ContaDoJaquim.h"...
int main()
{
// Esta instrução faz com que seja possível prescindir da utilização do prefixo
// Manel:: para referir a classe Conta apenas, e só dentro da função main():
using Manel::Conta;
Conta A; // A é uma variável do tipo conta definida no espaço Manel.
Jaquim::Conta B; // B é uma variável do tipo conta definida no espaço Jaquim....
}
int n = lerNumero("Diga um número: ");Coloque a função num ficheiro fonte utilitarios.cpp. Construa o correspondente ficheiro de cabeçalho utilitarios.h.
cout << "O número seguinte é " << n << endl;
1.b) Usando a função definida, faça um programa de teste para ler 5 números e calcular a sua média. Como é evidente a função main() não deverá ser colocada no ficheiro utilitarios.cpp.
1.c) A ideia é reunir no módulo utilitarios uma série de pequenas funções e/ou classes úteis no trabalho do dia a dia. Sempre que desenvolver uma função que lhe pareça que possa ser útil noutros contextos, junte-a ao módulo utilitarios. Por exemplo, pode juntar já uma para fazer a pergunta da praxe: "Quer continuar?". A essa pergunta o utilizador responde com S (ou s) ou N (ou n).
2.a) Faça uma classe ContadorCircular para contagem circular de 0 a n-1. Isto é uma classe que, dado um n = 5 por exemplo, conte 0, 1, 2, 3, 4, 0, 1, 2, 3, 4, 0, ...
A classe deve permitir as seguintes operações:
ContadorCircular c(5);deverá escrever no ecrã
c.conta();
c.conta();
c.conta();
cout << c.valor();
for(int i = 0; i != 4; i++)
c.conta();
cout << c.valor();
3Faça um programa deste tipo para testar a classe.
2
Junte a classe ao módulo utilitarios.
2.b) Faça um programa que leia uma série de números, parando quando aparecer o número 100, e mostre os três últimos números lidos por uma ordem arbitrária (se não foram lidos três números pode mostrar zeros no seu lugar).
Sugestões:
3.b) Faça um programa que gere 1000 números aleatórios entre 1 e 7 e mostre quantas vezes saiu cada um deles.
Sugestão: Faça primeiro uma função que, usando a função rand(), gere um número aleatório entre 1 e 7.
4.a) Faça uma classe Média para calcular a média de um conjunto de valores inteiros. A classe pode deve suportar as seguintes operações:
Junte a classe ao módulo utilitarios.
4.b) Faça um programa para calcular a classificação de uma competição desportiva, que é feita de acordo com o seguinte esquema:
Há um júri de oito elementos. Cada elemento do juri dá uma pontuação entre 1 e 10. Calcula-se a média e retira-se a pontuação que esteja mais longe dessa média. A classificação final é a média das restantes sete pontuações.
O programa lê as oito pontuações, calcula e mostra a classificação final e o número do elemento do júri cuja pontuação foi retirada. Use a classe Média.
[2] Bjarne Stroustrup, "The C++ Programming Language", 3ª edição, Addison-Wesley, Reading, Massachusetts, 1997. #
* Encomendados 10 exemplares pela biblioteca do ISCTE. Espera-se a sua disponibilização em breve.