Aula 11


1  Resumo da matéria (cont. da Aula 10)

1.3.4  Corrigindo a classe racional

Uma observação da classe Racional tal como definida anteriormente revela que, na sua implementação, se violou uma das regras que se estabeleceram inicialmente: as fracções devem estar no formato canónico.  Verdade seja dita, esta restrição tem apenas parcialmente a ver com a utilização da classe.  É que, para o utilizador final, a representação interna dos racionais é irrelevante, muito embora ele espere que a função membro escreve() resulte numa representação canónica dos racionais.  Poderia então parecer que o problema poderia ser resolvido alterando apenas a função membro escreve(), e deixando o restante código tal como está.

Mas a verdade é que, sendo os int limitados em C++, a utilização duma representação não canónica das fracções põe alguns problemas graves de implementação.  Suponha-se o seguinte código:

int main()
{
    Racional x(1/50000), y(1/50000);
    Racional z = x.soma(y);
    z.escreve();
    cout << endl;
}
No ecrã deveria aparecer
1/25000
mas a verdade é que, ao se calcular o denominador do resultado, multiplicam-se os dois denominadores, de que deveria resultar 50 000 x 50 000 = 2 500 000 000.  Mas, em máquinas em que os int têm 32 bits, esse valor não é representável, pelo que se obtem um valor errado (em Linux i386 obtem-se -1794967296!).  Isto apesar de a fracção resultado ser perfeitamente representável.

É pois desejável não só usar uma representação canónica para os racionais, o que de resto simplifica a escrita de operações de comparação, por exemplo, como tentar garantir que os resultados de cálculos intermédios são tão pequenos quanto possível.

Uma nova versão da função mdc()

A versão mais eficiente da função para cálculo do mdc de dois inteiros apresentada até agora é (ver Secção 3.2.11)
// PC: m >= 0 e n > 0 e m = m e n = n
// CO: n = mdc(m, n)
int mdc(int m, int n)
{
    while(m != 0) {
        int auxiliar = n % m;
        n = m;
        m = auxiliar;
    }
    return n;
}
É fácil verificar que a PC se pode relaxar de modo a admitir n >= 0, muito embora se tenha de continuar a impor que pelo menos uma dos valores é diferente de zero, sem que isso afecte o funcionamento da função:
// PC: m >= 0 e n >= 0 e (m <> 0 ou n <> 0) e m = m e n = n
// CO: n = mdc(m, n)
int mdc(int m, int n)
{
    while(m != 0) {
        int auxiliar = n % m;
        n = m;
        m = auxiliar;
    }
    return n;
}
Mas seria de todo o interesse que a função aceitasse valores negativos e mesmo os dois valores nulos, nesse caso convencionando-se que mdc(0, 0) = 1, por exemplo.  A função abaixo resolve o problema:
// Cálculo do mdc de dois números inteiros estendido de modo a funcionar
// para argumentos negativos ou nulos.
// PC: n = n e m = m
// CO: (n = 0 e m = 0 e n = 1) ou ((m <> 0 ou n <> 0) e n = mdc(|m|, |n|))
int mdc(int m, int n)
{
    if(m == 0 && n == 0)
        return 1;
    if(m < 0)
        m = -m;
    // Aqui forçosamente m = |m| e m >= 0
    if(n < 0)
        n = -n;
    // Aqui m = |m|, m >= 0, e n = |n|, n >= 0, sendo pelo menos um de m e n
    // diferente de zero.
    // CI: mdc(m, n) = mdc(|m|, |n|)
    while(m != 0) {
        int aux = n % m;
        n = m;
        m = aux;
    }
    return n;
}

Uma nova versão da função membro soma()

Usando a nova função mdc() e as ideias apresentadas em Adição e Subtracção, a função membro soma() pode-se reescrever como
// PC:
// CO: r = *this + b, sendo *this a instância da classe implícita na execução
//    da função.
Racional Racional::soma(Racional b)
{
    int l = mdc(n, b.n);
    int k = mdc(d, b.d);
    int termo = (n / l) * (b.d / k) + (b.n / l) * (d / k);
    int m = mdc(termo, k);
    Racional r;
    r.n = (termo / m) * l;
    r.d = (k / m) * (d / k) * (b.d / k);
    return r;
}
As mesmas ideias podem ser aplicadas a qualquer outra operação com os racionais.  Por exemplo, uma função membro para verificação se um racional é maior que outro poderia ser:
// PC:
// CO: devolve valor lógico de *this > b, sendo *this a instância da classe implícita
//        na execução da função.
Racional Racional::maior(Racional b)
{
    int l = mdc(n, b.n);
    int k = mdc(d, b.d);
    return (n / l) * (b.d / k) > (b.n / l) * (d / k);
}
Uma função para verificar da igualdade poderia ser mais simples, dada a certeza da canonicidade de representação das fracções:
// PC:
// CO: devolve valor lógico de *this = b, sendo *this a instância da classe implícita
//        na execução da função.
Racional Racional::igual(Racional b)
{
    return n == b.n && d == b.d;
}

Revisão da função construtora

Mas, para que a canonicidade de representação das fracções seja real, é necessário que a função (membro) construtora garanta que a fracção está em termos mínimos, coisa que não era feita até aqui.  Assim:
// Construtor com dois parâmetros (numerador e denominador):
Racional::Racional(int num, int den)
{
    // Se den = 0, então o programa deve assinalar uma falha
    // grave!  Para já terminar-se-á o programa.  Mais tarde
    // se aprenderão formas mais elegantes de lidar com estes
    // casos:
    if(den == 0) {
        // É má ideia escrever no ecrã aqui, mas por enquanto
        // não conhecemos melhores alternativas:
        cerr << "Erro: denominador inválido!" << endl;
        exit(1);
    } else if(den < 0) {
        n = -num;
        d = -den;
    } else {
        n = num;
        d = den;
    }
    // Redução da fracção:
    int k = mdc(n, d);
    n /= k;
    d /= k;
}

Versão completa

O programa completo seria então:
#include <iostream>
#include <cstdlib>
using namespace std;

// Cálculo do mdc de dois números inteiros estendido de modo a funcionar
// para argumentos negativos ou nulos.
// PC: n = n e m = m
// CO: (n = 0 e m = 0 e n = 1) ou ((m <> 0 ou n <> 0) e n = mdc(|m|, |n|))
int mdc(int m, int n)
{
    if(m == 0 && n == 0)
        return 1;
    if(m < 0)
        m = -m;
    // Aqui forçosamente m = |m| e m >= 0
    if(n < 0)
        n = -n;
    // Aqui m = |m|, m >= 0, e n = |n|, n >= 0, sendo pelo menos um de m e n
    // diferente de zero.
    // CI: mdc(m, n) = mdc(|m|, |n|)
    while(m != 0) {
        int aux = n % m;
        n = m;
        m = aux;
    }
    return n;
}

class Racional {
  private:
    int n; // numerador
    int d; // denominador
  public:
    Racional(int num = 0, int den = 1);
    void escreve();
    Racional soma(Racional b);
    bool igual(Racional b);
    bool maior(Racional b);
};

// Construtor com dois parâmetros (numerador e denominador):
Racional::Racional(int num, int den)
{
    if(den == 0) {
        cerr << "Erro: denominador inválido!" << endl;
        exit(1);
    } else if(den < 0) {
        n = -num;
        d = -den;
    } else {
        n = num;
        d = den;
    }
    int k = mdc(n, d);
    n /= k;
    d /= k;
}

// PC:
// CO: aparece no ecrã a representação fraccionária do racional *this, sendo *this
//    a instância da classe implícita na execução da função.
void Racional::escreve()
{
    cout << n << '/' << d;
}

// PC:
// CO: r = *this + b, sendo *this a instância da classe implícita na execução
//    da função.
Racional Racional::soma(Racional b)
{
    int l = mdc(n, b.n);
    int k = mdc(d, b.d);
    int termo = (n / l) * (b.d / k) + (b.n / l) * (d / k);
    int m = mdc(termo, k);
    Racional r;
    r.n = (termo / m) * l;
    r.d = (k / m) * (d / k) * (b.d / k);
    return r;
}

// PC:
// CO: devolve valor lógico de *this = b, sendo *this a instância da classe implícita
//        na execução da função.
bool Racional::igual(Racional b)
{
    return n == b.n && d == b.d;
}

// PC:
// CO: devolve valor lógico de *this > b, sendo *this a instância da classe implícita
//        na execução da função.
bool Racional::maior(Racional b)
{
    int l = mdc(n, b.n);
    int k = mdc(d, b.d);
    return (n / l) * (b.d / k) > (b.n / l) * (d / k);
}

int main()
{
    Racional x = 1, y(7, 15);
    Racional z = x.soma(y);
    z.escreve();
    cout << endl;
    if(x.maior(y))
        cout << "maior" << endl;
    else
        cout << "menor" << endl;
    if(x.igual(y))
        cout << "igual" << endl;
    else
        cout << "diferente" << endl;
}

Que, depois de executado, produz no ecrã:
22/15
maior
diferente

1.3.5  Usando sobrecarga de operadores

Tal como definida, a classe Racional obriga o utilizador a usar uma notação desagradável e pouco intuitiva para fazer operações com inteiros.  Em particular, seria desejável que a função main(), no programa acima, se pudesse escrever simplesmente como:
int main()
{
    Racional x = 1, y(7, 15);
    Racional z = x + y;
    z.escreve();
    cout << endl;
    if(x > y)
        cout << "maior" << endl;
    else
        cout << "menor" << endl;
    if(x == y)
        cout << "igual" << endl;
    else
        cout << "diferente" << endl;
}
Se se pudesse escrever o programa como acima, claramente a classe Racional, uma vez equipada com os operadores restantes dos tipos aritméticos, passaria a funcionar para o utilizador como qualquer outro tipo básico do C++.

A solução para este problema passa pela sobrecarga dos operadores do C++ de modo a terem novos significados quando aplicados ao novo tipo Racional, da mesma forma que se tinha visto antes relativamente aos tipos enumerados (ver Secção 5.1).  Mas, ao contrário do que se fez então, agora as funções de sobrecarga têm de continuar membros da classe Racional de modo a poderem aceder aos seus membros privados (alternativamente poder-se-iam usar funções membro amigas da classe, ver Secção 1.4).  Ou seja, a solução é simplesmente:

#include <iostream>
#include <cstdlib>
using namespace std;

// Cálculo do mdc de dois números inteiros estendido de modo a funcionar
// para argumentos negativos ou nulos.
// PC: n = n e m = m
// CO: (n = 0 e m = 0 e n = 1) ou ((m <> 0 ou n <> 0) e n = mdc(|m|, |n|))
int mdc(int m, int n)
{
    if(m == 0 && n == 0)
        return 1;
    if(m < 0)
        m = -m;
    // Aqui forçosamente m = |m| e m >= 0
    if(n < 0)
        n = -n;
    // Aqui m = |m|, m >= 0, e n = |n|, n >= 0, sendo pelo menos um de m e n
    // diferente de zero.
    // CI: mdc(m, n) = mdc(|m|, |n|)
    while(m != 0) {
        int aux = n % m;
        n = m;
        m = aux;
    }
    return n;
}

class Racional {
  private:
    int n; // numerador
    int d; // denominador
  public:
    Racional(int num = 0, int den = 1);
    void escreve();
    Racional operator + (Racional b);
    bool operator == (Racional b);
    bool operator > (Racional b);
};

// Construtor com dois parâmetros (numerador e denominador):
Racional::Racional(int num, int den)
{
    if(den == 0) {
        cerr << "Erro: denominador inválido!" << endl;
        exit(1);
    } else if(den < 0) {
        n = -num;
        d = -den;
    } else {
        n = num;
        d = den;
    }
    int k = mdc(n, d);
    n /= k;
    d /= k;
}

// PC:
// CO: aparece no ecrã a representação fraccionária do racional *this, sendo *this
//    a instância da classe implícita na execução da função.
void Racional::escreve()
{
    cout << n << '/' << d;
}

// PC:
// CO: r = *this + b, sendo *this a instância da classe implícita na execução
//    da função.
Racional Racional::operator + (Racional b)
{
    int l = mdc(n, b.n);
    int k = mdc(d, b.d);
    int termo = (n / l) * (b.d / k) + (b.n / l) * (d / k);
    int m = mdc(termo, k);
    Racional r;
    r.n = (termo / m) * l;
    r.d = (k / m) * (d / k) * (b.d / k);
    return r;
}

// PC:
// CO: devolve valor lógico de *this = b, sendo *this a instância da classe implícita
//        na execução da função.
bool Racional::operator == (Racional b)
{
    return n == b.n && d == b.d;
}

// PC:
// CO: devolve valor lógico de *this > b, sendo *this a instância da classe implícita
//        na execução da função.
bool Racional::operator > (Racional b)
{
    int l = mdc(n, b.n);
    int k = mdc(d, b.d);
    return (n / l) * (b.d / k) > (b.n / l) * (d / k);
}

int main()
{
    Racional x = 1, y(7, 15);
    Racional z = x + y;
    z.escreve();
    cout << endl;
    if(x > y)
        cout << "maior" << endl;
    else
        cout << "menor" << endl;
    if(x == y)
        cout << "igual" << endl;
    else
        cout << "diferente" << endl;
}

Note-se que, tal como acontecia com a função membro soma(), a expressão x + y invoca a função membro operator + da classe Racional usando x como instância (variável) implícita!  Isto é sempre verdade para sobrecarga de operadores usando funções membro: o primeiro operando (que pode ser o único no caso de operadores unários, i.e., só com um operando) é sempre a instância implícita do método (função membro).

1.3.6  Construtores: conversões implícitas e valores literais

Valores literais

Já se viu que a construção de classes corresponde a acrescentar à linguagem C++ novos tipos que funcionam praticamente como os seus tipos básicos.  Mas haverá equivalente aos valores literais?  Recorde-se que, num programa em C++, 10 e 100.0 são valores literais dos tipos int e double, respectivamente.  Será possível especificar uma forma para, por exemplo, escrever valores literais do novo tipo racional?  Infelizmente isso é impossível em C++.  Por exemplo, a tentativa
Racional r;
r = 1/3;
redunda num programa aparentemente funcional mas com um comportamento inesperado.  Acontece que a expressão 1/3 é interpretada como a divisão inteira, com resultado zero, que deve ser convertida para um Racional e atribuída à variável r.  Logo, r, depois da atribuição, conterá o racional zero!

Mas existe uma alternativa elegante, proporcionada pelos construtores das classes, e que funciona quase como se de valores literais se tratasse: os construtores podem ser chamados explicitamente para criar um novo valor dessa classe.  Assim, o código anterior deveria ser corrigido para:

Racional r;
r = Racional(1, 3);

Conversões implícitas

Os construtores que possam ser chamados com apenas um argumento definem automaticamente uma conversão implícita de tipos.  Por exemplo, o construtor da classe Racional pode ser chamado com apenas um argumento int, o que significa que, sempre que o compilador esperar um Racional e encontrar um int, converte o int implicitamente para um valor Racional.  Por exemplo, estando definido um operator + com operandos do tipo Racional, o seguinte pedaço de código é perfeitamente legal
Racional x(1, 3);
Racional z = x + 1;
tendo o mesmo significado que
Racional x(1, 3);
Racional z = x + Racional(1);
que por sua vez, tendo em conta os valores por omissão dos parâmetros do contrutor dos Racional, significa o mesmo que
Racional x(1, 3);
Racional z = x + Racional(1, 1);
que coloca em z o racional (4, 3) (ou 4/3).

Em casos em que esta conversão implícita de tipos é indesejável, pode-se preceder o respectivo construtor da palavra chave explicit.  Assim, se a classe Racional estivesse definida como

class Racional {
    ...
  public:
    explicit Racional(int num = 0, int den = 1);
    ...
};
o compilador assinalaria erro ao encontrar a expressão x + 1.  Neste caso, no entanto, a conversão implícita de int para Racional é útil, pelo que o qualificador explicit é desnecessário.

1.3.7  Exercícios

1.  Complete a classe Racional apresentada acima (use a classe que desenvolveu nos exercícios anteriores), convertendo as operações desenvolvidas de modo a que sejam possíveis expressões usando os operadores usuais no C++ (i.e., +, -, *, /, -, ==, !=, >, >=, <, <=, etc.).  Use sobrecarga de operadores C++.

2.  Use sobrecarga de operadores C++ para implementar os operadores de atribuição e incrementação e decrementação típicos (i.e., +=, -=, *=, /=, ++, --) para a classe Racional.

3.  O cálculo da raiz quadrada de um valor v pode ser feito, duma forma algo ineficiente, procurando a solução da equação f(x) = v, onde f(x) := x2 (em que se usou a notação := com o significado "é definida por"), usando o método de Newton.  Este método advoga que se deve construir uma sequência r0, r1, ... de raízes definida, duma forma recorrente, por rn+1 = rn - (rn2 - v) / (2rn).  Esta sequência converge para a raiz quadrada (positiva) de v quando n tende para infinito, desde que o valor inicial r0 seja positivo.  Escreva uma função Racional raizQuadrada(Racional v) que calcule um valor racional que aproxime a raiz quadrada do racional v.  Considere a aproximação razoável quando a diferença entre termos sucessivos da sequência for inferior a 1/100 (ou seja, quando |rn+1 - rn| = |(rn2 - v) / (2rn)| < 1/100).

Se procurar a raiz quadrada de 2 usando o método sugerido, chegará à surpreendente fracção 577/408, que é uma excelente aproximação (577/408 = 1,41421568..., cujo quadrado é 2,000006007...).

4. [difícil]  Os inteiros são limitados, pelo que, em máquinas em que os int são representados em complemento para 2 e têm 32 bits, o valor racional mais pequeno (em módulo) representável pela classe Racional tal como desenvolvida é 2-32 (aproximadamente 10-10) e o maior (em módulo) 232 (aproximadamente 1010).  Se se pretendesse tornar a classe dos racionais verdadeiramente útil, seria necessário estender a gama dos int consideravelmente.  Não sendo isso possível sem mudar de linguagem, compilador, sistema operativo e/ou computador, a solução pode passar por construir uma nova classe Inteiro, à custa da qual a classe Racional pode ser construída, e que represente inteiros de precisão (virtualmente) arbitrária.  Construa essa classe, usando sequências de int para representar os inteiros de precisão arbitrária.  Pode ter de usar memória dinâmica, a ensinar posteriormente.

1.4  Desenho de classes

Como se desenha uma classe? Quando e porquê o fazer?  Não há respostas taxativas para estas questões, mas tentar-se-á apresentar nesta secção um exemplo de resolução de um problema que sirva para as clarificar um pouco.

Suponha-se que se pretende escrever um programa que leia 10 valores inteiros do teclado e os escreva pela ordem inversa no ecrã.  Um pouco de reflexão sobre o problema revela que ele pode ser resolvido com matrizes C++.  Mas, antes de aceitar a solução como evidente, convém pensar um pouco no problema.

Ponha-se no lugar do computador e admita que lhe são passadas 10 folhas de papel, com número escritos, e que as deve devolver pela ordem inversa à da entrega.  Como organizaria as folhas de papel?  A resposta usual a esta pergunta é que os papeis deveriam ser organizados numa pilha (com os números voltados para cima) porque, numa pilha de papeis, o último papel a entrar é o primeiro a sair.

Não há equivalente a uma pilha de papeis na linguagem C++ (embora exista na biblioteca padrão...).  Surge então a ideia de construir uma classe para acrescentar esse conceito ao C++.  Mas resta uma dúvida: sendo o problema tão simples e facilmente resolúvel usando matrizes directamente, porquê definir uma classe nova?

Mais uma vez não existem respostas taxativas a esta questão.  A questão deve ser analisada sob diversos pontos de vista e uma decisão tomada com base nos resultados dessa análise.  Por exemplo:

  1. O desenvolvimento da nova classe exige substancialmente mais esforço que uma solução ad hoc?
  2. A nova classe pode vir a ser útil no futuro, no mesmo ou noutros programas?
  3. A modularização inerente à construção de uma classe vai trazer vantagens para o desenvolvimento e correcção do programa?
Neste caso, é evidente que um classe representando o conceito de pilha de inteiros pode vir a ser útil no futuro, sobretudo se for facilmente adaptável para representar pilhas de entidades de outro tipo tais como pilhas de char ou pilhas de Racional (mais tarde se verá como se podem construir classes genéricas, ou meta-classes).

1.4.1  Conceito de pilha

Uma pilha de inteiros é uma entidade na qual se podem inserir e remover inteiros, sendo que os inteiros são removidos pela ordem inversa à da inserção.

Operações com pilhas

As pilhas têm de suportar pelo menos as duas operações elementares de inserção de um valor (colocar mais um papel no topo da pilha) e remoção de um valor (tirar o papel que está no topo da pilha).  Mas existe uma outra operação importante: saber que valor está no topo da pilha (ou seja, olhar para o papel no topo da pilha).  É importante notar que as duas últimas operações apenas estão definidas para pilhas que não esteja vazias.  Este facto é perfeitamente normal: para quase todos os tipos de dados à determinadas operações que não estão bem definidas em algumas circunstâncias.  Por exemplo para os inteiros a divisão não está definida se o divisor for zero.  Assim, é conveniente definir uma operação que verifique se uma pilha está vazia ou não.

Note-se que, ao contrário das operações mais usuais com inteiros, como a soma, subtracção, etc., as operações de inserção e remoção de valores duma pilha afectam a própria pilha.  Seria possível definir as operações de tal modo que a inserção de um valor numa pilha resultasse numa nova pilha, idêntica à original mais com um valor adicional.  As vantagens práticas dum tal conceito são poucas, no entanto.

Pilhas limitadas

Na prática qualquer pilha tem um tamanho limite (mesmo uma pilha de papeis deixa de ser manejável quando atinge o tecto...).  Assim sendo, pode ser útil definir uma operação que verifique se a pilha comporta ou não mais algum elemento, i.e., se está ou não cheia.

Outras operações

Finalmente, pode ser útil definir uma operação que calcule o número de valores na pilha num dado instante (ou seja, o correspondente à simples contagem de folhas de papel numa pilha).

1.4.2  Concretização em C++

Uma vez definido o conceito de pilha, o próximo passo consiste concretização do conceito na forma duma classe C++.  Esta concretização é, normalmente, feita em duas fases.  Na primeira fase constrói-se a interface da classe e só na segunda se passa à implementação propriamente dita.  A construção da interface consiste em declarar aquilo que é público na classe, i.e., aquilo a que o utilizador final tem acesso (na analogia da aparelhagem de som, seria a definição dos botões e fichas a colocar no amplificador, por exemplo).  A implementação propriamente dita consiste em definir todos os membros privados e em definir os membros públicos entretanto declarados na interface da classe (na analogia da aparelhagem de som, seria o desenho dos circuitos internos que efectivamente proporcionam a amplificação e todas as outras funcionalidades do amplificador).

A interface

No caso da concretização do conceito de pilha, devem fazer parte da sua interface funções e procedimentos que representem cada uma das operações vistas na secção anterior, bem como os construtores necessários para inicializar as instâncias da classe.  Assim,
class PilhaInt {
    ....
  public:
    PilhaInt();      // construtor da classe.
    void põe(int v); // coloca v no topo da pilha.
    void tira();     // retira valor do topo da pilha.
    int topo();      // devolve valor no topo da pilha.
    bool vazia();    // devolve true sse a pilha estiver vazia.
    bool cheia();    // devolve true sse a pilha estiver cheia.
    int tamanho();   // devolve número de valores na pilha.
};

A implementação

A fase seguinte da concretização em C++ é a implementação de cada uma das operações definidas na interface.  Mas isso implica pensar antes em como guardar os valores presentes na pilha em cada instante.

Como é óbvio, sendo a construção de novos tipos um processo incremental de acrescento de funcionalidade à linguagem C++, é evidente que, para guardar os valores inteiros na pilha, ter-se-á de recorrer a conceitos previamente definidos (alternativamente poder-se-ia construir outro novo tipo, embora correndo o risco de entrar num processo recursivo de definição de novos tipos...).  Neste caso pretende-se de algum modo guardar um conjunto de valores todos do mesmo tipo (int).  A forma mais simples de o fazer, em C++, é usando o conceito de matriz.  Assim, os valores guardados na pilha vão, na realidade, ser guardados numa matriz.  Mas as matrizes em C++ têm de ter um tamanho constante.  O tamanho escolhido para a matriz é importante porque corresponde ao limite de valores que as pilhas suportarão.  Para que se possa facilmente alterar o tamanho limite das pilhas, definir-se-á uma constante para representar esse limite.

Assim,

const int limite = 100;

class PilhaInt {
    int valores[limite];
  public:
    PilhaInt();      // construtor da classe.
    void põe(int v); // coloca v no topo da pilha.
    void tira();     // retira valor do topo da pilha.
    int topo();      // devolve valor no topo da pilha.
    bool vazia();    // devolve true sse a pilha estiver vazia.
    bool cheia();    // devolve true sse a pilha estiver cheia.
    int tamanho();   // devolve número de valores na pilha.
};

Mas é também necessário saber em cada instante quantos elementos estão na pilha e em que posição.  Usando a simples convenção de que o primeiro elemento a entrar na pilha (e o último a sair) é o que está na posição 0 da matriz, encontrando-se todos os outros nos índices subsequentes, é óbvio que uma única variável indica não só o número de elementos na pilha como a posição do próximo elemento a inserir na pilha.  Assim:
const int limite = 100;

class PilhaInt {
    int valores[limite];
    int quantos;
  public:
    PilhaInt();      // construtor da classe.
    void põe(int v); // coloca v no topo da pilha.
    void tira();     // retira valor do topo da pilha.
    int topo();      // devolve valor no topo da pilha.
    bool vazia();    // devolve true sse a pilha estiver vazia.
    bool cheia();    // devolve true sse a pilha estiver cheia.
    int tamanho();   // devolve número de valores na pilha.
};

Por exemplo, se quantos tiver valor 4, isso significa que existem 4 elementos na pilha, nas posições 0 a 3 da matriz, e que o próximo valor a colocar ficará na posição 4 da matriz.

É agora possível definir cada uma das funções e procedimentos membro da classe.  Como é usual, começa-se pelos construtores (neste caso apenas existe um).  O construtor limite-se a colocar quantos a zero, o que é o mesmo que dizer que as pilhas, quando criadas, estão vazias:

PilhaInt::PilhaInt()
{
    quantos = 0;
}
Note-se que não é necessário inicializar os valores na matriz.  Basta que se convencione que, dos limite elementos da matriz, apenas os quantos primeiros são valores colocados na pilha, contendo os restantes limite - quantos valores que são irrelevantes (vulgo lixo).

Em seguida definem-se as funções membro vazia() e cheia(), por exemplo.  Como é óbvio, uma pilha está vazia sse quantos = 0 e está cheira sse quantos = limite.  Assim:

bool PilhaInt::vazia()
{
    return quantos == 0;
}

bool PilhaInt::cheia()
{
    return quantos == limite;
}

A função membro tamanho() é trivial:
int PilhaInt::tamanho()
{
    return quantos;
}
As restantes funções e procedimentos nem sempre estão bem definidas.  Que fazer quando o procedimento membro tira() é invocado para uma pilha vazia?  É claramente um erro, pelo que, e não se tendo ainda aprendido melhores mecanismos para lidar com este tipo de situação, o melhor é abortar imediatamente o programa.  Assim:
void PilhaInt::põe(int v)
{
    if(cheia()) {
        cerr << "Erro: pilha cheia." << endl;
        exit(1);
    }
    valores[quantos++] = v;
}

void PilhaInt::tira()
{
    if(vazia()) {
        cerr << "Erro: pilha vazia." << endl;
        exit(1);
    }
    quantos--;
}

int PilhaInt::topo()
{
    if(vazia()) {
        cerr << "Erro: pilha vazia." << endl;
        exit(1);
    }
    return valores[quantos - 1];
}

É de notar a utilização típica do operador ++ no procedimento membro poe().  O código é equivalente a
void PilhaInt::põe(int v)
{
    if(cheia()) {
        ...
    }
    valores[quantos] = v;
    quantos = quantos + 1;
}
mas apenas porque se usou o operador ++ sufixo!  A utilização do operador ++ prefixo teria um significado diferente e conduziria a um erro, pois
valores[++quantos] = v;
é o mesmo que
quantos = quantos + 1;
valores[quantos] = v;

Class e struct

Na definição da classe PilhaInt acima, não se utilizou o especificador de acesso private:.  Isso deve-se ao facto de, por omissão, os membros duma classe serem privados.  Existe uma construção alternativa a class com uma semântica muito semelhante: struct.  A única diferença está em que, quando se usa struct, os membros são públicos por omissão.

Constantes membro

Ao contrário do que poderia parecer lógico, não é possível escrever:
class PilhaInt {
    const int limite = 100;  // erro!
    int valores[limite];
    ....
}
A razão para a proibição é simples.  Sendo limite uma constante membro (de instância), cada instância da classe possuirá a sua cópia privada dessa constante.  Mas isso significa que, para ser verdadeiramente útil, essa constante deverá poder tomar valores diferentes para cada instância da classe, i.e., para cada variável dessa classe criada.  Daí que não se possam inicializar membros constantes (de instância) na própria declaração.  Como, por definição de constante em C++, não é possível atribuir um valor (através do operador =), o C++ proporciona uma forma algo estranha de inicializar membros constantes: colocando os inicializadores após o cabeçalho do construtor (separados por vírgulas e após um :).  Por exemplo:
class Aluno {
    const int número;
    int nota;
    ...
  public:
    Aluno(int n);
};

Aluno::Aluno(int n) : número(n) {
    ...
}

Este tipo de inicializadores é extremamente útil, sendo utilizado para inicializar não só membros constantes como também membros referência e membros de classes sem construtores por omissão (i.e., que exijam uma inicialização explícita).

1.4.3  O programa completo

Uma vez concretizado o conceito de pilha (limitada) em C++, é possível utilizá-lo para escrever pela ordem inversa dez valores inteiros lidos do teclado: basta definir uma pilha e colocar os valores lidos sucessivamente no topo dessa pilha, e depois ir retirando os valores da pilha, um a um, escrevendo-os no ecrã.  Ou seja, o programa completo seria:
#include <iostream>
using namespace std;

const int limite = 100;

class PilhaInt {
    int valores[limite];
    int quantos;
  public:
    PilhaInt();      // construtor da classe.
    void põe(int v); // coloca v no topo da pilha.
    void tira();     // retira valor do topo da pilha.
    int topo();      // devolve valor no topo da pilha.
    bool vazia();    // devolve true sse a pilha estiver vazia.
    bool cheia();    // devolve true sse a pilha estiver cheia.
    int tamanho();   // devolve número de valores na pilha.
};

PilhaInt::PilhaInt()
{
    quantos = 0;
}
bool PilhaInt::vazia()
{
    return quantos == 0;
}

bool PilhaInt::cheia()
{
    return quantos == limite;
}

int PilhaInt::tamanho()
{
    return quantos;
}
void PilhaInt::põe(int v)
{
    if(cheia()) {
        cerr << "Erro: pilha cheia." << endl;
        exit(1);
    }
    valores[quantos++] = v;
}

void PilhaInt::tira()
{
    if(vazia()) {
        cerr << "Erro: pilha vazia." << endl;
        exit(1);
    }
    quantos--;
}

int PilhaInt::topo()
{
    if(vazia()) {
        cerr << "Erro: pilha vazia." << endl;
        exit(1);
    }
    return valores[quantos - 1];
}

int main()
{
    PilhaInt pilha;

    // Coloca 10 valores na pilha:
    for(int i = 0; i != 10; i++) {
        int v;
        cin >> v;
        pilha.põe(v);
    }

    // Escreve-os pela ordem inversa:
    while(!pilha.vazia()) {
        cout << pilha.topo() << endl;
        pilha.tira();
    }
}

1.5  Mais sobre classes em C++

1.5.1  Funções e procedimentos inline

A classe PilhaInt tal como definida acima é muito simples, mas é também algo ineficiente: todas as operações são realizadas através da invocação de funções ou procedimentos, o que poderá resultar, em alguns casos, num peso computacional excessivo (ver Secção 3.3.1).  Sempre que uma função ou um procedimento membro duma classe for muito simples, é um bom candidato a ser inline, i.e., que a sua invocação resulte na substituição directa do código da função, em vez de se usar o mecanismo usual de invocação.  A razão para a parcimónia na utilização de funções e procedimentos inline é que podem conduzir a um aumento considerável do programa executável (depois da compilação).  No caso presente, como todas as operações realizadas sobre pilhas são triviais, todas as funções e procedimentos podem e devem ser definidos como inline.  Mas não o faça, em geral, duma forma arbitrária. Seleccione criteriosamente como candidatos a inline as funções e procedimentos membro que: a) possam vir a ser intensivamente usadas, b) sejam muito simples.

Para definir uma função ou procedimento membro como inline, podem-se fazer uma de duas coisas:

  1. Ao definir a classe definir logo a função ou procedimento membro (em vez de a declarar apenas).
  2. Ao definir a função ou procedimento membro declarada na definição da classe, preceder o seu cabeçalho do qualificador inline.
Assim, para definir como inline a função membro tamanho(), pode-se usar ou
class PilhaInt {
    ...
    int tamanho() {
        return quantos;
    }
    ...
}
ou, alternativamente,
class PilhaInt {
    ...
    int tamanho();
    ...
}
...
inline int PilhaInt::tamanho()
{
    return quantos;
}
Em geral a segunda alternativa é preferível à primeira, pois torna mais evidente a separação entre a interface e a implementação da classe.

Note-se que não só as funções ou procedimentos membro duma classe podem ser inline: qualquer função ou procedimento pode ser definido como inline, bastando para isso usar o qualificador inline.  Note-se ainda que a definição de uma função ou procedimento como inline não altera a semântica da sua chamada, tendo apenas consequências em termos da tradução, pelo compilador, para código máquina.

Nova versão do programa das pilhas

Usando os conceitos acima, pode-se escrever:
#include <iostream>
using namespace std;

const int limite = 100;

class PilhaInt {
    int valores[limite];
    int quantos;
  public:
    PilhaInt();      // construtor da classe.
    void põe(int v); // coloca v no topo da pilha.
    void tira();     // retira valor do topo da pilha.
    int topo();      // devolve valor no topo da pilha.
    bool vazia();    // devolve true sse a pilha estiver vazia.
    bool cheia();    // devolve true sse a pilha estiver cheia.
    int tamanho();   // devolve número de valores na pilha.
};

inline PilhaInt::PilhaInt()
{
    quantos = 0;
}
inline bool PilhaInt::vazia()
{
    return quantos == 0;
}

inline bool PilhaInt::cheia()
{
    return quantos == limite;
}

inline int PilhaInt::tamanho()
{
    return quantos;
}

inline void PilhaInt::põe(int v)
{
    if(cheia()) {
        cerr << "Erro: pilha cheia." << endl;
        exit(1);
    }
    valores[quantos++] = v;
}

inline void PilhaInt::tira()
{
    if(vazia()) {
        cerr << "Erro: pilha vazia." << endl;
        exit(1);
    }
    quantos--;
}

inline int PilhaInt::topo()
{
    if(vazia()) {
        cerr << "Erro: pilha vazia." << endl;
        exit(1);
    }
    return valores[quantos - 1];
}

int main()
{
    PilhaInt pilha;

    // Coloca 10 valores na pilha:
    for(int i = 0; i != 10; i++) {
        int v;
        cin >> v;
        pilha.põe(v);
    }

    // Escreve-os pela ordem inversa:
    while(!pilha.vazia()) {
        cout << pilha.topo() << endl;
        pilha.tira();
    }
}

1.5.2  Funções e procedimentos membro constantes

Para que as funcionalidades dos novos tipos sejam equivalentes às dos tipos básicos, é necessário que se possam definir constantes dos novos tipos.  Por exemplo, deve ser possível escrever:
const Racional um = 1;
const Racional zero = 0;
e de facto é-o!  Podem-se definir constantes de qualquer tipo definido pelo utilizador.  O único problema está na sua utilização.  Por exemplo:
Racional z = um + zero;
gera um erro de compilação.  Isto deve-se a que o compilador não pode adivinhar que a função membro operator +() da classe Racional não altera a instância que é passada implicitamente por referência, neste caso uma instância constante.  Como pode haver a possibilidade de a constante um ser alterada, o que é um contra-senso, o compilador simplesmente proíbe a expressão.   Relativamente à constante zero, sendo passada por valor para a função operator +(), o compilador não se queixa.

Para obviar a este comportamento, que impede a soma de uma constante com qualquer outro Racional, é necessário indicar ao compilador que a função membro operator +() é bem comportada, i.e., não altera a instância passada por referência implicitamente.  Para isso coloca-se o qualificador const após quer a declaração quer a definição da função membro:

class Racional {
    ...
 public:
    Racional operator + (Racional b) const;
    ...
};
...
Racional Racional::operator + (Racional b) const
{
    ...
}
Claro está que, se dentro duma função ou procedimento membro declarada como const se tentar alterar alguma variável membro, o compilador gerará uma mensagem de erro.

É uma boa regra declarar como const todas as funções e procedimentos membro que não efectem a instância implícita.  Assim, apresentam-se abaixo versões melhoradas dos programas desenvolvidos:

Nova versão do programa com racionais

(Aproveitou-se para definir como inline as funções membro mais simples.)
#include <iostream>
#include <cstdlib>
using namespace std;

// Cálculo do mdc de dois números inteiros estendido de modo a funcionar
// para argumentos negativos ou nulos.
// PC: n = n e m = m
// CO: (n = 0 e m = 0 e n = 1) ou ((m <> 0 ou n <> 0) e n = mdc(|m|, |n|))
int mdc(int m, int n)
{
    if(m == 0 && n == 0)
        return 1;
    if(m < 0)
        m = -m;
    // Aqui forçosamente m = |m| e m >= 0
    if(n < 0)
        n = -n;
    // Aqui m = |m|, m >= 0, e n = |n|, n >= 0, sendo pelo menos um de m e n
    // diferente de zero.
    // CI: mdc(m, n) = mdc(|m|, |n|)
    while(m != 0) {
        int aux = n % m;
        n = m;
        m = aux;
    }
    return n;
}

class Racional {
  private:
    int n; // numerador
    int d; // denominador
  public:
    Racional(int num = 0, int den = 1);
    void escreve() const;
    Racional operator + (Racional b) const;
    bool operator == (Racional b) const;
    bool operator > (Racional b) const;
};

// Construtor com dois parâmetros (numerador e denominador):
Racional::Racional(int num, int den)
{
    if(den == 0) {
        cerr << "Erro: denominador inválido!" << endl;
        exit(1);
    } else if(den < 0) {
        n = -num;
        d = -den;
    } else {
        n = num;
        d = den;
    }
    int k = mdc(n, d);
    n /= k;
    d /= k;
}

// PC:
// CO: aparece no ecrã a representação fraccionária do racional *this, sendo *this
//    a instância da classe implícita na execução da função.
inline void Racional::escreve() const
{
    cout << n << '/' << d;
}

// PC:
// CO: r = *this + b, sendo *this a instância da classe implícita na execução
//    da função.
Racional Racional::operator + (Racional b) const
{
    int l = mdc(n, b.n);
    int k = mdc(d, b.d);
    int termo = (n / l) * (b.d / k) + (b.n / l) * (d / k);
    int m = mdc(termo, k);
    Racional r;
    r.n = (termo / m) * l;
    r.d = (k / m) * (d / k) * (b.d / k);
    return r;
}

// PC:
// CO: devolve valor lógico de *this = b, sendo *this a instância da classe implícita
//        na execução da função.
inline bool Racional::operator == (Racional b) const
{
    return n == b.n && d == b.d;
}

// PC:
// CO: devolve valor lógico de *this > b, sendo *this a instância da classe implícita
//        na execução da função.
bool Racional::operator > (Racional b) const
{
    int l = mdc(n, b.n);
    int k = mdc(d, b.d);
    return (n / l) * (b.d / k) > (b.n / l) * (d / k);
}

int main()
{
    Racional x = 1, y(7, 15);
    Racional z = x + y;
    z.escreve();
    cout << endl;
    if(x > y)
        cout << "maior" << endl;
    else
        cout << "menor" << endl;
    if(x == y)
        cout << "igual" << endl;
    else
        cout << "diferente" << endl;
}

Nova versão do programa com pilhas

#include <iostream>
using namespace std;

const int limite = 100;

class PilhaInt {
    int valores[limite];
    int quantos;
  public:
    PilhaInt();          // construtor da classe.
    void põe(int v);     // coloca v no topo da pilha.
    void tira();         // retira valor do topo da pilha.
    int topo() const;    // devolve valor no topo da pilha.
    bool vazia() const;  // devolve true sse a pilha estiver vazia.
    bool cheia() const;  // devolve true sse a pilha estiver cheia.
    int tamanho() const; // devolve número de valores na pilha.
};

inline PilhaInt::PilhaInt()
{
    quantos = 0;
}
inline bool PilhaInt::vazia() const
{
    return quantos == 0;
}

inline bool PilhaInt::cheia() const
{
    return quantos == limite;
}

inline int PilhaInt::tamanho() const
{
    return quantos;
}
inline void PilhaInt::põe(int v)
{
    if(cheia()) {
        cerr << "Erro: pilha cheia." << endl;
        exit(1);
    }
    valores[quantos++] = v;
}

inline void PilhaInt::tira()
{
    if(vazia()) {
        cerr << "Erro: pilha vazia." << endl;
        exit(1);
    }
    quantos--;
}

inline int PilhaInt::topo() const
{
    if(vazia()) {
        cerr << "Erro: pilha vazia." << endl;
        exit(1);
    }
    return valores[quantos - 1];
}

int main()
{
    PilhaInt pilha;

    // Coloca 10 valores na pilha:
    for(int i = 0; i != 10; i++) {
        int v;
        cin >> v;
        pilha.põe(v);
    }

    // Escreve-os pela ordem inversa:
    while(!pilha.vazia()) {
        cout << pilha.topo() << endl;
        pilha.tira();
    }
}

1.6  Exercícios

1.  Crie um novo tipo em C++ que concretize a noção de fila (de espera) de valores int.  Ao contrário do que se passa para no caso das pilhas, o primeiro valor a entrar numa fila é também o primeiro valor a sair.  Use a metodologia sugerida na Secção 1.4 e os conceitos sobre classes em C++ aprendidos nas Secções  1.3 e 1.5.  Pense seriamente como usar matrizes para implementar o conceito antes de passar à implementação propriamente dita.