Guião da 12ª Aula Teórica

Sumário

  1. Definição dos operadores para o TAD Racional.
  2. Regra: uma rotina só deve ser membro se precisar de o ser.
  3. Conversões implícitas: construtores invocáveis com um único argumento.  Evitando as conversões implícitas com explicit.

Na última aula fizemos alguns aperfeiçoamentos no TAD Racional: acrescentámos o operador ++ prefixo e transformámos a função membro somaCom() numa sobrecarga do operador +:

#include <iostream>
#include <cassert>

using namespace std;

/** Devolve o máximo divisor comum dos inteiros passados como argumento.
    @pre m <> 0 ou n <> 0.
    @post mdc = mdc(m, n). */
int mdc(int const m, int const n) 
{
   
...
}

/** Representa números racionais.  
   
@invariant 0 < denominador e mdc(numerador, denominador) = 1. */
class Racional {

  public:
    /** Constrói racional com valor inteiro.
       
@pre V.
       
@post *this = valor. */
    Racional(int const valor = 0);

    /** Constrói racional correspondente a n/d.
       
@pre d <> 0.
       
@post *this = n/d. */
    Racional(int const numerador, int const denominador);

    /** Escreve um racional no ecrã no formato de uma fracção.
        @pre V.
        @post ¬cout.good() ou cout contém n/d (ou simplesmente
              n, se d = 1) em que n e d são os valores de numerador e denominador. */
    void escreve();

    /** Devolve a soma com o racional recebido como argumento.
        @pre V.
        @post operator+ = *this + r2. */
    Racional operator+(Racional const& r2);

    /** Lê do teclado o racional, na forma de dois inteiros sucessivos.
        @pre *this = r.
        @post Se cin.good() e cin tem dois inteiros n' e d' disponíveis para
              leitura, com d' <> 0,
              então *this = n'/d',
              senão *this = r e ¬cin.good(). */
    void lê();

    /** Incrementa o racional.
       
@pre *this = r.
        @post operator++ idêntico a *this e *this = r + 1. */
    Racional& operator++();

  private:
    int numerador;
    int denominador;

    /** Reduz a fracção que representa o racional.
        @pre denominador <> 0 e *this = r.
        @post denominador <> 0 e mdc(numerador, denominador) = 1 e
            *this = r. */
    void reduz();

    /** Indica se a CIC se verifica.
       
@pre V.
       
@post cumpreInvariante = 0 < denominador e mdc(numerador, denominador) = 1. */
    bool cumpreInvariante();
};

Racional::Racional(int const valor)
    : numerador(valor), denominador(1)
{
   
...
}

Racional::Racional(int const numerador, int const denominador) 
{
   
...

void Racional::escreve()
{
    ...
}

Racional Racional::operator+(Racional const& r2)
{
    assert(cumpreInvariante() and r2.cumpreInvariante());

    Racional r;

    r.numerador = numerador * r2.denominador +
        r2.numerador * denominador;
    r.denominador = denominador * r2.denominador;

    r.reduz();

    assert(cumpreInvariante() and r.cumpreInvariante());

    return r;
}

void Racional::lê()
{
   
...
}

Racional& Racional::operator++()
{
    assert(cumpreInvariante());

    numerador += denominador;

    assert(cumpreInvariante());

    return *this;
}

void Racional::reduz()
{
   
...
}

bool Racional::cumpreInvariante()
{
   
...
}

int main()
{
    // Ler fracções:
    cout << "Introduza duas fracções (numerador denominador): ";
    Racional r1, r2;
    r1.lê();
    r2.lê();

    if(not cin) {
        cout << "Opps!  A leitura dos racionais falhou!" << endl;
        return 1;
    }

    // Calcular racional soma:
    Racional r = r1 + r2;

    // Escrever resultado:
    cout << "A soma de ";
    r1.escreve();
    cout << " com ";
    r2.escreve();
    cout << " é ";
    r.escreve();
    cout << '.' << endl;
}

Já agora podíamos definir também o operador -- prefixo:

...

class Racional {
  public:
    ...
    /** Decrementa o racional.
       
@pre *this = r.
        @post operator-- idêntico a *this e *this = r - 1. */
    Racional& operator--();

    ...
};

...

Racional& Racional::operator--()
{
    assert(cumpreInvariante());

    numerador -= denominador;

    assert(cumpreInvariante());

    return *this;
}

Quais os operadores interessantes para o TAD?  Na aula anterior vimos que eram:

++, -- sufixo
++, -- prefixo
*=
/=
+=
-=
+
, - unários
*, /
+, - binários
<, <=, >, >=
== !=

Destes, os primeiros candidatos a implementar são os que alteram um dos seus operandos.  Seja:

Racional a(1,2), b(3,5);

a + b; // altera a? e b?
a++;   // altera a?

Discutir e concluir que os que alteram são: ++, -- (sufixo e prefixo) e os op=.  Dizer que o operador + devia ter sido deixado para o fim.  Deixar claro que estes operadores são mal comportados.  São definidos à custas de "coisos", mistos de funções e procedimentos, ou funções com efeitos laterais.

Agora podemos passar ao operador *= que, como tem dois operandos, vai ter um parâmetro (o primeiro operando é a instância implícita!).  Estes operadores também devolvem uma referência para o primeiro operando, de modo a se poder escrever:

Racional a(3), b(1, 2);

(a *= b) *= b;

Discutir resultado do código!  Explicar que se está a tentar reproduzir a semântica que o operador *= tem para os tipos básicos do C++.  Não se está a defender que este tipo de código deva ser escrito!

Passando ao código:

...

class Racional {
  public:
    ...
    /** Multiplica por um racional.
       
@pre *this = r.
        @post operator*= idêntico a *this e *this = r × r2. */
    Racional& operator*=(Racional const& r2);
   
...
};

...

Racional& Racional::operator*=(Racional const& r2)
{
    assert(cumpreInvariante() and r2.cumpreInvariante());

    Discutir código!

    numerador *= r2.numerador;
    denominador *= r2.denominador;

    reduz();

    assert(cumpreInvariante());

    return *this;
}

...

De igual forma para /= (com cuidado para não dividir por zero e com numerador do divisor negativo!), para += e para -=:

...

class Racional {
  public:
    ...
    /** Multiplica por um racional.
       
@pre *this = r.
        @post operator*= idêntico a *this e *this = r × r2. */
    Racional& operator*=(Racional const& r2);

    /** Divide por um racional.
       
@pre *this = r e r2 <> 0.
        @post operator/= idêntico a *this e *this = r / r2. */
    Racional& operator/=(Racional const& r2);

    /** Adiciona de um racional.
       
@pre *this = r.
        @post operator+= idêntico a *this e *this = r + r2. */
    Racional& operator +=(Racional const& r2);

    /** Subtrai de um racional.
       
@pre *this = r.
        @post operator-= idêntico a *this e *this = r - r2. */
    Racional& operator-=(Racional const& r2);

    ...
};

...

Racional& Racional::operator*=(Racional const& r2)
{
    assert(cumpreInvariante() and r2.cumpreInvariante());

    numerador *= r2.numerador;
    denominador *= r2.denominador;

    reduz();

    assert(cumpreInvariante());

    return *this;
}

Racional& Racional::operator/=(Racional const& r2)
{
    assert(cumpreInvariante() and r2.cumpreInvariante());

    assert(r2 != 0);

    Discutir asserção!  Só depois se fará o operador !=!

    Discutir código!

    numerador *= r2.denominador;
    denominador *= r2.numerador;

    reduz();

    assert(cumpreInvariante());

    return *this;
}

Racional& Racional::operator +=(Racional const& r2)
{
    assert(cumpreInvariante() and r2.cumpreInvariante());

    numerador = numerador * r2.denominador +
        r2.numerador * denominador;

    denominador *= r2.denominador;

    Pode-se trocar a ordem destas atribuições?

    reduz();

    assert(cumpreInvariante());

    return *this;
}

Racional& Racional::operator-=(Racional const& r2)
{
    assert(cumpreInvariante() and r2.cumpreInvariante());

    numerador = numerador * r2.denominador -
        r2.numerador * denominador;

    denominador *= r2.denominador;

    reduz();

    assert(cumpreInvariante());

    return *this;
}

...

Agora podemos passar a *, /, + e -.  

Podemos escrevê-los à custa de *=, /=, += e -=!  Como?  

Discutir para *.  Concluir:

/** Produto de dois racionais.
   
@pre V.
    @post operator* = r1 × r2. */
Racional const operator*(Racional r1, Racional const& r2)

{
    r1 *= r2;

    return r2;
}

Discutir que r1 é cópia.  Pode-se alterar localmente!

Explicar vantagem de devolver constante (não se pode escrever ++(r1 * r2)...!

Pense-se no seguinte código:

Racional r1(1, 2), r2(3, 2);

++(r1 + r2);

Faz algum sentido?  O que acontece?  É coisa que seja admissível se em vez de racionais tivéssemos inteiros?

Discutir.

A solução para este problema é simples: basta alterar as rotinas (membro ou não) que devolvem um racional por valor para devolverem um racional constante por valor.  Isso levaria imediatamente a um erro de compilação, pois não se pode incrementar um racional constante!

E da mesma forma para os outros operadores!  Estes operadores binários sem efeitos laterais não precisam de ser membros!

Discutir vantagens de implementar uns métodos em termos de outros, e a vantagem de ser + implementado à custa de +=, e não o contrário.

Fazer /, + e -!

/** Divisão de dois racionais.
   
@pre r2 <> 0.
    @post operator/ = r1 / r2. */
Racional const operator/(Racional r1, Racional const& r2)

{
    assert(r2 != 0);

    r1 /= r2;

    return r1;
}

/** Soma de dois racionais.
       
@pre V.
        @post operator+ = r1 + r2. */
Racional const operator+(Racional r1, Racional const& r2)

{
    r1 += r2;

    return r1;
}

/** Subtracção de dois racionais.
       
@pre V.
        @post operator- = r1 - r2. */
Racional const operator-(Racional r1, Racional r2)

{
    r1 -= r2;

    return r1;
}

Regra aproximativa:  Se uma rotina não precisa de ser membro, não deve ser membro.

Que vantagem é que tem o operador +, por exemplo, não ser membro?  É que, se for membro, o seguinte código é válido:

Racional r(1, 3);
Racional s = r + 3;

pois o C++ converte o 3 implicitamente para Racional (usando a primeira versão do construtor), e usa o operador + definido.

Mas o código seguinte é inválido:

Racional r(1, 3);
Racional s = 3 + r;

pois o C++ nunca converte a instância através da qual se invoca um método.

Se a rotina (neste caso um operador) for normal, todos os seus argumentos pode sofrer conversões implícitas, o que resolve o problema.

Repare-se que a existência de um construtor que pode ser invocado com apenas um argumento estabelece uma conversão implícita, neste caso de int para Racional.  Se não for o pretendido pode-se usar a palavra chave explicit:

...

class Racional {
  public:
    ...
    explicit Racional(int const valor = 0);
    ...
};

...

Racional r(1, 3);
Racional s = 4;
Racional t(4);
Racional u = r + 3;
Racional v = r + Racional(3);

Note-se que Racional(3) é muitas vezes interpretado como se fosse um valor literal para racionais!

Podemos agora passar aos operadores == e !=.  Precisam de ser membros?  Qual o seu código?

Discutir: têm de ser membros porque se tem de conhecer numerador e denominador.  Mas não precisam se existirem funções membro para devolver os o numerador e o denominador da fracção representativa do racional!

A estas operações de TAD que se limitam a devolver propriedades das variáveis chama-se inspectores.  É como num peep show: permitem olhar para o que é privado mas não se lhe pode tocar...

Discutir também questão dos nomes: dizer que é muito importante manter a interface, pelo que o melhor é mudar o nome dos atributos.  A forma usual e recomendada é acrescentar-lhes o sublinhado.

...

class Racional {
  public:
    ...
    /** Devolve numerador da fracção mínima correspondente ao racional.
       
@pre V.
       
@post numerador/denominador() = *this. */
    int numerador();

    /** Devolve denominador da fracção mínima correspondente ao racional.
       
@pre V.
       
@post (E n : V : n/denominador = *this e 0 < denominador e
              mdc(n, denominador) = 1). */
    int denominador();
   
...
  private:
    int numerador_;
    int denominador_;
   
...
};

...

int Racional::numerador()
{
    assert(cumpreInvariante());

    assert(cumpreInvariante());

    return numerador_;
}

int Racional:: denominador()
{
    assert(cumpreInvariante());

    assert(cumpreInvariante());

    return denominador_;
}

...

/** Devolve verdadeiro se dois racionais forem iguais.
   
@pre V.
   
@post operator== = (r1 = r2). */
bool operator==(Racional const& r1, Racional const& r2)

{
    return r1.numerador() == r2.numerador() and
           r1.denominador() == r2.denominador();
}

Funciona?  De certeza?  Porquê?  Porque 0 < denominador_ e mdc(numerador_, denominador_) = 1sempre?

De certeza?  Vejam /=!  Que se passa?

Chamar a atenção para que as instruções de asserção para verificar o cumprimento da CIC levariam naturalmente à detecção deste erro.

É preciso corrigir:

Racional& Racional::operator/=(Racional const& r)
{
    assert(cumpreInvariante() and r2.cumpreInvariante());

    assert(r2 != 0);

    if(r2.numerador_ < 0) {
        numerador_ *= -r2.denominador_;

        denominador_ *= -r2.numerador_;
    } else {
        numerador_ *= r2.denominador_;

        denominador_ *= r2.numerador_;
    }

    reduz();

    assert(cumpreInvariante());

    return *this;
}

E o operador !=?

/** Devolve verdadeiro se dois racionais forem iguais.
   
@pre V.
   
@post operator!= = (r1 <> r2). */
bool operator!=(Racional const& r1, Racional const& r2)
{
    return not (r1 == r2);
}

Discutir solução!

Versão final:

Explicar alterações em /=!  Devem-se à possibilidade de escrever r /= r!

#include <iostream>
#include <cassert>

using namespace std;

/** Devolve o máximo divisor comum dos inteiros passados como argumento.
    @pre m <> 0 ou n <> 0.
    @post mdc = mdc(m, n). */
int mdc(int const m, int const n) 
{
    assert(m != 0 or n != 0);

    ...
}

/** Representa números racionais.  
   
@invariant 0 < denominador_ e mdc(numerador_, denominador_) = 1. */
class Racional {

  public:
    /** Constrói racional com valor inteiro.
       
@pre V.
       
@post *this = valor. */
    Racional(int const valor = 0);

    /** Constrói racional correspondente a n/d.
       
@pre d <> 0.
       
@post *this = numerador/denominador. */
    Racional(int const numerador, int const denominador);

    /** Devolve numerador da fracção mínima correspondente ao racional.
       
@pre V.
       
@post numerador /denominador() = *this. */
    int numerador();

    /** Devolve denominador da fracção mínima correspondente ao racional.
       
@pre V.
        @post (E n : : n/denominador = *this e 0 < denominador e
              mdc(n, denominador) = 1). */
    int denominador();

    /** Escreve um racional no ecrã no formato de uma fracção.
        @pre V.
        @post ¬cout.good() ou cout contém n/d  (ou simplesmente
              n, se d = 1) em que n e d são os valores de numerador() e denominador(). */
    void escreve();

    /** Lê do teclado o racional, na forma de dois inteiros sucessivos.
        @pre *this = r.
        @post Se cin.good() e cin tem dois inteiros n' e d' disponíveis para
              leitura, com d' <> 0,
              então *this = n'/d',
              senão *this = r e ¬cin.good(). */
    void lê();

    /** Incrementa o racional.
       
@pre *this = r.
        @post operator++ idêntico a *this e *this = r + 1. */
    Racional& operator++();

    /** Decrementa o racional.
       
@pre *this = r.
        @post operator-- idêntico a *this e *this = r - 1. */
    Racional& operator--();

    /** Multiplica por um racional.
       
@pre *this = r.
        @post operator*= idêntico a *this e *this = r × r2. */
    Racional& operator*=(Racional const& r2);

    /** Divide por um racional.
       
@pre *this = r e r2 <> 0.
        @post operator/= idêntico a *this e *this = r / r2. */
    Racional& operator/=(Racional const& r2);

    /** Adiciona de um racional.
       
@pre *this = r.
        @post operator+= idêntico a *this e *this = r + r2. */
    Racional& operator +=(Racional const& r2);

    /** Subtrai de um racional.
       
@pre *this = r.
        @post operator-= idêntico a *this e *this = r - r2. */
    Racional& operator-=(Racional const& r2);

  private:
    int numerador_;
    int denominador_;

    /** Reduz a fracção que representa o racional.
        @pre denominador_ <> 0 e *this = r.
        @post denominador_ <> 0 e mdc(numerador_, denominador_) = 1 e
            *this = r. */
    void reduz();

    /** Indica se a CIC se verifica.
       
@pre V.
       
@post cumpreInvariante = 0 < denominador_ e mdc(numerador_, denominador_) = 1. */
    bool cumpreInvariante();
};

Racional::Racional(int const valor)
    : numerador_(valor), denominador_(1)
{

    assert(cumpreInvariante());
    assert(numerador() == valor * denominador());
}

Racional::Racional(int const numerador, int const denominador
{
    assert(d
enominador != 0);

    if(denominador < 0) {
        numerador_ = -numerador;
        denominador_ = -denominador;
    } else {
        numerador_ = numerador;
        denominador_ = denominador;
    }

    reduz();

    assert(cumpreInvariante());
    assert(numerador_() * denominador == numerador * denominador());
}

int Racional::numerador()
{
    assert(cumpreInvariante());

    assert(cumpreInvariante());

    return numerador_;
}

int Racional::denominador()
{
    assert(cumpreInvariante());

    assert(cumpreInvariante());

    return denominador_;
}

void Racional::escreve()
{
    assert(cumpreInvariante());

    cout << numerador_;
    if(denominador() != 1)
        cout << '/' << denominador();

    assert(cumpreInvariante());
}

void Racional::lê()
{
    assert(cumpreInvariante());

    int n, d;

    cin >> n >> d;
    if(cin.good())
        if(d == 0)
            cin.setstate(ios_base::failbit);
        else {
            if(d < 0) {
                numerador_ = -n;
                denominador_ = -d;
            } else {
                numerador_ = n;
                denominador_ = d;
            }

            reduz();

            assert(cumpreInvariante());
            assert(numerador_() * d == n * denominador() and cin.good());

            return;
        }

    assert(cumpreInvariante());

    assert(not cin.good());
}

Racional& Racional::operator++()
{
    assert(cumpreInvariante());

    numerador_ += denominador_;

    assert(cumpreInvariante());

    return *this;
}

Racional& Racional::operator--()
{
    assert(cumpreInvariante());

    numerador_ -= denominador_;

    assert(cumpreInvariante());

    return *this;
}

Racional& Racional::operator*=(Racional const& r2)
{
    assert(cumpreInvariante() and r2.cumpreInvariante());

    numerador_ *= r2.numerador_;
    denominador_ *= r2.denominador_;

    reduz();

    assert(cumpreInvariante());

    return *this;
}

Racional& Racional::operator/=(Racional const& r2)
{
    assert(cumpreInvariante() and r2.cumpreInvariante());

    assert(r2 != 0);

    // Necessário para evitar problemas com r /= r!
    int numerador = r2.numerador_;

    if(r2.numerador_ < 0) {
        numerador_ *= -r2.denominador_;

        denominador_ *= -r2.numerador_numerador;
    } else {
        numerador_ *= r2.denominador_;

        denominador_ *= r2.numerador_numerador;
    }

    reduz();

    assert(cumpreInvariante());

    return *this;
}

Racional& Racional::operator+=(Racional const& r2)
{
    assert(cumpreInvariante() and r2.cumpreInvariante());

    numerador_ = numerador_ * r2.denominador_ +
        r2.numerador_ * denominador_;

    denominador_ *= r2.denominador_;

    reduz();

    assert(cumpreInvariante());

    return *this;
}

Racional& Racional::operator-=(Racional const& r2)
{
    assert(cumpreInvariante() and r2.cumpreInvariante());

    numerador_ = numerador_ * r2.denominador_ -
        r2.numerador_ * denominador_;

    denominador_ *= r2.denominador_;

    reduz();

    assert(cumpreInvariante());

    return *this;
}

void Racional::reduz()
{
    assert(denominador_ != 0);

    int divisor = mdc(numerador_, denominador_);

    numerador_ /= divisor;
    denominador_ /= divisor;

    assert(denominador_ != 0 and mdc(numerador_, denominador_) == 1);
}

bool Racional::cumpreInvariante()
{
    return 0 < denominador_ and mdc(numerador_, denominador_) == 1;
}

/** Produto de dois racionais.
   
@pre V.
    @post operator* = r1 × r2. */
Racional const operator*(Racional r1, Racional const& r2)

{
    r1 *= r2;

    return r1;
}

/** Divisão de dois racionais.
   
@pre r2 <> 0.
    @post operator/ = r1 / r2. */
Racional const operator/(Racional r1, Racional const& r2)

{
    assert(r2 != 0);

    r1 /= r2;

    return r1;
}

/** Soma de dois racionais.
   
@pre V.
    @post operator+ = r1 + r2. */
Racional const operator+(Racional r1, Racional const& r2)

{
    return r1 += r2;
}

/** Subtracção de dois racionais.
   
@pre V.
    @post operator- = r1 - r2. */
Racional const operator-(Racional r1, Racional const& r2)

{
    r1 -= r2;

    return r1;
}

/** Devolve verdadeiro se dois racionais forem iguais.
   
@pre V.
   
@post operator== = (r1 = r2). */
bool operator==(Racional const& r1, Racional const& r2)

{
    return r1.numerador() == r2.numerador() and
           r1.denominador() == r2.denominador();
}

/** Devolve verdadeiro se dois racionais forem iguais.
   
@pre V.
   
@post operator != = (r1 <> r2). */
bool operator!=(Racional const& r1, Racional const& r2)

{
    return not (r1 == r2);
}

int main()
{
    // Ler fracções:
    cout << "Introduza duas fracções (numerador denominador): ";
    Racional r1, r2;
    r1.lê();
    r2.lê();

    if(not cin) {
        cout << "Opps!  A leitura dos racionais falhou!" << endl;
        return 1;
    }

    // Calcular racional soma:
    Racional r = r1 + r2;

    // Escrever resultado:
    cout << "A soma de ";
    r1.escreve();
    cout << " com ";
    r2.escreve();
    cout << " é ";
    r.escreve();
    cout << '.' << endl;
}

Curiosamente este código tem um erro: não se pode invocar os métodos numerador() e denominador() através de instâncias constantes, como é o caso nos operadores == e !=!  A ver na próxima aula...

Chamar a atenção para o trabalho todo que estamos a ter...  

Mais trabalho do produtor, menos do consumidor.  Menos trabalho do produtor, mais trabalho do consumidor.

Ficaram a faltar...

++, -- sufixo
+, - unários
<, <=, >, >=

e ainda

<< e >> para inserção e extracção em e de canais

A ver mais tarde...