Resumo da Aula 10

Sumário

Tipos enumerados

Servem para definir novos tipos que consistem num conjunto finito e enumerado de diferentes valores.

1.1  Sintaxe

enum nome {valor1, valor2, ..., valorn};

Os identificadores valor1 a valorn ficam com os valores 0 a n - 1, excepto se algum deles for explicitamente inicializado, caso em que a numeração recomeça a partir do valor que lhe for atribuído.

1.2  Exemplo

Pode-se definir um tipo enumerado para representar os dias da semana:

enum DiaDaSemana {
    segunda_feira,
    terça_feira,
    quarta_feira,
    quinta_feira,
    sexta_feira,
    sábado,
    domingo
};

Este tipo pode ser utilizado como se segue:

DiaDaSemana dia;
dia = terça_feira;

1.3  Conversões

É possível converter de enumerado para int:

DiaDaSemana dia = quarta_feira;
int número_do_dia = int(dia);
cout << número_do_dia << endl; // surge 2 no ecrã.

e também de int para enumerado:

int número_do_dia = 5;
DiaDaSemana dia = DiaDaSemana(número_do_dia); // fica com sábado.

Sobrecarga de operadores

Pode-se sobrecarregar o significado dos operadores do C++ de modo a suportarem valores de tipos definidos pelo utilizador.

2.1  Sintaxe

Declaração:

tipo_devolvido operator operador(lista_de_parâmetros);

Definição:

tipo_devolvido operator operador(lista_de_parâmetros)
{
}

O número de argumentos (ou seja, de operandos) de um determinado operador é fixo.  Por exemplo, o operador / tem obrigatoriamente de ter dois argumentos.  No entanto, existem símbolos que correspondem a mais do que um operador.  Por exemplo, o símbolo - pode corresponder quer ao operador subtracção (operador binário, com dois operandos) quer ao operador simétrico (operador unário prefixo, com um único operando).

2.2  Exemplos

Definição:

enum DiaDaSemana {
    segunda_feira,
    ...
    domingo
};

int const número_de_dias_da_semana = 7;

DiaDaSemana operator+(DiaDaSemana const dia, int const salto)
{
    return DiaDaSemana((int(dia) + salto) % número_de_dias_da_semana);
}

DiaDaSemana operator+(int const salto, DiaDaSemana const dia)
{
    return dia + salto;
}

Utilização:

DiaDaSemana x = terça_feira;
int z = 2;

DiaDaSemana a = x + z;  // inicializada com o valor quinta_feira.
DiaDaSemana b = 3 + x;  // inicializada com o valor sexta_feira.

Definição do operador de incrementação prefixa:

DiaDaSemana& operator++(DiaDaSemana& dia)
{
    if(dia == domingo)
        dia = segunda_feira;
    else
        dia = DiaDaSemana(int(dia) + 1);

    return dia;
}

Utilização do operador:

DiaDaSemana x = terça_feira;

++x;  // x passa a ter o valor quarta_feira.

Classes

3.1  Definição de uma classe

Os tipos enumerados são muito limitados.  As classes são a forma de definir novos tipos por excelência.  As classes são também a unidade mais clara de modularização em C++, onde se tem maior liberdade de separar o que é implementação do que é interface simplesmente.

Uma classe é constituída por um conjunto de membros ou características.  Os membros podem ser atributos, operações, tipos, etc.  Os atributos correspondem a instâncias (variáveis ou constantes) membro que cada instância da classe possui.  Uma instância de uma classe é uma variável dessa classe.  As operações são rotinas membro, que podem ser invocados através de instâncias da classe.  Às instâncias de uma classe também se chama objectos e a invocação de uma operação através de um dado objecto é muitas vezes expressa dizendo que se passou uma mensagem ao objecto.  À implementação de uma operação chama-se o respectivo método.

3.1.1  Sintaxe

class Nome {
    // Caso não seja indicada a categoria de acesso esta é (por omissão) privada.
  public:
    // Declaração dos membros públicos da classe entre os quais normalmente
    // existem os construtores, se necessários.
    // Estes membros podem ser acedidos sem limitações.
    // Não devem existir quaisquer variáveis membro públicas!

 
private:

    // Declaração dos membros privados da classe.
    // Estes membros só podem ser acedidos a partir de outros membros da
    // própria classe (e em outros casos a ver mais tarde).
};

3.1.2  Exemplo

Definição de uma classe denominada Complexo para representar números complexos:

/** Representa números complexos.
   
@invariant V. */
class Complexo {

  public:
    /** Constrói um novo número complexo com partes real e imaginária dadas.
       
@pre V.
       
@post Complexo.parteReal() = parte_real
              Complexo
.parteImaginária() = parte_imaginária. */
    Complexo(double const parte_real, double const parte_imaginária);

    /** Devolve a parte real do complexo.
       
@pre V.
       
@post parteReal() = parte real do complexo. */
    double parteReal();

    /** Devolve a parte imaginária do complexo.
       
@pre V.
       
@post parteImaginária() = parte imaginária do complexo. */
    double parteImaginária();

    ... // outros membros públicos (tipicamente operações).

  private:
    double parte_real;
    double parte_imaginária;
};

double Complexo::parteReal()
{
    return parte_real;
}

double Complexo::parteImaginária()
{
    return parte_imaginária;
}

Esta classe pode-se usar como se segue:

Complexo c(2.1, 3.0); // construção de uma nova variável do tipo Complexo.

cout << "A parte real de c é: " << c.parteReal() << endl;
cout << "A parte imaginária de c é: " << c.parteImaginária() << endl;

// A instrução seguinte não é possível porque parte_real é um membro privado
// da classe Complexo.
cout << "A parte real de c é : " << c.parte_real << endl;

3.1.3  Nomenclatura e estilo

Normalmente os nomes das classes começam por maiúsculas e correspondem a substantivos.

Depois de se definir uma variável x de uma classe C a frase "o x (ou a variável x) é um C" deve estar gramaticalmente correcta.  Dadas as instruções acima, pode-se dizer perfeitamente "o c é um Complexo", pelo que o nome dado à classe é apropriado.  Normalmente isso significa que o nome de uma classe é uma substantivo ou frase substantiva*.

A relação que existe entre uma variável de uma classe e essa classe é a relação "é uma instância de".  É (presumivelmente) o mesmo tipo de relação que existe entre o leitor desta frase e a classe dos Humanos.

*  No entanto note-se que a palavra "complexo", na acepção usada é um substantivo, embora na sua origem seja um adjectivo.

3.2  Operações suportadas

As instâncias (variáveis ou constantes) de uma classe podem-se atribuir entre si, desde que se não tente fazer atribuições a constantes ou a variáveis de classes com atributos constantes (mesmo nesse caso é possível desde que a classe defina um operador de atribuição apropriado, como se verá)*.  O efeito de uma atribuição é o de atribuir uma a uma todas as variáveis membro das instâncias em causa (excepto se se tiver fornecido explicitamente um operador de atribuição para a classe, como se verá).  Assim,

Complexo c(2.1, 3.0), d(10.1, 11.2);

d = c;

cout << "A parte real de d é: " << d.parteReal() << endl;
cout << "A parte imaginária de d é : " << d.parteImaginária() << endl;

escreve no ecrã:

A parte real de d é: 2.1
A parte imaginária de d é: 3

As instâncias de uma classe definida pelo utilizador podem ser usadas exactamente da mesma forma que as instâncias dos tipos básicos do C++, podendo (em geral) ser passadas como argumentos de rotinas e devolvidas por funções.

* É possível proibir atribuições entre instâncias de uma classe desde que se declare o respectivo operador de atribuição como privado, como se verá.

3.3  Construtores

Um construtor é uma operação (procedimento) especial com o mesmo nome da classe e sem tipo de devolução.

Um construtor de uma classe é invocado sempre que uma instância dessa classe é construída e é utilizado para inicializar correctamente os atributos da classe.

Podem existir vários construtores sobrecarregados para cada classe, desde que tenham assinaturas diferentes (listas de parâmetros diferentes).

3.3.1  Exemplo

Declaração:

...

class Complexo {
  public:
    /** Constrói um novo número complexo com partes real e imaginária dadas.
       
@pre V.
       
@post Complexo.parteReal() = parte_real
              Complexo
.parteImaginária() = parte_imaginária. */
    Complexo(double const parte_real, double const parte_imaginária);

    ...

};

Definição:

Complexo::Complexo(double const r, double const i)
    : parte_real(r), parte_imaginária(i) // isto é uma lista de inicializadores.
{
}

ou, tirando partido da possibilidade de haver parâmetros e atributos com o mesmo nome,

Complexo::Complexo(double const parte_real, double const parte_imaginária)
    : parte_real(parte_real), parte_imaginária(parte_imaginária)
{
}

ou ainda,

Complexo::Complexo(double r, double i)
{
    // Aqui não se inicializa: atribui-se.  Mas o efeito prático é o mesmo neste caso.
    parte_real = r;
    parte_imaginária = i;
}

Sempre que possível prefira listas de inicializadores a atribuições.

3.4  Destrutores

Um destrutor é uma operação (procedimento) com o mesmo nome da classe precedido de um til (~) e não tem tipo de devolução nem parâmetros.

O destrutor é invocado automaticamente sempre que uma instância de uma classe é destruída e serve para executar todo o código necessário para que o sistema possa funcionar coerentemente após a destruição dessa instância: serve para "arrumar a casa".  A utilidade do destrutor será mais óbvia quando for introduzido o conceito de instância dinâmica, no início do segundo semestre.

Só pode existir um destrutor em cada classe.  Em muitos casos o destrutor pode ser omitido, pois a linguagem fornece um sempre que possível e que, na maioria dos casos de interesse neste momento, age de forma apropriada.

3.4.1  Exemplo

Declaração:

...

class Complexo {
  public:
    /** Destrói o número complexo.
       
@pre V. */
    ~ Complexo();

    ...

};

Definição:

Complexo::~Complexo() 
{

    // Este destrutor não precisa de fazer nada e poderia ser omitido.
}

3.5  Parâmetros com argumentos por omissão

No cabeçalho de qualquer rotina (inclusive na declaração das operações de uma classe) podem ser indicados argumentos por omissão para seus parâmetros.  Estes argumentos serão usados para inicializar os parâmetros sempre que os argumentos correspondentes forem omitidos numa invocação.  Normalmente os argumentos por omissão indicam-se apenas nas declarações e não nas definições (nas classes, isso significa que se indicam apenas nas operações, e não nos métodos correspondentes).

3.5.1  Exemplo

Declaração:

// Numa invocação desta função o primeiro argumento é obrigatório e os dois últimos
// são opcionais:
int somaDe(int const a, int const b = 1, int const c = 1);

Definição:

int somaDe(int const a, int const b, int const c)
{
    return a + b + c;
}

Utilização:

// Todas estas invocações da função somaDe() são válidas:
int i;
i = somaDe(1, 2, 3); // a = 1, b = 2, c = 3.
i = somaDe(1, 2);    // a = 1, b = 2, c = 1 (por omissão).
i = somaDe(1);       // a = 1, b = 1 (por omissão), c = 1 (por omissão).

3.6  Diferenças entre class e struct

Quando é utilizada a palavra-chave class para definir um novo tipo de dados, todos os membros para os quais não é indicada a categoria de acesso (público ou privado) são, por omissão, privados.

Quando é utilizada a palavra-chave struct para definir um novo tipo de dados, todos os membros para os quais não é indicada a categoria de acesso (público ou privado) são, por omissão, públicos.

Prefira a definição com a palavra chave class.  Reserve a palavra chave struct para os raros casos em que se pretende mesmo que todos os membros sejam públicos.  Nesses casos não se fala em classe, mas sim em agregado de informação heterogénea (agregados de informação homogénea são as matrizes).

Nota importante para quem já aprendeu C:  Existem algumas diferenças entre as possibilidades de utilização da palavra-chave struct em C e em C++, nomeadamente:

3.7  Encapsulamento: membros públicos e privados

O programador produtor de uma classe deve esconder do programador consumidor tudo o que puder ser escondido.  I.e., os pormenores de implementação devem ser escondidos, devendo-se fornecer interfaces limpas e simples para a manipulação das entidades produzidas.  A interface de uma classe é o conjunto dos seus membros públicos.

Exemplo

A utilidade de const no final dos cabeçalhos de algumas das operações e métodos da classe abaixo será vista em aulas posteriores.

#include <iostream>
#include <cmath>

using namespace std;

/** Representa números complexos.
   
@invariant V. */
class Complexo {

  public:
    /** Constrói um novo número complexo com partes real e imaginária dadas.
       
@pre V.
       
@post Complexo.parteReal() = parte_real
              Complexo
.parteImaginária() = parte_imaginária. */
    Complexo(double const parte_real = 0.0, 
             double const parte_imaginária = 0.0);

    /** Devolve a parte real do complexo.
       
@pre V.
       
@post parteReal() = parte real do complexo. */
    double parteReal() const;

    /** Devolve a parte imaginária do complexo.
       
@pre V.
       
@post parteImaginária() = parte imaginária do complexo. */
    double parteImaginária() const;

    /** Devolve o módulo do complexo.
       
@pre V.
       
@post módulo() = módulo do complexo. */
    double módulo() const;

    /** Devolve o ângulo do complexo.
       
@pre V.
       
@post ângulo() = ângulo do complexo. */
    double ângulo() const;

    /** Mostra o complexo no ecrã.
       
@pre V.
       
@post ecrã passou a conter representação textual do complexo. */
    void mostra() const;

  private:
    double parte_real;
    double parte_imaginária;
};

Complexo::Complexo(double const parte_real, double const parte_imaginária)
    : parte_real(parte_real), parte_imaginária( parte_imaginária)
{
}

double Complexo::parteReal() const
{
    return parte_real;
}

double Complexo::parteImaginaria() const
{
    return parte_imaginária;
}

double Complexo::módulo() const
{
    return sqrt(parteReal() * parteReal() + 
                parteImaginária() * parteImaginária());

}

double Complexo::ângulo() const
{
    return atan2(parteImaginária(), parteReal());
}

void Complexo::mostra() const
{
    if(0.0 < parteImaginária())
        cout << parteReal() << "+i×" << parteImaginária();
    else if(parteImaginária() == 0.0)
        cout << parteReal();
    else // parteImaginária() < 0.0
        cout << parteReal() << "-i×" << -parteImaginária();
}

Complexo const operator+(Complexo const& a, Complexo const& b)
{
    Complexo soma(a.parteReal() + b.parteReal(),
                  a.parteImaginaria() + b.parteImaginaria());
    return soma;
    // Ou simplesmente:
    //     return Complexo(a.parteReal() + b.parteReal(),
                           a.parteImaginaria() + b.parteImaginaria());
}

int main()
{
    Complexo x(1.1, 0.5), y(2.2, -4.5), z;
    Complexo w = x + y + z;

    z.escreve();
    cout << endl;
    // Aparece:
    //     3.3-i×4.0
}