Resumo da Aula 6


1  Índice


2  Resumo

2.1  Classes que reservam recursos externos

Quando as instâncias de uma classe reservam recursos externos para sua utilização exclusiva, é necessário ter uma série de cuidados na implementação da classe.  De longe o recurso externo mais utilizado pelas classes é memória livre.  Sempre que uma classe reserva memória livre para sua utilização exclusiva é necessário tomar atenção a um conjunto característico de problemas.

Seja a classe PilhaInt representando pilhas limitadas de inteiros.  O seu código divide-se por dois ficheiros fonte (de interface e auxiliar de implementação):

pilha_int.H

#ifndef PILHA_INT_H
#define PILHA_INT_H

class PilhaInt {

  public:
    typedef int Item;

    PilhaInt();

    int altura() const;
    bool estáVazia() const;

    bool estáCheia() const;
    Item const& topo() const;

    Item& topo();
    void põe(Item const& novo_item);

    void tira();

  private:
    static int const capacidade = 100;
    Item itens[capacidade];
    int número_de_itens;
};

#include "pilha_int_impl.H"

#endif // PILHA_INT_H

pilha_int_impl.H

#include <cassert>

inline PilhaInt::PilhaInt()

    : número_de_itens(0) {
}

inline int PilhaInt::altura() const {
    return número_de_itens;
}

inline bool PilhaInt::estáVazia() const {

    return altura() == 0;
}

inline bool PilhaInt::estáCheia() const {

    return altura() == capacidade;
}

inline PilhaInt::Item const& PilhaInt::topo() const {

    assert(not estáVazia());

    return itens[número_de_itens - 1];
}

inline PilhaInt::Item& PilhaInt::topo() {

    assert(not estáVazia());

    return itens[número_de_itens - 1];

}

inline void PilhaInt::põe(Item const& novo_item) {

    assert(not estáCheia());

    itens[número_de_itens++] = novo_item;

}

inline void PilhaInt::tira() {

    assert(not estáVazia());

    --número_de_itens;
}

2.1.1  Problemas com a implementação

Esta classe tem um problema grave, como já se viu antes: as instâncias têm todas a mesma capacidade limitada.  É impossível escolher uma capacidade apropriada para todas as aplicações.  Se for muito grande, conduzirá a desperdícios quando o número de itens realmente utilizado for pequeno.  Se for pequeno, torna impossível a resolução de problemas que necessitam de colocar nas pilhas maior número de itens.

Uma possível solução do problema passa por guardar os itens da pilha não numa matriz normal do C++, mas sim numa matriz dinâmica, de modo que se possa ir aumentando a sua dimensão à medida das necessidades.

Na realidade a matriz dinâmica não vai crescer.  Sempre que a capacidade actual da pilha estiver esgotada e for necessário acrescentar-lhe um novo item, será construída uma nova matriz dinâmica de maior dimensão, contendo os mesmos itens que a matriz original, que será usada de aí em diante.  A implementação, portanto, usará um ponteiro para guardar o endereço do primeiro elemento da matriz dinâmica, em vez da matriz não dinâmica usada na implementação original.  O atributo número_de_itens manter-se-á, mas é necessário passar o atributo capacidade de constante de classe a variável de instância de modo a deixar claro que já não reflecte a capacidade definitiva de todas as pilhas mas sim a capacidade de cada pilha específica em cada instante de tempo.  Entretanto introduz-se uma nova constante membro de classe capacidade_inicial que representa a capacidade inicial da pilha, ou seja, a dimensão inicial da matriz dinâmica:

class PilhaInt {
  public:
    typedef int Item;

    PilhaInt();

    int altura() const;
    bool estáVazia() const;

    bool estáCheia() const;
    Item const& topo() const;

    Item& topo();
    void põe(Item const& novo_item);

    void tira();

  private:
    static int const capacidade_inicial = 32;
    int capacidade;

    Item* itens;
    int número_de_itens;
};

2.1.2  Construtores

As mudanças principais a realizar na implementação da classe são na definição do construtor e na definição do procedimento membro põe().  O construtor deverá construir a matriz dinâmica com a dimensão apropriada.  O procedimento põe() terá de aumentar a dimensão da matriz dinâmica sempre que necessário.  Uma vez que na realidade os aumentos são conseguidos através da construção de uma nova matriz dinâmica e consequente cópia dos itens da matriz original para a nova matriz, este processo de reconstrução é oneroso computacionalmente.  É pois conveniente que a matriz dinâmica aumente a bom ritmo, sem no entanto exagerar, pois tal conduziria a grandes desperdícios de memória.  Pode-se demonstrar que um bom aumento passa por duplicar o tamanho da matriz.  Dessa forma os aumentos da matriz são esporádicos (acabam por ser amortizados entre todas as operações de colocação de novos itens intermédias que não obrigam a aumentos da matriz) e, além disso, garante-se que a matriz dinâmica está sempre ocupada a 50% pelo menos (bom, quase sempre).  Assim:

inline PilhaInt::PilhaInt()
    : capacidade(capacidade_inicial), 
      itens(new Item[capacidade]),
      número_de_itens(0) {

}

void PilhaInt::põe(Item const& novo_item)

{
    if(número_de_itens == capacidade) {
        // Como não há espaço, constrói-se uma nova matriz dinâmica com o dobro
        //  da dimensão, duplicando-se a capacidade da pilha:
        capacidade *= 2;
        Item* const novos_itens = new Item[capacidade];

        //
Copia-se para a nova matriz os itens que estavam na matriz dos itens
        // original:
        for(int i = 0; i != altura(); ++i)
            novos_itens[i] = itens[i];

        //
Destrói-se a matriz dos itens original:
        delete[] itens;

        //
A matriz dos itens passa a ser a nova matriz construída (já com os itens
        // antigos):
        itens = novos_itens;
    }
    // Agora há espaço para o novo item de certeza, pode-se inserir normalmente:
    itens[número_de_itens++] = novo_item;
}

Note-se que se referiu que a ocupação da pilha era sempre superior a 50%.  Será verdade?  Em rigor, não.  Em primeiro lugar porque a capacidade inicial é 32, e portanto a ocupação só passará a ser superior a 50% a partir do momento em que estejam pelo menos 17 itens na pilha.  Em segundo lugar porque os itens também podem sair da pilha!  Claro está que seria possível reduzir o tamanho da matriz dinâmica para metade sempre que se retirasse um item da pilha e a sua ocupação se tornasse inferior ou igual a 50%, ou outro critério semelhante que evitasse possíveis oscilações em torno de dois tamanhos.  Mas tal não será feito aqui.  Assim a implementação dos restantes métodos da classe mantém-se, com excepção do método estáCheia().  Este método torna-se complicado pela simples razão de que, se a matriz dinâmica estiver ocupada a 100%, não é possível saber se há espaço para um item adicional sem tentar aumentar a dimensão da matriz.  A forma mais simples de o fazer recorre a excepções, pelo que para já se adoptará a mesma solução (errada) que se adoptou no caso das listas: devolver sempre false.  Mais tarde se reimplementará este método de forma mais apropriada.  Assim:

inline bool PilhaInt::estáCheia() const {
    return false;
}

Esta discussão, no entanto, não visa propriamente levar a uma implementação mais eficiente do conceito de pilha.  O objectivo é discutir a utilização de memória livre dentro de classes.  A construção de variáveis dinâmicas dentro de uma classe e para seu uso exclusivo é frequente.  Sendo os princípios básicos da utilização da variáveis dinâmicas que estas variáveis têm de ser destruídas e de preferência pela mesma entidade que as construiu, torna-se evidente que a classe PilhaInt deverá responsabilizar-se pela destruição da matriz dinâmica dos itens.

2.1.3  Destrutores

Que acontece quando é executado o seguinte ciclo (admite-se que o módulo pilha_int define a classe PilhaInt)?

#include "pilha_int.H"
...
for(int i = 0; i != 100000; ++i) {
    PilhaInt p;
    ... // operações com a pilha.
}

Em cada passo do ciclo é criada uma pilha com pelo menos 32 itens.  No fim de cada passo a pilha é destruída.  Mas o que é destruído é o espaço ocupado pela instância da pilha em si (que consiste em duas variáveis inteiras, capacidade e número_de_itens, e num ponteiro, itens).  A matriz dinâmica construída no construtor da classe (ou reconstruída no procedimento põe()) nunca chega a ser destruída!  Isto significa que, depois do ciclo, existem em memória (e inacessíveis) 100000 matrizes dinâmicas com pelo menos 32 inteiros cada uma.  Se cada inteiro ocupar 4 octetos (bytes), isso significa 12,8 Moctetos de memória ocupada e impossível de libertar.

Como evitar esta brutal "fuga" de memória?  Se se recordar que as variáveis, ao serem destruídas, invocam o destrutor da respectiva classe, é claro que a solução é definir um destrutor para a classe PilhaInt que proceda à destruição da matriz dinâmica e consequente libertação da memória ocupada.

Note-se, a propósito, que o C++ procede à invocação dos destrutores de cada variável membro implicitamente, exista ou não destrutor da classe que as contém.  Ou seja, o C++ fornece sempre um destrutor às classes definidas pelo utilizador, desde que estas não forneçam um explicitamente.

Acontece, neste caso, que a matriz dinâmica é apontada pelo ponteiro itens, e que o destrutor dos ponteiros não destrói as variáveis apontadas.  Assim, a classe deverá passar a definir explicitamente um destrutor que se encarregue de destruir a matriz dinâmica dos itens:

class PilhaInt {
  public:
    typedef int Item;

    PilhaInt();
    ~PilhaInt();

   
...
};

que se define simplesmente por

inline PilhaInt::~PilhaInt() {
    delete[] itens; // é uma matriz dinâmica, por isso delete[].
}

2.1.4  Pilhas de ponteiros

É importante não confundir as variáveis dinâmicas com os ponteiros que as apontam: são variáveis diferentes.  Suponha-se que a classe implementando o conceito de pilha era alterada para guardar ponteiros para inteiros passados do exterior, e não inteiros propriamente ditos.   Isso implicava apenas alterar a definição de Item para ser um sinónimo de int* e alterar o nome da classe para PilhaPonteiroInt:

typedef int* Item;

Esta alteração é suficiente.  Aliás seria muito má ideia ter a tentação de libertar as variáveis apontadas por esses ponteiros no procedimento membro tira(), por exemplo, ou mesmo no destrutor da classe.  Ou seja:

inline void PilhaPonteiroInt::tira() {
    assert(not estáVazia());
    delete itens[número_de_itens - 1]; // péssima ideia!
    --número_de_itens;
}

Porque é essa destruição má ideia?  Imagine-se a seguinte utilização:

PilhaPonteiroInt p;
int i;
p.põe(&i);
p.tira();

O que acontece ao tentar retirar o item da pilha?  O procedimento tenta destruir uma variável que nem sequer é dinâmica.  A variável i, é local e automática, e não dinâmica!

2.1.5  Cópias

Regresse-se de novo às pilhas de inteiros.  O que acontece ao executar o seguinte código?

PilhaInt p1;
PilhaInt p2;
...
p2 = p1; // atribuição por cópia.

Isto é, o que acontece quando se atribui uma pilha a outra?  O operador de atribuição por cópia é fornecido automaticamente pelo C++ a todas as classes que não o definam explicitamente.  Este operador de atribuição por cópia gerado automaticamente simplesmente copia as variáveis membro uma a uma.  Claro está que o operador de atribuição por cópia só é fornecido automaticamente pelo C++ quando tal for possível.  Em particular tal nunca acontece se a classe possuir constantes ou referências de instância, ou se possuir atributos de instância pertencentes a classes que não tenham por sua vez operador de atribuição por cópia.

O mesmo se passa para o construtor por cópia, que também é fornecido automaticamente pela linguagem (excepto se a classe possuir atributos de instância pertencentes a classes que não tenham por sua vez construtor por cópia).  Por exemplo, o que resulta do seguinte código?

PilhaInt p1;
PilhaInt p2(p1); // (ou PilhaInt p2 = p1;) construtor por cópia.

Em ambos os casos o resultado é desastroso (mais ainda no primeiro).  Porquê?  Simplesmente porque a variável membro itens da instância p2 passa a conter o mesmo endereço que a variável membro itens da variável p1, o que significa que ambas contêm o endereço da mesma matriz dinâmica.  I.e., alterações numa das pilhas passam a afectar a outra e vice-versa.  Uma situação muito indesejável, sobretudo se se levar em conta que inserir um item numa das pilhas afectará a matriz dinâmica comum, mas não as variáveis membro número_de_itens e capacidade da outra, o que pode ser desastroso.  No caso da atribuição por cópia há um problema adicional: perde-se o ponteiro para a matriz dinâmica original, e portanto há uma fuga de memória.

Em geral as classes devem ser implementadas usando a chamada semântica de valor.  Isto significa que variáveis diferentes devem ser totalmente independentes, podendo no entanto tomar o mesmo valor.  Por exemplo, depois de

int i = 5;
int j = i;

as duas variáveis continuam perfeitamente independentes embora possuam o mesmo valor.  Uma posterior atribuição ou alteração de uma das variáveis não afecta a outra.  De igual forma se desejaria que depois de

PilhaInt p1;
PilhaInt p2;
...
p2 = p1; // atribuição por cópia.

as duas variáveis continuassem independentes, embora com o mesmo valor (que neste caso significa com o mesmo número de itens e com itens de valor idêntico).

No caso das pilhas, no entanto, o comportamento descrito acima é altamente indesejável.  É portanto necessário, portanto, definir explicitamente o construtor por cópia e o operador de atribuição por cópia que efectuem as operações com a semântica de valor desejada.

Construtor por cópia

O construtor por cópia é semelhante ao construtor já definido, mas tem um parâmetro que é uma referência constante para uma PilhaInt (no caso do construtor por cópia tem de se usar uma passagem por referência, tipicamente constante, pois se se usasse uma passagem por valor o próprio construtor por cópia seria invocado recursivamente para copiar o valor do argumento para o parâmetro respectivo).  Este construtor simplesmente constrói a matriz dinâmica com a mesma dimensão que a da pilha passada como argumento e enche-a com cópias dos seus itens, fazendo também cópias dos outros atributos:

PilhaInt::PilhaInt(PilhaInt const& outra_pilha)
    : capacidade(outra_pilha.capacidade),
      itens(new Item[capacidade]),
      número_de_itens(outra_pilha.número_de_itens) {

    // Copia itens:
    for(int i = 0; i != número_de_itens; ++i)
        itens[i] = outra_pilha.itens[i];
}

Naturalmente é necessário declarar o construtor por cópia na definição da classe:

class PilhaInt {
  public:
    typedef int Item;

    PilhaInt(); 
    PilhaInt(PilhaInt const& outra_pilha); // construtor por cópia.
    ...
};

Operador de atribuição por cópia

O caso do operador de atribuição por cópia é um pouco mais complicado.  Em primeiro lugar, é necessário recordar que a pilha à qual é feita a atribuição já possui ela própria uma matriz dinâmica, com endereço guardado na variável membro itens.  Se não se destruir essa matriz, ela permanecerá na memória depois da atribuição, constituindo assim uma fuga de memória.  Assim, no caso da atribuição por cópia é necessário não só fazer a cópia, como aconteceu no caso do construtor por cópia, mas também libertar a matriz dinâmica pré-existente.  Ou seja, é necessário definir o operador como:

PilhaInt& PilhaInt::operator = (PilhaInt const& outra_pilha) {
    capacidade = outra_pilha.capacidade;
    delete[] itens;
    itens = new Item[capacidade];
    número_de_itens = outra_pilha.número_de_itens;
 
    //
Copia itens:
    for(int i = 0; i != número_de_itens; ++i)
        itens[i] = outra_pilha.itens[i];

    return *this;

}

Uma observação atenta do código revela que há um caso para o qual a libertação e subsequente reserva de memória é desnecessária: se ambas as matrizes dinâmicas tiverem o mesmo tamanho.  Assim, a função pode-se reescrever como:

PilhaInt& PilhaInt::operator = (PilhaInt const& outra_pilha) {
    if(capacidade != outra_pilha.capacidade) {

        capacidade = outra_pilha.capacidade;
        delete[] itens;
        itens = new Item[capacidade];
    }
    número_de_itens = outra_pilha.número_de_itens;
 
    //
Copia itens:
    for(int i = 0; i != número_de_itens; ++i)
        itens[i] = outra_pilha.itens[i];

    return *this;

}

Incidentalmente esta última alteração corrigiu também um erro grave da versão original.  A versão original daria resultados dramáticos se o utilizador se lembrasse de escrever:

p1 = p1;

Tente perceber porquê executando a função passo a passo e lembrando-se que, neste caso particular, capacidade e outra_pilha.capacidade são a mesma variável (a instância implícita *this e a referência outra_pilha dizem respeito à mesma variável p1) tal como todas as outras variáveis membro (atenção ao ponteiro itens!).  Em particular verifique o que acontece durante a cópia dos itens...

Para resolver este problema é típico envolver o corpo do operador de atribuição por cópia num teste que verifica se a instância implícita e a referência outra_pilha a partir da qual a cópia será feita se referem à mesma variável.  Isso faz-se comparando os seus endereços, pois variáveis diferentes estão sempre em zonas de memória diferentes, com endereços diferentes.  Ou seja, a versão definitiva da função passa a ser:

PilhaInt& PilhaInt::operator = (PilhaInt const& outra_pilha) {
    if(this != &outra_pilha) {
        if(capacidade != outra_pilha.capacidade) {

            capacidade = outra_pilha.capacidade;
            delete[] itens;
            itens = new Item[capacidade];

        }
        número_de_itens = outra_pilha.número_de_itens;
        for(int i = 0; i != número_de_itens; ++i)
            itens[i] = outra_pilha.itens[i];
    }
    return *this;

}

Naturalmente é necessário declarar o operador de atribuição por cópia na definição da classe:

class PilhaInt {
  public:
    ...
    // Atribuição por cópia:
    PilhaInt& operator = (PilhaInt const& outra_pilha);

    ...
};

Conclui-se portanto que, sempre que uma classe reserva recursos externos para utilização exclusiva pelas suas instâncias, há que ter cuidados acrescidos na definição dos seus construtores, há que definir explicitamente um destrutor que liberte esses recursos, e há que definir explicitamente versões do construtor por cópia e do operador de atribuição por cópia que implementem semântica de valor.  Em particular, o construtor por cópia deverá normalmente reservar os seus próprios recursos externos, o mesmo acontecendo com o operador de atribuição por cópia, que deverá previamente libertar os recursos que a instância já reservava ou, alternativamente, reciclá-los (como se fez no caso das pilhas acima).  O operador de atribuição por cópia deverá ainda ser implementado de modo a ter um comportamento apropriado mesmo quando o original e a cópia são o mesmo objecto (ou variável).

2.1.6  Versão completa das pilhas

Apresenta-se abaixo a versão completa do módulo das pilhas:

pilha_int.H

#ifndef PILHA_INT_H
#define PILHA_INT_H

class PilhaInt {

  public:
    typedef int Item;

    PilhaInt();
    PilhaInt(PilhaInt const& outra_pilha);

    ~PilhaInt();

    PilhaInt& operator = (PilhaInt const& outra_pilha);


    int altura() const;
    bool estáVazia() const;

    bool estáCheia() const;
    Item const& topo() const;

    Item& topo();
    void põe(Item const& novo_item);

    void tira();

  private:
    static int const capacidade_inicial = 32;
    int capacidade;

    Item* itens;
    int número_de_itens;
};

#include "pilha_int_impl.H"

#endif // PILHA_INT_H

pilha_int_impl.H

#include <cassert>

inline PilhaInt::PilhaInt()

    : capacidade(capacidade_inicial),
      itens(new Item[capacidade]),
      número_de_itens(0) {

}

inline PilhaInt::~PilhaInt() {

    delete[] itens;
}

inline int PilhaInt::altura() const {

    return número_de_itens;
}

inline bool PilhaInt::estáVazia() const {

    return altura() == 0;
}

inline bool PilhaInt::estáCheia() const {

    return altura() == capacidade;
}

inline PilhaInt::Item const & PilhaInt::topo() const {

    assert(not estáVazia());

    return itens[número_de_itens - 1];
}

inline PilhaInt::Item& PilhaInt::topo() {

    assert(not estáVazia());

    return itens[número_de_itens - 1];

}

inline void PilhaInt::tira() {

    assert(not estáVazia());

    --número_de_itens;
}

pilha_int.C

#include "pilha_int.H"

PilhaInt::PilhaInt(PilhaInt const& outra_pilha)

    : capacidade(outra_pilha.capacidade),
      itens(new Item[capacidade]),
      número_de_itens(outra_pilha.número_de_itens) 
{

    for(int i = 0; i != número_de_itens; ++i)
        itens[i] = outra_pilha.itens[i];
}

PilhaInt& PilhaInt::operator = (PilhaInt const& outra_pilha)
{
    if(this != &outra_pilha) {
        if(capacidade != outra_pilha.capacidade) {

            capacidade = outra_pilha.capacidade;
            delete[] itens;
            itens = new Item[capacidade];

        }
        número_de_itens = outra_pilha.número_de_itens;
        for(int i = 0; i != número_de_itens; ++i)
            itens[i] = outra_pilha.itens[i];
    }
    return *this;

}

/*
O ideal seria criar um método privado para aumentar a matriz dinâmica e chamá-lo
   no if.  Nesse caso o procedimento põe() passaria a inline. */
void PilhaInt::põe(Item const& novo_item)
{
    if(número_de_itens == capacidade) {
        capacidade *= 2;
        Item* const novos_itens = new Item[capacidade];
        for(int i = 0; i != altura(); ++i)
            novos_itens[i] = itens[i];
        delete[] itens;
        itens = novos_itens;
    }
    itens[número_de_itens++] = novo_item;
}

2.2  Introdução às excepções

Como lidar com erros?  Esta é uma pergunta muito importante em programação, e a que não é fácil responder duma forma taxativa.  A primeira coisa a fazer quando se discute o tratamento de erros é distinguir entre as suas possíveis origens.  Esse assunto será tratado com maior cuidado numa aula posterior.  Para já distinguem-se três tipos:
  1. Erros do programador (produtor ou consumidor).  Verificados até agora com asserções.
  2. Erros do utilizador humano do programa.  Verificados com instruções condicionais e de selecção e corrigidos à custa de ciclos.
  3. Erros de recursos externos ao programa não humanos e portanto não corrigíveis.  Exemplos: falta de memória, ficheiros corrompidos, falta de espaço em disco, ficheiros inexistentes, etc.  Como lidar com este tipo de erros?

Existem várias abordagens possíveis para lidar com erros.  Até agora já se utilizaram as seguintes:

  1. Os erros são assinalados devolvendo valores especiais (no caso das funções) ou fazendo os procedimentos devolverem um valor booleano indicando sucesso ou insucesso.  Quem invoca as funções ou procedimentos tem a obrigação de verificar se ocorreu um erro e lidar com esse facto apropriadamente.
  2. Os erros conduzem à terminação imediata do programa com uma mensagem de erro (uma versão sofisticada é a utilização de asserções [assert]).
A primeira solução usava-se muito na biblioteca padrão da linguagem C (disponível na biblioteca padrão do C++ através dos ficheiros de cabeçalho começados por 'c', como cstdlib).  Tem a vantagem de ser flexível, pois permite ao programador consumidor do código lidar com os erros da forma que lhe parecer mais conveniente.  Mas, como normalmente verificar todos os possíveis erros leva a programas muito complexos ("cheios de ifs"), os programadores tendem a ignorar os valores devolvidos.  Na prática, portanto, esta é uma não-solução.

A segunda solução é demasiado drástica.  É verdade que muitas vezes é preferível abortar o programa a continuar depois de um erro grave.  Mas esta solução não deixa qualquer possibilidade ao utilizador programador de lidar com o erro em circunstâncias em que é possível recuperar.

O C++ fornece um mecanismo que tem as vantagens de ambas as soluções: as excepções.  Ao ocorrer um erro diz-se que se "lança uma excepção" de um dado tipo.  Se o utilizador programador nada tiver feito, o programa aborta com uma mensagem apropriada.  Se o utilizador programador tiver preparado o seu código para "capturar a excepção", o programa não aborta, sendo executado código específico, escrito pelo programador utilizador, para lidar com o erro.

Será um valor inválido introduzido pelo utilizador do programa um erro que mereça o lançamento de uma excepção, sendo o programa interactivo?  Não.  Será um erro violar as pré-condições de uma função ou procedimento, passando argumentos inválidos?  Claramente.  Deverá nesse caso ser lançada uma excepção?  Para já não, embora este assunto seja discutido em pormenor mais tarde.  Assim, as excepções ficarão reservadas para já para lidar com erros nos recursos externos de um programa.

2.2.1  Definindo excepções

As excepções são instâncias de qualquer tipo, incluindo classes definidas pelo utilizador.  Por exemplo, uma excepção para assinalar que se tentou inserir um item numa pilha cuja capacidade se esgotou e não foi possível reservar mais memória poderia ser simplesmente uma instância da classe:

class PilhaIntMemóriaEsgotada {};

Ou, usando classes embutidas:

class PilhaInt {
  public:
    ...
    class MemóriaEsgotada {};
    ...
};

As classes cujas instâncias são usadas como excepção servem mais para distinguir entre tipos de excepções (erros), do que para guardar dados, embora também se possam usar com esse objectivo, servindo os dados para guardar informação pormenorizada sobre o erro que lhes deu origem.  Assim, é comum encontrarem-se classes usadas para excepções sem quaisquer membros.

Como se utilizam as excepções?

2.2.2  Lançando excepções

Suponha-se que se pretende assinalar um erro no procedimento membro põe() da classe PilhaInt quando a pilha estiver na sua capacidade máxima e não for possível construir a nova matriz dinâmica dos itens.  Pode-se definir o procedimento como se segue:

void PilhaInt::põe(Item const& novo_item)
{
    if(número_de_itens == capacidade) {
        Item* const novos_itens = new Item[capacidade * 2];
        se a construção falhou faça-se

            throw MemóriaEsgotada();
        capacidade *= 2;

        for(int i = 0; i != altura(); ++i)
            novos_itens[i] = itens[i];
        delete[] itens;
        itens = novos_itens;
    }
    itens[número_de_itens++] = novo_ item;
}

Ou seja, se a pilha estiver no limite e não for possível construir uma nova matriz dinâmica dos itens é lançada uma excepção que é uma instância da classe PilhaInt::MemóriaEsgotada.  Note-se bem: uma instância da classe, e não uma classe, daí a necessidade dos parênteses após o nome da classe, que provocam a invocação do construtor da classe e portanto a criação duma nova instância da classe.

Note-se que se a excepção for lançada a pilha fica rigorosamente no estado em que estava originalmente.  Foi por isso que se atrasou a operação de duplicação do atributo capacidade.

Por vezes os construtores das classes de excepções têm parâmetros que identificam melhor o erro.  Nesse caso colocam-se argumentos entre parênteses na instrução de lançamento da excepção.

2.2.3  Capturando excepções e a excepção bad_alloc

O código acima não está completo.  Como saber se uma utilização do operador new[] teve sucesso?  Acontece que, em cado de insucesso, o operador new lança uma excepção: bad_alloc, que está definida no ficheiro de interface padrão new (fazer #include <new>).  Como pretendemos, como programadores produtores da classe,  lidar com essa excepção, i.e., capturá-la, temos de envolver o código onde a excepção pode ser lançada num bloco de tentativa, dizendo explicitamente o que fazer quando uma excepção de um determinado tipo é capturada *:

void PilhaInt::põe(Item const& novo_item)
{
    if(número_de_itens == capacidade) {
        try {

            Item* const novos_itens = new Item[capacidade * 2];
            capacidade *= 2;

            for(int i = 0; i != altura(); ++i)
                novos_itens[i] = itens[i];
            delete[] itens;
            itens = novos_itens;
        } catch(bad_alloc) {
            throw MemóriaEsgotada();
        }
    }

    itens[número_de_itens++] = novo_ item;
}

Curiosamente neste caso captura-se uma excepção simplesmente para a substituir/traduzir por outra.

* Para capturar qualquer tipo de excepção, usar catch(...).

2.2.4  Excepções com dados

A classe excepção definida pode ser melhorada.  Em particular pode ser vantajoso indicar qual a capacidade para a qual não foi possível obter uma nova matriz dinâmica.  

class PilhaInt {
  public:
    ...
    class MemóriaEsgotada {
      public:
        MemóriaEsgotada(int dimensão_pretentida)
            : dimensão_pretendida(dimensão_pretendida) {
        }
        int dimensãoPretendida() {
            return dimensão_pretendida;
        }
      private:
        int dimensão_pretendida; 
    };

    ...
};

Nesse caso o método põe() seria:

void PilhaInt::põe(Item const& novo_item)
{
    if(número_de_itens == capacidade) {
        try {

            Item* const novos_itens = new Item[capacidade * 2];
            capacidade *= 2;

            for(int i = 0; i != altura(); ++i)
                novos_itens[i] = itens[i];
            delete[] itens;
            itens = novos_itens;
        } catch(bad_alloc) {
            throw MemóriaEsgotada(capacidade * 2);
        }
    }

    itens[número_de_itens++] = novo_ item;
}

A captura da excepção poderia ser feita como se segue:

#include <iostream>

using namespace std;

#include "pilha_int.H"

int main()

{
    try {
        PilhaInt p;

       
...
    } catch(PilhaInt::MemóriaEsgotada e) {
        cout << "Estoirou ao tentar aumentar capacidade para " 
             << e.dimensãoPretendida()
<< "!" << endl;
    }
}

Note-se que neste caso se passaram argumentos ao construtor da excepção e que portanto o código que lida com erro recebe mais informação, nomeadamente qual a capacidade pretendida para a pilha que conduziu à excepção PilhaInt::MemóriaEsgotada.  Note-se também que para receber a informação da excepção faz-se a captura da excepção nomeando uma variável para a conter depois de capturada (neste caso de nome e).

O mesmo bloco de tentativa pode capturar tipos de excepção diferentes.  Por exemplo:

#include <iostream>

using namespace std;

#include "pilha_int.H"

int main()

{
    try {
        PilhaInt p;

       
...
    } catch(PilhaInt::MemóriaEsgotada e) {
        cout << "Estoirou ao tentar aumentar capacidade para " 
             << e.dimensãoPretendida() << "!" << endl;

    } catch(...) {
        cout << "Ooops...  Outra excepção qualquer..." << endl;
    }

}

Finalmente, todo o corpo de uma função ou procedimento pode consistir num grande bloco de tentativa.  Por exemplo:

#include <iostream>

using namespace std;

#include "pilha_int.H"

int main()

    try {
        PilhaInt p;

       
...
    } catch(PilhaInt::MemóriaEsgotada e) {
        cout << "Estoirou ao tentar aumentar capacidade para " 
             << e.dimensãoPretendida() << "!" << endl;

    } catch(...) {
        cout << "Ooops...  Outra excepção qualquer..." << endl;
    }

Como é óbvio, um bloco de tentativa pode constar em qualquer rotina ou método (e não apenas em main()).

2.2.5  Módulo das pilhas com excepções

A versão completa do módulo pilha_int das pilhas com excepções, a melhorar na aula prática, divide-se pelos ficheiros pilha_int.H, pilha_int_impl.H e pilha_int.C.

2.3  Leitura recomendada

Recomenda-se a leitura do Capítulo 5 de [1].


3  Referências

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

# Existem 10 exemplares na biblioteca do ISCTE.