No resumo anterior apresentou-se uma classe para representar pilhas de itens sem qualquer limitação a priori quanto ao número de itens que pode suportar*. Neste resumo far-se-á o desenvolvimento passo a passo dessa classe partindo de uma versão inicial, com dimensão limitada.
Seja uma classe PilhaDeInt representando pilhas limitadas
de inteiros.  O seu código
divide-se por dois ficheiros fonte (de interface e auxiliar de implementação):
pilha_de_int.H
#ifndef PILHA_DE_INT_H#define PILHA_DE_INT_H
/**Representa pilhas deint.@invariant 0 <=
número_de_itens<=capacidade_máxima.*/
class PilhaDeInt {
public:
typedef int Item;
/**Constrói pilha vazia.@pre V.
@post
estaVazia().*/
PilhaDeInt();
/**Indica se a pilha está vazia.@pre V.
@post estaVazia =
*thisestá vazia.*/
bool estáVazia() const;
/**Indica se a pilha está cheia.@pre V.
@post estaVazia =
*thisestá cheia.*/
bool estáCheia() const;
/**Devolve altura da pilha.@pre V.
@post
altura= altura de*this.*/
int altura() const;
/**Devolve o item que está no topo da pilha.@pre
¬estaVazia().@post
topoidêntico ao item no topo de*this.*/
Item const& topo() const;
/**Devolve o item que está no topo da pilha.@pre
¬estaVazia().@post
topoidêntico ao item no topo de*this.*/
Item& topo();
/**Põe um novo item na pilha (no topo).@pre V.
@post
*thiscontém um item adicional no topo igual anovo_item.*/
void põe(Item const& novo_item);
/**Tira o item que está no topo da pilha.@pre ¬
estaVazia().@post
*thiscontém os itens originais menos o do topo.*/
void tiraItem();
private:
static int const capacidade_máxima = 100;Item itens[capacidade_máxima];int número_de_itens;
/**Indica se a pilha verifica a condição invariante da classe.@pre V.
@post
cumpreInvariante=
(0 <=número_de_itens<=capacidade_actual).*/
bool cumpreInvariante() const;
};
#include "pilha_de_int_impl.H"
#endif // PILHA_DE_INT_H
pilha_de_int_impl.H
#include <cassert>
inline PilhaDeInt::PilhaDeInt(): número_de_itens(0)
{
assert(cumpreInvariante());
}
inline bool PilhaDeInt::estáVazia() const
{assert(cumpreInvariante());
return altura() == 0;}
inline bool PilhaDeInt::estáCheia() const
{assert(cumpreInvariante());
return altura() == capacidade_máxima;}
inline int PilhaDeInt::altura() const
{assert(cumpreInvariante());
return número_de_itens;}
inline PilhaDeInt::Item const& PilhaDeInt::topo() const
{assert(cumpreInvariante());
assert(not estáVazia());
return itens[número_de_itens - 1];}
inline PilhaDeInt::Item& PilhaDeInt::topo()
{assert(cumpreInvariante());
assert(not estáVazia());
return itens[número_de_itens - 1];}
inline void PilhaDeInt::põe(Item const& novo_item)
{assert(cumpreInvariante());
assert(not estáCheia());
itens[número_de_itens] = novo_item;
++número_de_itens;
assert(cumpreInvariante());
}
inline void PilhaDeInt::tiraItem()
{assert(cumpreInvariante());
assert(not estáVazia());
--número_de_itens;
assert(cumpreInvariante());
}
inline bool PilhaDeInt::cumpreInvariante() const
{
return 0 <= número_de_itens <= capacidade_máxima;
}
Estude atentamente o código acima antes de passar à próxima secção: ele
corresponde à versão completa do TAD PilhaDeDouble desenhado na
última aula teórica de Introdução à Programação.
* Na realidade há sempre o limite dado pelo tipo inteiro usado para guardar o número de itens na pilha. Mas esse limite é suficientemente grande para não trazer grandes problemas.
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 clássica (não-dinâmica) usada na implementação
original.  O atributo número_de_itens manter-se-á,
mas é necessário passar o atributo capacidade_máxima de
constante de classe a variável de instância, mudando-lhe o nome para capacidade_actual,
de modo a deixar claro que já
não reflecte a capacidade máxima de todas as pilhas mas sim a capacidade de
cada instância da classe C++, i.e., 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
das pilhas, ou seja, a dimensão inicial da matriz dinâmica:
...
/**Representa pilhas deint.@invariant
itensaponta matriz comcapacidade_actualitens e
capacidade_inicial<=capacidade_actuale
0 <=número_de_itens<=capacidade_actual.*/
class PilhaDeInt {
public:
...
private:
static int const capacidade_inicial = 32;
int capacidade_actual;Item* itens;int número_de_itens;
/**Indica se a pilha verifica a condição invariante da classe.@pre V.
@post
cumpreInvariante= (itens<> 0 e
capacidade_inicial<=capacidade_actuale
0 <=número_de_itens<=capacidade_actual).*/
bool cumpreInvariante() const;
};...
inline bool PilhaDeInt::cumpreInvariante() const
{
return itens != 0 and
capacidade_inicial <= capacidade_actual and
0 <= número_de_itens <= capacidade_actual;
}
As mudanças principais a realizar na implementação
da classe são na definição do construtor e na definição
da operação PilhaDeInt::põe().  O construtor deverá
construir a matriz dinâmica com a dimensão apropriada.  A
operação PilhaDeInt::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 do ponto de vista computacional.  É, 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 uma boa estratégia passa por duplicar a capacidade da matriz sempre que a capacidade actual for insuficiente. 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).
Esta estratégia, de fazer a capacidade da matriz aumentar geometricamente, tem claras vantagens face a uma estratégia em que o aumento se faz aritmeticamente. A seguinte tabela ilustra as diferenças:
| Progressão geométrica em m (capacidade multiplicada por m, com 1 < m). Admite-se que capacidade inicial é 1. | Progressão aritmética em m (capacidade somada de m, com 0 < m). Admite-se que a capacidade inicial é 0. | |
| Número médio c de cópias de itens realizadas após n inserções de itens | c = (1 - 1 / n) /
      (m - 1) < 1 / (m - 1) c = (1 - 1 / n) para m = 2 (c aprox= 1 para n grande) c = (1 - 1 / n) / 2 para m = 3 (c aprox= 1/2 para n grande) etc. | c = n / m / 2 - 1
      / 2 < n / m / 2 c = n / 2 - 1 / 2 para m = 1 (c aprox= n / 2 para n grande) c = n / 4 - 1 / 2 para m = 2 (c aprox= n / 4 para n grande) etc. | 
| Taxa t de ocupação da matriz com n itens na pilha | 1 / m < t <= 1 | n / (n + m)
      < t <= 1 t aprox= 1 para n grande | 
| Conclusões: | Pior taxa de ocupação,
      reduzido número de cópias. Bom compromisso: m = 2 (c aprox= 1 cópia por inserção, t > 50%) | Melhor taxa de ocupação, elevado número cópias. | 
Assim:
inline PilhaDeInt::PilhaDeInt(): capacidade_actual(capacidade_inicial),
itens(new Item[capacidade_actual]),
número_de_itens(0)
{}
void PilhaDeInt::põe(Item const& novo_item){assert(cumpreInvariante());
assert(not estáCheia());
if(número_de_itens == capacidade_actual) {//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_actual *= 2;Item* const novos_itens = new Item[capacidade_actual];
//Copia-se para a nova matriz os itens que estavam na matriz dos
//itens original:for(int i = 0; i != número_de_itens; ++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;
++número_de_itens;
assert(cumpreInvariante());
}
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 não 1, como
indicado na tabela), 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 duas
capacidades sucessivas.  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 PilhaDeInt::estáCheia().
O método PilhaDeInt::estáCheia() 
representa um problema 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.  Discutiu-se no resumo
da aula 5 que havia essencialmente três soluções para este problema (no
contexto das listas):
false.  É a pior das soluções porque há situações em que não corresponde à verdade.PilhaDeInt::estáCheia(), no caso de não haver mais
espaço disponível, tenta aumentar (ou melhor, reconstruir) a matriz dos itens.  Se conseguir,
devolve false, uma vez que passou a haver de certeza espaço para
mais um item.  Senão
devolve true.  Note-se que há uma mudança no
contrato da classe.  Dantes, se PilhaDeInt::estáCheia() devolvesse
true
era certo que a próxima operação de inserção
(se não fosse precedida por nenhuma de remoção) falharia. 
O novo contrato estabelece que se PilhaDeInt::estáCheia() devolver true
nada pode ser garantido (pois pode passar a haver memória disponível
por um outro programa concorrente ter libertado memória).No entanto, o problema pode ser visto por um prisma um pouco diferente. 
Pode-se considerar que só excepcionalmente poderá falhar a colocação de
itens numa pilha que não tenha nenhuma limitação a priori da sua
capacidade.  Como tal, o lançamento da excepção bad_alloc
pode ser visto como a forma mais indicada de lidar com este problema.  Face
a esta forma de lidar com erros, que será vista pormenorizadamente mais abaixo,
a melhor solução talvez seja mesmo eliminar a operação PilhaDeInt::estáCheia()
deixando à operação PilhaDeInt::põe() a responsabilidade de
lançar essa excepção em caso de falta de memória (na realidade não será o
código do método correspondente a lançar a excepção, mas o operador new
por ele usado, como se verá).
É frequente a construção de variáveis
dinâmicas dentro de uma classe e para seu uso exclusivo.  Uma das
políticas de gestão variáveis dinâmicas mais simples e eficaz é
a de que estas variáveis devem ser destruídas pela mesma entidade que
as construiu.  Se se seguir essa política neste caso, é evidente que a classe
PilhaDeInt deverá
responsabilizar-se pela destruição da matriz dinâmica
dos itens.
pilha_de_int define a classe PilhaDeInt)?
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 dois atributos inteiros,
#include "pilha_de_int.H"...
for(int i = 0; i != 100000; ++i) {PilhaDeInt p;
...//operações com a pilha.}
capacidade_actual
e número_de_itens, e um ponteiro, itens).  A matriz dinâmica
construída no construtor da classe (ou reconstruída no método PilhaDeInt::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 instâncias de uma classe C++, ao serem destruídas, invocam o destrutor
dessa classe, é claro que a solução é
definir um destrutor para a classe PilhaDeInt 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 explícito para a classe que as contém. Ou seja, o C++ fornece sempre implicitamente um destrutor às classes definidas pelo utilizador, desde que estas não declarem 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 declarar, e definir, explicitamente um destrutor que se encarregue de destruir
a matriz dinâmica dos itens:
que se define simplesmente por
class PilhaDeInt {
public:
...
/**Destrói a pilha.@pre V.
@post recursos externos reservados foram libertados.
*/
~PilhaDeInt();
...
};
inline PilhaDeInt::~PilhaDeInt()
{assert(cumpreInvariante());
delete[] itens; //é uma matriz dinâmica, por issodelete[].}
Item para ser um sinónimo
de int* e alterar o nome da classe para PilhaDePonteiroInt:
Esta alteração é suficiente. Aliás seria muito má ideia ter a tentação de libertar as variáveis apontadas por esses ponteiros na operação
typedef int* Item;
PilhaDePonteiroInt::tiraItem(), por exemplo,
ou mesmo no destrutor da classe.  Ou seja:
Porque é essa destruição má ideia? Imagine-se a seguinte utilização:
inline void PilhaDePonteiroInt::tiraItem()
{assert(cumpreInvariante());
assert(not estáVazia());
delete itens[número_de_itens - 1]; //péssima ideia!
--número_de_itens;
assert(cumpreInvariante());
}
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
PilhaDePonteiroInt pilha;
int i;
pilha.põe(&i);pilha.tiraItem();
i, é local e automática, e não
dinâmica: é um erro gravíssimo tentar destruí-la através do operador delete.Isto é, o que acontece quando se atribui uma pilha a outra? O operador de atribuição por cópia é fornecido implicitamente pelo C++, sempre que possível, a todas as classes C++ que não o declarem explicitamente. Este operador de atribuição por cópia fornecido implicitamente limita-se a copiar os atributos variáveis um a um. Se existirem atributos de instância constantes ou referências, o operador de atribuição por cópia não é fornecido implicitamente, como é evidente. O mesmo acontece se a classe possuir atributos variáveis de instância pertencentes a classes C++ que não tenham, por sua vez operador, de atribuição por cópia.
PilhaDeIntp1;PilhaDeIntp2;...
p2 = p1; //atribuição por cópia.
Algo de semelhante se passa com o construtor por cópia, que também é fornecido implicitamente pela linguagem (excepto se a classe possuir atributos de instância pertencentes a classes C++ que não tenham por sua vez construtor por cópia). Por exemplo, o que resulta do seguinte código?
Em ambos os casos o resultado é desastroso (mais ainda no primeiro). Porquê? Simplesmente porque a variável membro
PilhaDeIntp1;PilhaDeIntp2(p1); //(ouPilhaDeIntp2 = p1;) construtor por cópia.
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_actual
da outra, o que pode ser desastroso.  No caso da atribuição
por cópia há ainda um problema adicional grave: perde-se o ponteiro para
a matriz dinâmica original, e portanto há uma fuga de memória!Em geral, as classes C++ que sirvam para concretizar TAD 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
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
int i = 5;int j = i;
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).
PilhaDeIntp1;PilhaDeIntp2;...
p2 = p1; //atribuição por cópia.
No caso das pilhas o comportamento descrito acima, em que depois de uma atribuição por cópia ou de uma construção por cópia duas instâncias da classe partilham os seus "órgão internos", é altamente indesejável. É como se as duas variáveis ficassem siamesas, em vez de gémeos verdadeiros, como se desejava. É necessário, portanto, declarar (e definir) explicitamente o construtor por cópia e o operador de atribuição por cópia de tal forma que efectuem as operações com a semântica de valor desejada.
PilhaDeInt (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...).
O construtor por cópia limita-se a construir a matriz dinâmica com a mesma dimensão que a da pilha passada como argumento, a pilha original, e enche-a com cópias dos seus itens, fazendo também cópias dos restantes atributos:
Naturalmente é necessário declarar o construtor por cópia na definição da classe:
PilhaDeInt::PilhaDeInt(PilhaDeInt const& original): capacidade_actual(original.capacidade_actual),
itens(new Item[capacidade_actual]),
número_de_itens(original.número_de_itens)
{//Copia os itens:for(int i = 0; i != número_de_itens; ++i)itens[i] = original.itens[i];
assert(cumpreInvariante());
}
class PilhaDeInt {public:
...
/**Construtor por cópia.@pre V.
@post
*this=original.*/
PilhaDeInt(PilhaDeInt const& original);
...
};
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:
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 rescrever como:
PilhaDeInt& PilhaDeInt::operator=(PilhaDeInt const& modelo)
{assert(cumpreInvariante());
capacidade_actual = modelo.capacidade_actual;delete[] itens;itens = new Item[capacidade_actual];número_de_itens = modelo.número_de_itens;Copia itens:
//for(int i = 0; i != número_de_itens; ++i)itens[i] = modelo.itens[i];
assert(cumpreInvariante());
return *this;}
Acidentalmente, 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:
PilhaDeInt& PilhaDeInt::operator=(PilhaDeInt const& modelo)
{
assert(cumpreInvariante());
if(capacidade_actual != modelo.capacidade_actual) {capacidade_actual = modelo.capacidade_actual;delete[] itens;itens = new Item[capacidade_actual];
}número_de_itens = modelo.número_de_itens;
for(int i = 0; i != número_de_itens; ++i)itens[i] = modelo.itens[i];
assert(cumpreInvariante());
return *this;}
Tente perceber porquê executando o corpo do operador passo a passo e lembrando-se que, neste caso particular,
p1 = p1;
capacidade e modelo.capacidade
são  a mesma variável (a instância implícita
*this
e a referência modelo 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...
De facto, no código original o que acontece é que a matriz dinâmica do modelo é destruída antes de construída a matriz dinâmica da cópia, pelo que, sendo ambas a mesma pilha na realidade, a cópia dos itens faz-se... a partir da matriz dinâmica acabada de construir. Se os itens forem de tipos básicos do C++, isto equivale a encher a pilha com itens com valores arbitrários. Se os itens forem de uma classe C++, ficarão todos inicializados com o construtor por omissão.
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 modelo
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: a identidade verifica-se em C++
comparando endereços!  Ou seja, a versão
definitiva da função passa a ser:
Naturalmente é necessário declarar o operador de atribuição por cópia na definição da classe:
PilhaDeInt& PilhaDeInt::operator=(PilhaDeInt const& modelo)
{
assert(cumpreInvariante());
if(this != &modelo) {
if(capacidade_actual != modelo.capacidade_actual) {capacidade_actual = modelo.capacidade_actual;delete[] itens;itens = new Item[capacidade_actual];
}número_de_itens = modelo.número_de_itens;
for(int i = 0; i != número_de_itens; ++i)itens[i] = modelo.itens[i];
}
assert(cumpreInvariante());
return *this;}
class PilhaDeInt {public:
...
/**Atribuição por cópia.@pre V.
@post
*this=modelo.*/
PilhaDeInt& operator=(PilhaDeInt const& modelo);
...
};
"Uma classe que reserve recursos externos para utilização exclusiva pelas suas instâncias deverá geralmente fornecer explicitamente (a) um destrutor, (b) um construtor por cópia e (c) um operador de atribuição por cópia."
De facto, 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 e há geralmente que declarar, e normalmente definir, explicitamente um destrutor que liberte esses recursos e 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 modelo e a instância a alterar são a mesma instância, i.e., mesmo quando são idênticas instâncias.
Alternativamente poderia ter-se protegido o operador de atribuição por cópia de modo a realizar a cópia apenas quando a instância a alterar e o modelo fossem diferentes. Se forem iguais, para quê copiar? No entanto, isso implicaria definir operadores de igualdade e diferença para a classe C++, o que nem seria má ideia, mas sobretudo isso implicaria que antes de proceder a uma cópia o código teria de percorrer as pilhas para verificar se de facto elas são diferentes. A solução apresentada verifica se as pilhas são idênticas, e não iguais, pelo que de facto se evitam auto-atribuições, mas por outro lado se realizam cópias mesmo entre pilhas iguais: o trabalho de fazer as cópias dos itens entre as duas pilhas é semelhante ao de verificar se elas são iguais.
Finalmente, note-se a importante diferença que se estabeleceu entre identidade e igualdade. Podemos resumir essa diferença dizendo que dois gémeos verdadeiros são iguais, mas não são idênticos, ou seja, não são a mesma pessoa.
Como lidar com erros? Esta é uma pergunta muito importante em programação, e a que não é fácil responder de uma forma taxativa. A primeira coisa a fazer quando se discute o tratamento de erros é distinguir entre as suas possíveis origens. Este assunto será tratado com maior cuidado numa aula posterior, distinguindo-se para já três origens para os erros:
Por outro lado, quando se desenvolvem aplicações, surgem ocasionalmente casos excepcionais que, se se quiser que a aplicação seja capaz de lidar com eles, tendem a obrigar a um considerável aumento da complexidade do código, que deixa de ter um fluxo de execução tão claro. Quando ocorrem semelhantes casos, é melhor tratá-los como tal, i.e., como excepções, usando as ferramentas que a linguagem C++ fornece para as suportar.
Existem várias abordagens possíveis para lidar com erros. Até agora já se utilizaram as seguintes:
assert]).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" e por isso com muitos possíveis fluxos de
execução), 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 programador consumidor do código que gerou o erro de lidar com ele, sendo certo que há circunstâncias em que é possível recuperar de situações de erro.
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 programador consumidor nada tiver feito, o programa aborta com uma mensagem apropriada. Se o programador consumidor 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 consumidor, 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 e com situações que façam claramente parte das que devem ser previstas pelo programa, mas que constituam casos suficientemente excepcionais para não se justificar aumentar a complexidade do código de modo a lidar directamente com eles, preferindo-se lançar um excepção que assinala esse caso e ter a esperança que algum outro pedaço de código seja capaz de lidar com ele e, por isso, capture essa excepção.
Ou, usando classes embutidas:
class PilhaDeIntMemó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 as circunstâncias que lhes deram origem. Assim, é comum encontrarem-se classes C++ usadas para excepções sem quaisquer membros.
class PilhaDeInt {public:
...
/*Nota importante: No final verificar-se-á que é má ideia usar esta classe!
Leia o texto até ao fim!*/
class MemóriaEsgotada {
};
...
};
Como se utilizam as excepções?
PilhaDeInt::põe()
 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:
Ou seja, se a pilha estiver no limite de capacidade 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
void PilhaDeInt::põe(Item const& novo_item)
{assert(cumpreInvariante());
if(número_de_itens == capacidade_actual) {Item* const novos_itens = new Item[capacidade_actual * 2];
se a construção falhou faça-sethrow MemóriaEsgotada();
capacidade_actual *= 2;
for(int i = 0; i != número_de_itens; ++i)novos_itens[i] = itens[i];
delete[] itens;
itens = novos_itens;}
itens[número_de_itens] = novo_item;
++número_de_itens;
assert(cumpreInvariante());
}
PilhaDeInt::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 por omissão da classe e portanto a
construçã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_actual, pois de outra forma a
pilha ficaria num estado inconsistente, i.e., sem que se cumprisse a sua
condição invariante, quando fosse lançada uma excepção.  Este tipo de
preocupação é muito importante e será visto com mais pormenor mais à
frente.
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.
bad_allocO código acima não está completo.  Como saber se uma utilização do
operador new[] teve sucesso?  Acontece que, em caso de
insucesso, o operador new lança uma excepção: std::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 PilhaDeInt::põe(Item const& novo_item)
{assert(cumpreInvariante());
if(número_de_itens == capacidade_actual)
try {Item* const novos_itens = new Item[capacidade_actual * 2];
capacidade_actual *= 2;
for(int i = 0; i != número_de_itens; ++i)novos_itens[i] = itens[i];
delete[] itens;
itens = novos_itens;} catch(std::bad_alloc) {
throw MemóriaEsgotada();
}
itens[número_de_itens] = novo_item;
++número_de_itens;
assert(cumpreInvariante());
}
Curiosamente neste caso captura-se uma excepção simplesmente para a
substituir/traduzir por outra.  I.e., quando se captura uma excepção do
tipo std::bad_alloc, lança-se uma excepção do tipo PilhaDeInt::MemóriaEsgotada.
* Para capturar qualquer tipo de excepção,
usar catch(...).
Nesse caso o método
class PilhaDeInt {public:
...
class MemóriaEsgotada {
public:
MemóriaEsgotada(int dimensão_pretentida)
: dimensão_pretendida(dimensão_pretendida)
{
}
int dimensãoPretendida() const
{
return dimensão_pretendida;
}
private:
int dimensão_pretendida;
};
...
};
PilhaDeInt::põe() seria:
A captura da excepção poderia ser feita como se segue:
void PilhaDeInt::põe(Item const& novo_item)
{assert(cumpreInvariante());
if(número_de_itens == capacidade_actual)
try {Item* const novos_itens = new Item[capacidade_actual * 2];
capacidade_actual *= 2;
for(int i = 0; i != número_de_itens; ++i)novos_itens[i] = itens[i];
delete[] itens;
itens = novos_itens;} catch(std::bad_alloc) {
throw MemóriaEsgotada(capacidade * 2);
}
itens[número_de_itens] = novo_item;
++número_de_itens;
assert(cumpreInvariante());
}
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
#include <iostream>
using namespace std;
#include "pilha_de_int.H"
int main(){try {PilhaDeInt p;
...
} catch(PilhaDeInt::MemóriaEsgotada& excepção) {cout << "Estoirou ao tentar aumentar capacidade para "
<< excepção.dimensãoPretendida() << "!" << endl;}
}
PilhaDeInt::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 (neste
caso uma referência) para a conter depois de capturada (neste caso de nome excepção).
O mesmo bloco de tentativa pode capturar tipos de excepção diferentes. Por exemplo:
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_de_int.H"
int main(){try {PilhaDeInt p;...
} catch(PilhaDeInt::MemóriaEsgotada excepção) {cout << "Estoirou ao tentar aumentar capacidade para "
<< excepção.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
#include <iostream>
using namespace std;
#include "pilha_de_int.H"
int main()try {PilhaDeInt p;...
} catch(PilhaDeInt::MemóriaEsgotada excepção) {cout << "Estoirou ao tentar aumentar capacidade para "
<< excepção.dimensãoPretendida() << "!" << endl;} catch(...) {
cout << "Ooops... Outra excepção qualquer..." << endl;
}
main()).Na realidade o exemplo dado acima não faz muito sentido.  Se não
houver memória suficiente, o melhor é não capturar a excepção std::bad_alloc,
deixando que seja código a montante a lidar com ela.  Traduzir excepções
raramente é uma solução inteligente.
Muito mais importante do que lidar directamente com excepções é preparar o
código para que seja seguro de utilizar mesmo que sejam lançadas
excepções.  Voltemos ao código original do método PilhaDeInt::põe():
void PilhaDeInt::põe(Item const& novo_item)
{assert(cumpreInvariante());
if(número_de_itens == capacidade_actual) {capacidade_actual *= 2;Item* const novos_itens = new Item[capacidade_actual];
for(int i = 0; i != número_de_itens; ++i)novos_itens[i] = itens[i];
delete[] itens;
itens = novos_itens;}
itens[número_de_itens] = novo_item;
++número_de_itens;
assert(cumpreInvariante());
}
Que sucede se o operador new[] lançar uma excepção?  O
fluxo de execução é interrompido, sendo retomado no primeiro bloco de captura
encontrado a montante que esteja preparado para capturar a excepção.  Por
exemplo, se no código
...
void f(PilhaDeInt& pilha)
{
PilhaDeInt outra_pilha;
pilha.põe(100);
}
void g(PilhaDeInt& pilha)
{
PilhaDeInt ainda_outra_pilha;
f(pilha);
}
int main()
{
PilhaDeInt pilha;
try {
g(pilha);
} catch(std::bad_alloc) {
cout << "Falta de memória!" << endl;
} catch(...) {
cout << "Outro erro!" << endl;
}
}
for lançada uma excepção ao adicionar um item à pilha, o fluxo de
execução abandona o procedimento f(), o procedimento g()
que o invocou, e abandonaria a função main(), se esta não
tivesse um bloco de tentativa com uma captura da excepção lançada.  Mas
o processo de abandono é controlado.  No exemplo acima esse abandono
garante a destruição das variáveis locais outra_pilha e ainda_outra_pilha,
apesar de não se ter atingido o final do bloco de instruções onde elas estão
definidas.
No método PilhaDeInt::põe() esse abandono tem, no entanto, uma
consequência nefasta: se for lançada uma excepção, o atributo capacidade_actual
foi já alterado (o valor foi duplicado) sem que na realidade a matriz dinâmica
apontada pelo atributo itens tenha sido alterado.  Ou seja, o
invariante da classe tornou-se falso!  Uma solução para o problema passa
por alterar a ordem pela qual as instruções do método são invocadas de modo
a garantir que a pilha fica num estado válido (i.e., cumprindo o invariante),
apesar do lançamento de uma excepção:
void PilhaDeInt::põe(Item const& novo_item)
{assert(cumpreInvariante());
if(número_de_itens == capacidade_actual) {capacidade_actual *= 2;Item* const novos_itens = new Item[capacidade_actual * 2];
capacidade_actual *= 2;
for(int i = 0; i != número_de_itens; ++i)novos_itens[i] = itens[i];
delete[] itens;
itens = novos_itens;}
itens[número_de_itens] = novo_item;
++número_de_itens;
assert(cumpreInvariante());
}
Este tipo de problema é mais profundo do que parece à primeira vista, e
não é fácil lidar com ele.  Suponha-se, por exemplo, que a classe PilhaDeInt
era alterada de modo a conter cadeias de caracteres.  Ou seja, mudava-se o
nome da classe para PilhaDeString e definia-se Item
como sinónimo de string.  Acontece que a classe string
usa memória livre para guardar os caracteres que compõem a cadeia de
caracteres, e que, por isso, pode precisar de reservar memória dinâmica
durante uma atribuição entre duas instâncias da classe.  Isso significa
que a excepção std::bad_alloc pode ser lançada durante a
cópia dos itens da matriz original para a sua nova versão, com a capacidade
duplicada.  Se isso acontecer, o fluxo de execução abandona o método,
deixando não apenas o atributo capacidade_actual com o valor
errado, facto que se resolve facilmente movendo de novo a instrução que lhe
duplica o valor, mas sobretudo deixa a nova matriz dinâmica construída em
memória sem nenhum ponteiro que lhe permita aceder, pois a variável novos_itens,
sendo local, é destruída.
Infelizmente, neste caso é necessário capturar a excepção para lidar com a situação:
void PilhaDeInt::põe(Item const& novo_item)
{assert(cumpreInvariante());
if(número_de_itens == capacidade_actual) {Item* const novos_itens = new Item[capacidade_actual * 2];
capacidade_actual *= 2;
try {
for(int i = 0; i != número_de_itens; ++i)novos_itens[i] = itens[i];
} catch(...) {
delete[] novos_itens;
}
capacidade_actual *= 2;
delete[] itens;
itens = novos_itens;}
itens[número_de_itens] = novo_item;
++número_de_itens;
assert(cumpreInvariante());
}
Note-se que se decidiu capturar qualquer excepção. Desta forma, se classe dos itens lançar um qualquer tipo de excepção durante a atribuição entre os itens, tem-se a garantia de que a matriz dinâmica dos novos itens é destruída. Infelizmente o código não se pode ficar por aqui. É que o código acima captura a excepção, destrói a matriz dinâmica, e depois continua como se nada tivesse sucedido! Nada mais errado. O que se pretendia era simplesmente lidar com a libertação da matriz dinâmica em caso de erro, e não lidar com as excepções em si! A solução passa por, depois de destruir a matriz dinâmica, relançar a excepção capturada.
Se excepção capturada tivesse sido de uma tipo específico, relançá-la seria fácil:
try {
for(int i = 0; i != número_de_itens; ++i)novos_itens[i] = itens[i];
} catch(std::bad_alloc& excepção) {
delete[] novos_itens;
throw excepção;
}
No entanto, a melhor forma de relançar uma excepção capturada num bloco de captura é escrever simplesmente
throw;
Esta forma tem a vantagem adicional de garantir o relançamento independentemente de ser ou não conhecido o tipo da excepção. Ou seja, funciona mesmo que se tenha usado a forma catch(...) de captura indiscriminada. Assim, o código final fica:
void PilhaDeInt::põe(Item const& novo_item)
{assert(cumpreInvariante());
if(número_de_itens == capacidade_actual) {Item* const novos_itens = new Item[capacidade_actual * 2];
capacidade_actual *= 2;
try {
for(int i = 0; i != número_de_itens; ++i)novos_itens[i] = itens[i];
} catch(...) {
delete[] novos_itens;
throw;
}
capacidade_actual *= 2;
delete[] itens;
itens = novos_itens;}
itens[número_de_itens] = novo_item;
++número_de_itens;
assert(cumpreInvariante());
}
Este método está, agora, "à prova de bala". Porquê? Porque nenhuma das restantes instruções pode lançar excepções: as operações sobre tipos básicos do C++ e sobre ponteiros nunca lançam excepções.
O objectivo do código produzido foi garantir que a instância da pilha não
sofre qualquer alteração no caso de a inserção de um novo item falhar. 
Trata-se de fornecer a operação PilhaDeInt::põe() da garantia
forte de segurança face a excepções.  Há três tipos de garantias
de segurança face a excepções que podem ser fornecidas:
Obviamente que se deve pugnar para obter sempre a garantia mais forte. Garantir o não-lançamento de excepções é em geral difícil. Mas há que tentar garanti-lo. Quando não se puder evitar o lançamento de excepções, a garantia básica só se deve usar se o esforço de fornecer a garantia forte for demasiado pesado (e.g., computacionalmente) para se justificar. A ausência de garantias é inaceitável.
Feitas estas observações, torna-se claro que o código produzido até agora precisa de ser revisto. Nomeadamente o construtor por cópia e a atribuição por cópia não fornecem nenhuma garantia de segurança face a excepções, o que é inaceitável!
O código completo da classe PilhaDeInt encontra-se no módulo 
físico pilha_int, constituído pelos ficheiros pilha_int.H, pilha_int_impl.H e pilha_int.C.