Guião da 5ª Aula Teórica

Sumário

Guião

O que acontece quando se escreve

int j = 20;

int f()
{
    int const i = 10;
    ...
}

Há uma constante do tipo int que tem um nome (i) e que dura enquanto a função estiver a ser executada...  É automática!  E há uma variável do tipo int que tem nome (j) e que dura enquanto o programa durar.  É estática!  Ambas são instâncias declaradas pelo programador e têm a sua permanência controlada pelas regras da linguagem.

Em C++ há também instâncias (variáveis ou constantes) não declaradas, que não têm nome.  Por exemplo, as instâncias temporárias.  Recordam-se da classe Racional, por nós definida?  Lembram-se que tínhamos sobrecarregado o operador adição +?

Racional operator+(Racional um_racional, Racional const& outro_racional)
{
    um_racional += outro_racional;

    return um_racional;
}

Que sucede quando se escreve:

int main()
{
    Racional r1(1, 3);
    Racional r2(2, 3);

    cout << r1 + r2 << endl;
}

É óbvio que surge no ecrã o valor 1, mas como?

Discutir e dizer que o valor devolvido pela função acima é uma variável temporária!  Existe enquanto a instrução estiver a ser executada!

Melhor seria se fosse uma constante temporária...

Racional const operator+(Racional um_racional, 
                         Racional const& outro_racional)
{
    um_racional += outro_racional;

    return um_racional;
}

Tal como com as instâncias declaradas, também as instâncias não-declaradas temporárias têm uma permanência que é dada pelas regras da linguagem.

Hoje vamos ver como construir instâncias também não-declaradas mas com uma duração arbitrária, controlada directamente pelo programador.  Esta instâncias chamam-se dinâmicas, exactamente porque têm uma duração arbitrária.  

Resumindo:

Instâncias

Características: 

  • tipo e

  • valor

Variáveis Constantes
Valor alterável Valor fixo

 

Instâncias
Declaradas Não-declaradas

Características:

  • nome,

  • tipo e

  • valor

Características:

  • sem nome,

  • tipo e

  • valor

Automáticas Estáticas Temporárias Dinâmicas
Construídas quando a sua instrução de definição é atingida e destruídas no final do respectivo bloco. Construídas no início do programa (globais) ou quando a instrução das sua definição é atingida pela primeira vez (locais) e destruídas no final do programa Construídas durante o cálculo de uma expressão (e.g., para guardar valores devolvidos ou intermédios) e destruídas no final da expressão completa em que são criados*.

* Há excepções.

Construídas e destruídas sob o domínio integral do programador.

Se uma instância dinâmica não tem nome, como nos podemos referir a ela?

Discutir.  Referir aula anterior.  Concluir que se manipulam através de ponteiros.

Escrever:

int* p = new int(10);

O que se passa?  Em p fica o endereço da variável dinâmica criada, que é do tipo int e tem valor 10.

Explicar bem sintaxe.  Falar dos argumentos do construtor.  Fazer diagrama de objectos UML.  Não colocar nome na variável dinâmica!

Discutir também:

int const* pc = new int const(10);

Dizer que se construiu constante dinâmica.

Escrever classe:

class Aluno {
  public:
    Aluno(string const& nome, int const número);

    string const& nome() const;
    int número() const;

  private:
    string nome_;
    int número_;
};

Aluno::Aluno(string const& nome, int const número)
    : nome_(nome), número_(número) 
{

}

string const& Aluno:: nome() const 
{

    return nome_;
}

int Aluno::número() const 
{

    return número_;
}

Como criar uma variável dinâmica do tipo Aluno com nome "Zé" e número 100?

Discutir.  Concluir:

Aluno* pa = new Aluno("Zé", 100);

E uma constante?

Discutir.  Concluir:

Aluno const* pac = new Aluno const("Zé", 100);

Desenhar no ecrã em UML!  Usar {frozen} para a constante.

Como escrever o aluno no ecrã?

Discutir.  Concluir:

cout << pa->nome() << ' ' << pa->número() << endl;

Como destruir uma instância dinâmica?

delete pa;
delete pac;

A partir desse momento o ponteiro pa e pac contêm lixo!

Explicar que o conteúdo de pa e pac não muda.  Só a sua interpretação!

Repare-se:

void f(int* p) 
{

    cout << *p << endl;
    delete p;
}

int main()
{
    int* pi = new int(10);
    f(pi);
}

Qual a duração da variável dinâmica?

Este código é exemplificativo!  Não estou a defender que deve ser assim sempre!  Aliás, não deve ser:

Regras:

  1. Todas as instâncias dinâmicas têm de ser destruídas uma e uma só vez!
  2. Quem constrói destrói é uma política eficaz, embora não seja de todo a única.
Explicar muito bem o porquê desta última regra.  É um problema de responsabilidades.  Se não forem claras vão ocorrer erros!

Falar em alternativas:

  1. Quem constrói destrói ou, posse única da instância dinâmica.
  2. Quem possui destrói, ou posse única mas transferível da instância dinâmica.
  3. O último fecha a porte, ou posse partilhada da instância dinâmica.

Erros mais comuns:

for(int i = 0; i != 100000000; ++i) {
    int* p = new int(i);
}

Discutir.

É uma chamada fuga de memória: instâncias dinâmicas construídas e nunca destruídas.

int const* p = new int const(10);
p = new int const(20);

Fazer diagrama UML.  Mostrar que também é fuga de memória.

double* p = new double(1.1);
delete p;
delete p;

Discutir com base em diagrama UML!

Que acontece durante um new?

Discutir: reserva de memória (espaço a ocupar pela instância) e chamada do construtor (inicialização desse espaço).  Deixar muito clara a importância dos construtores.

Qual é então o problema do seguinte código?

Aluno* pa = new Aluno;

O único construtor de Aluno tem dois parâmetros sem valores por omissão!

Discutir solução.

E que acontece quando se destrói?

Explicar destrutor.  Acrescentar destrutor a Aluno.  Explicar que Aluno não precisa de destrutor!  É só para exemplificar!

class Aluno {
  public:
    Aluno(string const& nome, int const número);
    ~Aluno();

    string const& nome() const;
    int número() const;

  private:
    string nome_;
    int número_;
};

Aluno::Aluno(string const& nome, int const número)
    : nome_(nome), número_(número) 
{

}

Aluno::~Aluno()
{

    cout << "Arghhh!  Destruiram-me!" << endl;
}

string const& Aluno::nome() const 
{

    return nome_;
}

int Aluno::número() const 
{

    return número_;
}

Explicar sintaxe.  Explicar resultado de:

Aluno* pa = new Aluno;
delete pa;

Mas também se podem criar matrizes dinâmicas!  Para isso usam-se os operadores new[] e delete[]:

int* p = new int[10];

for(int i = 0; i != 10; ++i)
    p[i] = i;

for(int i = 0; i != 10; ++i)
    cout << p[i] << endl;

delete[] p;

Uma matriz dinâmica tem de ser destruída com delete[]!

Como são construídos os elementos da matriz?  Com o construtor por omissão.  Excepto se forem de tipos básicos, que não são inicializados...

Que acontece quando não há memória?  O operador lança uma excepção, que é um conceito que discutiremos numa próxima aula.  O resultado prático é que o programa aborta.  Mas depois veremos como se pode capturar a excepção.  Alternativamente pode-se usar:

#include <new>
...
int* p = new(nothrow) int(20);

Neste caso se não houver memória o ponteiro fica com o valor singular 0.

Dizer para evitar este tipo de utilização!

Explicar valor singular: nenhum objecto pode ter endereço 0!

#include <new>
...
int* p = new(nothrow) int(20);

if(p == 0) {
    cerr << "Não havia memória!  Que fazer?" << endl;
   
...
}

Deixar claro que é melhor a solução com excepções!

Começar por escrever a interface da classe PilhaDeInt do semestre passado:

/** Representa pilhas de double.
   
@invariant Dizer que não se conhece por ser questão de implementação. */
class PilhaDeInt {
  public:
    typedef double Item;

    /** Constrói pilha vazia.
       
@pre V.
       
@post estaVazia(). */
    PilhaDeInt();

    /** Devolve o item que está no topo da pilha.
       
@pre ¬estaVazia().
       
@post topo idêntico ao item no topo de *this. */
    Item const& topo() const;

    /** Indica se a pilha está vazia.
       
@pre V.
       
@post estaVazia = *this está vazia. */
    bool estáVazia() const;

    /** Devolve altura da pilha.
       
@pre V.
       
@post altura = altura de *this. */
    int altura() const;

    /** Põe um novo item na pilha (no topo).
       
@pre V.
       
@post *this contém um item adicional no topo igual a novo_item. */
    void põe(Item const& novo_item);

    /** Tira o item que está no topo da pilha.
       
@pre ¬estaVazia().
       
@post *this contém os itens originais menos o do topo. */
    void tiraItem();
 
  private:
   
...

    bool cumpreInvariante() const;
};

Discutir possíveis soluções.  Propor matrizes dinâmicas.  Discutir solução passo a passo.

/** Representa pilhas de double.
   
@invariant itens aponta matriz com capacidade_actual itens e
              capacidade_inicial <= capacidade_actual e
               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;

    bool cumpreInvariante() const;
};

...

bool PilhaDeInt::cumpreInvariante() const
{
    return capacidade_inicial <= capacidade_actual and
           0 <= número_de_itens <= capacidade_actual;
}

Fazer construtor e põe().  Discutir passo a passo!  Explicar método de crescimento!  Se o crescimento tivesse de ser feito a cada inserção a implementação era muito ineficiente!  Assim o impacte do redimensionamento é negligenciável (o impacte amortizado é constante, de cerca de duas cópias por inserção, de outra forma seria de n/2 para a n-ésima inserção).

inline PilhaDeInt::PilhaDeInt()
    : capacidade_actual(capacidade_inicial), 
      itens(new Item[capacidade_actual]), 
      número_de_itens(0)
{
    assert(cumpreInvariante());

}

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

    assert(cumpreInvariante());

    if(número_de_itens == capacidade_actual) {
        Item* novos_itens = new Item[capacidade_actual * 2];
        for(int i = 0; i != número_de_itens; ++i)
            novos_itens[i] = itens[i];
        capacidade_actual *= 2;
        delete[] itens;
        itens = novos_itens;
    }
    itens[número_de_itens] = novo_item;
    ++número_de_itens;

    assert(cumpreInvariante());
}

Justificar bem destrutor!  Fuga de memória!

Quem destrói a matriz dinâmica dos itens quando uma pilha é destruída?  Quem a construiu foi a pilha, quem a destrói deve ser a pilha (primeira política).  Conclusão: é necessário um destrutor!

/** Representa pilhas de double.
   
@invariant Dizer que não se conhece por ser questão de implementação. */
class PilhaDeInt {
  public:
    typedef int Item;

    /** Constrói pilha vazia.
       
@pre V.
       
@post estaVazia(). */
    PilhaDeInt();

    /** Destrói a pilha.
       
@pre V.
       
@post recursos externos reservados foram libertados. */
    ~PilhaDeInt();

    ...

};

...

inline PilhaDeInt::~PilhaDeInt()
{

    assert(cumpreInvariante());

    delete[] itens;
}

Discutir resultado de:

PilhaDeInt p;

p.põe(2);
p.põe(3);
p.põe(4);

PilhaDeInt cópia = p;

cópia.tira();

PilhaDeInt outra;

outra = p;

Explicar problema.  Mencionar construtor por cópia: definido pelo compilador e que se limita a copiar os atributos!  Deixar solução para a próxima aula!