Aula 1

1  Leitura independente

A matéria dos Capítulos 1, 2 e 3 do livro de texto seleccionado para a cadeira [1] não será coberta na íntegra pelas aulas teóricas, visto ser em grande parte uma revisão da matéria do primeiro semestre (Programação I e Introdução à Programação).  Recomenda-se por isso a sua leitura independente pelos alunos.

2  Resumo da matéria

2.1  Modularização física

No primeiro semestre viu-se que a forma mais natural de reaproveitar código é através de funções e procedimentos.  Viu-se também como se criavam novos tipos de dados e como se equipavam esses novos tipos das respectivas operações.  Mas como, depois de escrita uma função ou criado um novo tipo (e.g., uma classe), se pode utilizar o código produzido em diferentes programas?  Isso consegue-se através dum mecanismo de compilação separada de módulos físicos correspondentes a diferentes ficheiros.

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;
}

2.2  Fusão de um conjunto de ficheiros num único executável

O processo de criação de programas constituídos por diferentes módulos físicos, i.e., por diferentes ficheiros, está (no caso do ambiente de desenvolvimento do Visual C++ que estamos a usar) "disfarçado" sob a capa da gestão de um projecto.

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:

  1. Análise lexical (onde são verificados todos os símbolos existentes no código e a sua correcção).
  2. Análise sintáctica (onde são verificados erros gramaticais no código, por exemplo conjuntos de símbolos que não fazem sentido).
  3. Análise semântica (onde é verificada a adequação dos tipos de dados, por exemplo).

  4. [Durante estas primeiras fases é gerada a maior parte das mensagens de compilação que aparecem na janela de compilação do Visual C++.]
  5. Optimização (eliminação de código redundante, etc.).
  6. Geração de código máquina.
No final da compilação de um ficheiro fonte é gerado um ficheiro objecto, com extensão .obj (ou .o em Unix).  Este ficheiro contém código máquina utilizável (sem necessidade de compilação) por outros projectos.  Se o ficheiro fonte for alterado já depois de compilado, o ambiente Visual C++ encarregar-se-á de o recompilar quando necessário.

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.

2.2.1 Projectos

Os projectos são mais do que um mero conjunto de módulos.  No Visual C++ um projecto contém muita informação acerca do modo como os componentes do projecto devem ser integrados para produzir o programa final.  Estes projectos estão escondidos do utilizador, que acede a eles através dos menus do Visual C++.  Em Unix básico, pois também existem ambientes integrados de desenvolvimento para Unix, consegue-se o mesmo efeito usando as chamadas makefiles.

2.2.2 Espaços nominativos

Um dos problemas mais comuns em grandes projectos de programação é a existência de classes, variáveis, funções ou procedimentos com o mesmo nome, embora desenvolvidos por pessoas diferentes e, por vezes, com objectivos diferentes.  Este facto é normalmente referido como levando a uma "colisão" de nomes.  Por exemplo, imagine que dois colegas seus estão a escrever classes, que serão integradas no mesmo programa, e que ambos chamam à sua classe Conta.  Quando o programa que usa essas classes for compilado, será emitido um erro que indica que há duas definições incompatíveis de Conta.  A solução para esse problema passa pela utilização de espaços nominativos (namespaces).  Um espaço nominativo envolve um conjunto de definições e declarações num invólucro com um determinado nome, o que permite que não haja ambiguidade quando são utilizadas.  Veja-se a solução para o problema mencionado acima:
// 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:
#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.

    ...
}

ou, alternativamente,
// Programa que utiliza ambas as contas:
#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.

    ...
}

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:
// 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.

    ...
}

2.3  Leitura recomendada

Recomenda-se a leitura do Capítulo 9 de [2], visto que o livro de texto da cadeira [1] não cobre a matéria sobre espaços nominativos (namespaces).

2.4  Exercícios

1.a)  Faça uma função int lerNumero(const string& mensagem) que escreva a mensagem no ecrã, e leia e devolva um número inteiro.  Por exemplo:
int n = lerNumero("Diga um número: ");
cout <<  "O número seguinte é " << n << endl;
Coloque a função num ficheiro fonte utilitarios.cpp.  Construa o correspondente ficheiro de cabeçalho utilitarios.h.

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:

Por exemplo, o seguinte troço de programa
ContadorCircular c(5);
c.conta();
c.conta();
c.conta();
cout << c.valor();
for(int i = 0; i != 4; i++)
    c.conta();
cout << c.valor();
deverá escrever no ecrã
3
2
Faça um programa deste tipo para testar a classe.

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:

  1. Use uma matriz com três elementos e uma variável da classe ContadorCircular para indexar a matriz.
  2. Sempre que ler mais um número, acrescente-o à matriz na posição indicada contador circular e avance esse contador.  No fim, escreva as três posições da matriz.
3.a)  Procure no sistema de ajuda do Visual C++ (help) uma função chamada rand().  Dê uma vista de olhos na explicação.  Em particular, veja o #include necessário para usar esta função.

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:

Para teste, refaça o exercício 1.b) usando esta classe.

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.

4  Referências

[1]  Michael Main e Walter Savitch, "Data Structures and Other Objects Using C++", Addison Wesley, Reading, Massachusetts, 1997. *

[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.

# Existe um exemplar na biblioteca do ISCTE.