Guião da 5ª aula teórica

Sumário

  1. Memória livre e variáveis dinâmicas:
    1. Criação de variáveis dinâmicas com o operador new: e se não houver memória?  Excepção bad_alloc ou operador especial nothrow.
    2. Destruição de variáveis dinâmicas com o operador delete.
    3. Princípio: quem constrói responsabiliza-se por destruir.
    4. Problemas comuns com variáveis dinâmicas.
    5. Construtores e destrutores: quando são invocados.
    6. Matrizes dinâmicas com os operadores new[] e delete[].
  2. Introdução à memória dinâmica em classes:
    1. Construtores e destrutores.
    2. Problema da cópia e da atribuição: construtor por cópia e atribuição por cópia automáticos.

Guião

O que acontece quando se escreve

int j = 20;

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

Há uma variável 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!

Hoje vamos ver como construir variáveis sem nome e com uma duração arbitrária, controlada directamente pelo programador.  Esta variáveis chamam-se dinâmicas, exactamente porque têm uma duração arbitrária.  

Se uma variável 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!

Escrever classe:

class Aluno {
  public:
    Aluno(string const& nome, int const número)
        : nome_(nome), número_(número) {
    }
    string const& nome() const {
        return nome_;
    }
    int número() const {
        return número_;
    }
  private:
    string nome_;
    int 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);

Como escrever o aluno no ecrã?

Discutir.  Concluir:

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

Como destruir uma variável dinâmica?

delete pa;

A partir desse momento o ponteiro pa contém LIXO!

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 variáveis dinâmicas têm de ser destruídas UMA e UMA SÓ VEZ!
  2. Quem constrói destrói.
Explicar muito bem o porquê desta última regra.  É um problema de responsabilidades.  Se não forem claras vão ocorrer erros!

Erros mais comuns:

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

Discutir.

É uma chamada fuga de memória: variáveis dinâmicas construídas e nunca destruídas.

int* p = new int(10);
p = new int(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 variável) 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)
        : nome_(nome), número_(número) {
    }
    ~Aluno() {
        cout << "Arghhh!  Destruiram-me!" << endl;
    }
    string nome() const {
        return nome_;
    }
    int número() const {
        return número_;
    }
  private:
    string nome_;
    int 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.

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;
    exit(1);
}

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

Começar por escrever classe do semestre passado!

class PilhaInt {
  public:
    typedef int Item;

    PilhaInt();


    bool estáVazia() const;

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

    void põe(Item const& item);

    void tira();
    Item& topo();

  private:

    static int const capacidade = 100;
    Item itens[número_máximo_de_itens];
    int número_de_itens;
};
Discutir solução antiga.  Propor nova.  Discuti-la passo a passo.

class PilhaInt {
  public:
    typedef int Item;

    PilhaInt();

    ~PilhaInt();

    bool vazia() const;

    bool cheia() const;
    int altura() const;
    Item const& topo() const;

    void põe(Item const& item);

    void tira();
    Item& topo();

  private:

    static int const capacidade_inicial = 32;
    int capacidade_actual;
    Item* itens;
    int número_de_itens;
};

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 PilhaInt::PilhaInt()
    : capacidade_actual(capacidade_inicial), 
      itens(new Item[capacidade_actual]), 
      número_de_itens(0) {

}

void PilhaInt::põe(Item const& item) {
    if(número_de_itens == capacidade_actual) {
        capacidade_actual *= 2;
        Item* 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++] = item;
}

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.  Conclusão: é necessário um destrutor!

PilhaInt::~PilhaInt() {
    delete[] itens;
}

Discutir resultado de:

PilhaInt p;
p.põe(2);
p.põe(3);
p.põe(4);
PilhaInt cópia = p;
cópia.tira();
PilhaInt 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!