Aula 13


6  Classes (actualização)

6.2  Construção de classes (actualização)

6.2.5  Usando sobrecarga de operadores (continuação)

Note-se que, tal como acontecia com a função membro soma(), a expressão x + y invoca a função membro operator + () da classe Racional usando x como instância (variável) implícita!  Isto é sempre verdade para sobrecarga de operadores usando funções membro: o primeiro operando (que pode ser o único no caso de operadores unários, i.e., só com um operando) é sempre a instância implícita do método (função membro).

Em geral, se @ for um operador binário (e.g., +, -, *, etc.), então a sobrecarga do operador @ para uma classe C pode ser feita definindo uma função membro tipo C::operator @ (tipo_do_2º_operando) ou através de uma função normal tipo operator @ (tipo_do_1º_operando, tipo_do_2º_operando).  A expressão a @ b pode portanto ser interpretada como

a.operator @ (b)
ou
operator @ (a, b)
Se @ for um operador unário (e.g., +, -, ++ prefixo, ++ sufixo, indexação [], etc.), então a sobrecarga do operador @ para uma classe C pode ser feita definindo uma função membro tipo C::operator @ () ou através de uma função normal tipo operator @ (tipo_do_operando).  A expressão a @ (ou @ a se @ for prefixo) pode portanto ser interpretada como
a.operator @ ()
ou
operator @ (a)
É importante notar que:
  1. Quando a sobrecarga se faz por intermédio duma função membro, o primeiro operando (e único no caso duma operação binária) numa expressão que envolva essa operador não sofre nunca conversões de tipo.  Em todos os outros casos as conversões são possíveis.
  2. Nunca se deve alterar a semântica dos operadores.  Se se tivesse sobrecarregado o operador + para a classe Racional como significando a multiplicação, imaginam-se os problemas que isso traria!
  3. Nem todos os operadores podem ser sobrecarregados por intermédio de funções normais.  Os operadores = (atribuição), [] (indexação), () (invocação) e -> (selecção), só podem ser sobrecarregados por meio de funções membro.
  4. Para todas as classes que não os redifinam, os operadores = (atribuição), & (unário, endereço de) e , (sequenciamento) são definidos implicitamente (por isso é possível atribuir instâncias de classes, como os Racional, sem para isso ter de sobrecarragar o operador =).

6.3  Desenho de classes (actualização)

6.3.4  Condição invariante de instância

Para a maior parte das classes, as suas instâncias só estão bem definidas se verificarem um conjunto de restrições.  Quando esse conjunto de restrições se exprime na forma duma condição, diz-se que a classe tem uma CII, ou seja, um condição invariante de instância.  A classe dos racionais possui uma CII, que, como se verá abaixo, passa por exigir que as variáveis membro n e d sejam o numerador e o denominador da fracção canónica representativa do racional correspondente, i.e., CII: d > 0 e mdc(n, d) = 1.

A vantagem da definição duma CII para a classe tem a ver com o facto de todas as funções e procedimentos membro públicos bem como as funções e procedimentos amigas (parte da interface da classe com o utilizador) poderem admitir que as instâncias da classe com que trabalham verificam inicialmente a CII, o que normalmente as simplifica bastante.  I.e., a CII para cada instância em causa é parte da PC de cada uma dessas funções e procedimentos.  Claro que, para serem "bem comportadas", essas funções e procedimentos devem também garantir que a CII se verifica para todas as instâncias da classe criadas ou alteradas pela função.  Ou seja, a CO dessas funções e procedimentos inclui também a CII para cada instância da classe criada ou alterada.

Assim, como todas funções e procedimentos que podem criar e alterar instâncias da classe em causa garantem que todas instâncias alteradas ou criadas verificam a CII, esta condição verifica-se sempre, excepto, possivelmente, durante a execução dessas mesmas funções e procedimentos.

Tal como sucedia nos ciclos, em que durante a execução do passo a CI muitas vezes não se verificava, embora se verificasse garantidamente antes e após o passo, também a CII pode não se verificar durante a execução das funções e procedimentos membro (e públicos) ou amigos da classe em causa, embora se verifique garantidamente no seu início e no seu final.  Acontece que, durante esses períodos em que a CII não se verifica, pode ser conveniente invocar alguma função ou procedimento membro auxiliar, que portanto terá de lidar com instâncias que não verifiquem inicialmente a CII e que poderão não garantir que a verificam as instâncias por si criada ou alteradas.  Essas funções e procedimentos "mal comportados" devem ser privados, de modo a evitar utilizações erróneas por parte do utilizador final da classe que coloquem alguma instância num estado inválido (em que a CII não se verifica).

Note-se que a definição de uma CII e de funções e procedimentos membro e funções e procedimentos amigos para uma classe não passam de um esforço inútil se as variáveis membro envolvidas na CII forem públicas.  É que, se o forem, o utilizador da classe pode alterá-las por engano ou maliciosamente, invalidando a CII, com consequências potencialmente dramáticas no comportamento da classe e no programa no seu todo.  Essas consequências são normalmente graves porque as funções e procedimentos que lidam com as variáveis membro da classe assumem que estas verificam a CII, não fazendo quaisquer garantias acerca do seu funcionamento quando a CII não se verifica.

De todas as funções e procedimentos membro de uma classe com CII, porventura as mais importantes sejam as funções construtoras.  É que estas são as que garantem que as instâncias são criadas verificando imediatamente a CII.  A sua importância pode ser vista na classe Racional, em que o construtor garante, se tal for possível, que a CII da classe se verifica, abortando o programa caso tal seja impossível.

Finalmente, é de notar que algumas classes não têm CII.  Essas classes são normalmente meros repositórios de informação.  É o caso, por exemplo, duma classe que guarde nome e morada de utentes de um serviço qualquer.  Essas classes têm normalmente todas as suas variáveis membro públicas, e portanto usam normalmente a palavra chave struct em vez de class no C++.

O caso da classe Racional

A CII, da classe Racional exige simplesmente que as variáveis membro n e d sejam o numerador e o denominador da fracção canónica representativa do racional correspondente, i.e., CII: d > 0 e mdc(n, d) = 1.  Disse-se que as funções e procedimentos membro (e públicos) ou amigos duma classe devem assumir que a CII se verifica para as instâncias com as quais trabalham, e que devem garantir que a CII se verifica para as instâncias que criam ou alteram.  É o caso da função membro operator + () da classe Racional.

O objectivo da função Racional::operator + () é calcular a soma de duas fracções n1/d1 e n2/d2.  A função para o cálculo da soma assume que ambas as fracções verificam a CII, i.e., que mdc(n1, d1) = 1 e d1 > 0 e mdc(n2, d2) = 1 e d2 > 0 (quando dois inteiros x e y são tais que mdc(x, y) = 1, diz-se que x e y são mutuamente primos).  O objectivo é calcular uma fracção n/d = n1/d1 + n2/d2 que verifique também a CII, i.e., mdc(n, d) = 1 e d > 0.

Sejam l = mdc(n1, n2) (se n1 e n2 forem ambos zero, faz-se l = 1) e k = mdc(d1, d2) (note-se que l > 0 e k >0, por definição do mdc).  Então, é evidente que

n1/d1 + n2/d2 = l/k (n'1/d'1 + n'2/d'2)
em que
n'1 = n1 / l
n'2 = n2 / l
d'1 = d1 / k
d'2 = d2 / k
Como é óbvio, estas operações garantem que mdc(d'1, d'2) = 1.

Ou seja,

n1/d1 + n2/d2 = (l (n'1d'2 + n'2d'1)) / (kd'1d'2)
Esta fracção, com numerador l (n'1d'2 + n'2d'1) e denominador kd'1d'2 não verifica forçosamente a CII: apesar de o denominador ser forçosamente > 0 (pois todos os termos da multiplicação são positivos), pode haver divisores comuns não unitários entre o denominador e o numerador.

Será que pode haver termos (não unitários) comuns a l e a k?  Suponha-se que existe um x > 1 divisor de l e de k.  Nesse caso, x é também divisor de n1, d1, n2, e d2.  Mas isso não pode acontecer pois, por hipótese, mdc(n1, d1) = 1 e mdc(n2, d2) = 1.  Logo, l e k não têm termos (não unitários) comuns.

Será que pode haver termos comuns a l e a d'1d'2?  Suponha-se que existe um x > 1 divisor de l e de d'1d'2.  Nesse caso, existe forçosamente um y > 1 divisor de l e de d'1 ou de d'2.  Se y for divisor de l e de d'1, então y é também divisor de n1 e d1, o que é impossível, pois por hipótese mdc(n1, d1) = 1.  O mesmo argumento se aplica se y for divisor de l e de d'2.  Logo,  l e d'1d'2 não têm termos (não unitários) comuns.

Será que podem haver termos comuns a n'1d'2 + n'2d'1 e a d'1d'2?  Suponha-se que existe um x > 1 divisor de n'1d'2 + n'2d'1 e de d'1d'2.  Nesse caso, existe forçosamente um y > 1 divisor de n'1d'2 + n'2d'1 e de d'1 ou de d'2.  Se y for divisor de n'1d'2 + n'2d'1 e de d'1, então y tem de dividir também n'1 ou d'2.  Mas isso não pode acontecer, pois implicaria que mdc(n1, d1) <> 1, o que é impossível por hipótese, ou mdc(d'1, d'2) <> 1, o que não pode acontecer por construção de d'1 e d'2.  O mesmo argumento se aplica se y for divisor de n'1d'2 + n'2d'1 e de d'2.  Logo, n'1d'2 + n'2d'1 e d'1d'2 não têm termos (não unitários) comuns.

Assim, a existirem termos comuns (não unitários) ao denominador e numerador da fracção

(l (n'1d'2 + n'2d'1)) / (kd'1d'2)
eles devem-se à existência de termos comuns (não unitários) a n'1d'2 + n'2d'1 e a k.  Assim, sendo
m = mdc(n'1d'2 + n'2d'1, k)
a fracção
n1/d1 + n2/d2 = (l (n'1d'2 + n'2d'1)/m) / ((k/m)d'1d'2)
verifica a CII, i.e., o denominador é positivo e numerador e denominador são mutuamente primos.

Qual foi a vantagem de factorizar l e k e proceder aos restantes cálculos face à alternativa, mais simples, de calcular a fracção como

n1/d1 + n2/d2 = ((n1d2 + n2d1)/h) / (d1d2)/h)
com h = mdc(n1d2 + n2d1, d1d2)?

A vantagem é meramente computacional.  Apesar de os cálculos propostos exigirem mais operações, os valores intermédios dos cálculos são em geral mais pequenos, o que minimiza a possibilidade de existirem valores intermédios não representáveis nos inteiros da linguagem de programação em causa (C++).

6.4  Mais sobre classes em C++ (actualização)

6.4.3  Referências

Na Secção 3.2.7 viu-se que se pode passar um argumento por referência a um procedimento se este tiver definido o parâmetro respectivo como uma referência.  Por exemplo,
int incrementa(int& x)
{
    x++;
    return x;
}
é um procedimento que incrementa a variável passado como argumento.  Este procedimento pode ser usado como no programa que se segue
#include <iostream>
using namespace std;

int incrementa(int& x)
{
    x++;
    return x;
}

int main()
{
    int y = 1;
    incrementa(y);
    cout << y << endl;
}

que imprime no ecrã o valor incrementado de y, isto é, 2.

Mas o conceito de referência pode ser usado de formas diferentes.  Por exemplo,

int i = 1;
int& j = i; // a partir daqui j é sinónimo de i
j = 3;
cout << i << endl;
imprime 3 no ecrã, pois alterar a variável j é o mesmo que alterar a variável i, já que j é sinónimo de i.

As variáveis que são referências, caso do j no exemplo anterior e do parâmetro x do procedimento iincrementa(), têm de ser inicializadas com a variável de que são sinónimos.  Essa inicialização é feita explicitamente no caso de j e implicitamente no caso da variável x, neste caso através da passagem de y como argumento na chamada de incrementa().

Devolução de referências

Suponha-se agora que se pretendia escrever o procedimento incrementa() de tal forma que fosse possível o seguinte programa:
#include <iostream>
using namespace std;

int incrementa(int& x)
{
    x++;
    return x;
}

int main()
{
    int y = 1;
    incrementa(incrementa(y)); // erro!
    cout << y << endl;
}

O objectivo seria incrementar duas vezes a variável y, de modo que aparecesse no ecrã o valor 3.  Mas isso não é possível, pois o procedimento incrementa() devolve um valor do tipo int, e valores não podem ser usados para inicializar referências.  Por isso, a segunda chamada do procedimento é inválida.  Para resolver o problema é necessário que o procedimento incrementa() devolva não um valor do tipo int mas uma referência para um int.  Assim, a versão correcta do programa é
#include <iostream>
using namespace std;

int& incrementa(int& x)
{
    x++;
    return x;
}

int main()
{
    int y = 1;
    incrementa(incrementa(y));
    cout << y << endl;
}

A possibilidade de devolução de referências é extremamente importante, pois permite-nos definir operadores como a incrementação prefixa (++ prefixo) para tipos definidos pelo utilizador (e.g., classes).

Na realidade, ao devolver numa função ou procedimento uma referência, está-se a dar a possibilidade ao utilizador da função ou procedimento de colocar a sua invocação do lado esquerdo da atribuição.  Por exemplo, definido o procedimento incrementa() como acima, é possível escrever código como:

int a = 11;
incrementa(a) = 0;  // possível (mas absurdo), incrementa e depois atribui zero a a.
incrementa(a) /= 2; // possível, incrementa e depois divide a por dois.
Uma vez que o operador de indexação [], usado normalmente para as matrizes, pode ser sobrecarregado para ter significado para tipos definidos pelo utilizador, a devolução de referências permite definir a classe VectorInt abaixo, que se comporta aproximadamente como a classe vector<int> descrita na Secção 7.1, embora com verificação de erros de indexação:
#include <iostream>
#include <vector>
#include <cstdlib>

using namespace std;

class VectorInt {
    vector<int> v;
  public:
    VectorInt(int t);
    // ...
    int& operator [] (int i);
    // ...
};

VectorInt::VectorInt(int t) : v(t) {
}

//...

// vector<int>::size_type é um tipo inteiro sem sinal usado para
// guardar tamanhos e para os índices dos vectores:
int& VectorInt::operator [] (vector<int>::size_type i) {
    if(i < 0 || i >= v.size()) {
        // Há melhores formas de lidar com o erro!
        cerr << "Erro: índice inválido." << endl;
        exit(1);
    }
    return v[i];
}

int main()
{
    VectorInt v(10);

    v[0] = 1;
    v[10] = 3; // índice errado! aborta com mensagem de erro.
}

Operador ++ prefixo para a classe Racional
 Suponha-se que se queria equipar a classe Racional com o operador ++ prefixo.  Em primeiro lugar, é importante perceber que a incrementação de um racional pode ser feita duma forma muito mais simples do que recorrendo à soma de racionais em geral.  Seja n/d o racional guardado numa instância da classe Racional, e que portanto verifica a CII dessa classe, ou seja, d > 0 e mdc(n, d) = 1.  Então,
n/d + 1 = (n + d)/d
em que a fracção (n + d)/d verifica já a CII.  Porquê?  Porque, d > 0 e, a existirem divisores comuns (não unitários) ao numerador e ao denominador, eles teriam de ser também divisores comuns a n e d, o que não pode ocorrer, pois mdc(n, d) = 1.

Assim, o operador ++ prefixo pode ser definido simplesmente como

class Racional {
    ...
    public:
    ...
    Racional& operator ++ ();
};

...

inline Racional& operator ++() {
    n += d;
    return *this;
}

em que, mais uma vez, se devolveu uma referência para um Racional (neste caso a instância implícita na chamada da função membro, ou seja, *this) de modo a permitir escrever código como o que se segue:
Racional r = 1;
++ ++r;
que, de facto, incrementa a variável r duas vezes.
Operador ++ sufixo para a classe Racional
Suponha-se que se pretende implementar o operador ++ sufixo para a classe Racional.  Note-se que o operador ++ sufixo, pelo menos para os tipos básicos do C++, está definido como: "incremente-se a variável, mas sendo o resultado da operação o valor da variável antes da incrementação".  Como é óbvio, isso leva a que não se possa devolver uma referência, mas sim um valor, quando se define esse operador para outros tipos mantendo a mesma semântica para a operação (o que se recomenda).

Por outro lado, como é que se pode indicar ao compilador, na definição do operador, que nos estamos a referir ao operador ++ sufixo e não ao operador ++ prefixo?  Isso faz-se usando um método pouco elegante: acrescentando um parâmetro do tipo int no seu cabeçalho.

Finalmente, é importante perceber que, uma vez definido o operador ++ prefixo, é possível recorrer a ele para construir o operador ++ sufixo: i.e., o novo operador a definir não precisa de ser uma função (ou procedimento) membro, podendo ser uma função (ou procedimento) normal.  É boa política definir como funções (ou procedimentos) membro apenas as funções (ou procedimentos) que de facto necessitem de o ser, i.e., que precisem de aceder (em geral alterar) aos membros privados da classe.

Assim, usando as ideias acima, o operador ++ sufixo poderia ser definido para a classe Racional como se segue:

class Racional {
    ...
    public:
    ...
    Racional& operator ++ ();
};

...

// Incrementação prefixa:
inline Racional& operator ++() {
    n += d;
    return *this;
}
// Incrementação sufixa:
inline Racional operador ++(Racional& r, int) {
    Racional temporário = r;
    ++r;
    return temporário;
}

Como é óbvio, tendo-se devolvido um valor em vez de uma referência, não é possível escrever
Racional r;
r++ ++; // erro!
que de resto já era uma construção inválida no caso dos tipos básicos do C++.

Referências constantes

Considere-se a seguinte função para somar todos os elementos dum vector de inteiros *:
#include <vector>

int soma(std::vector<int> v)
{
    int s = 0;
    for(int i = 0; i != v.size(); i++)
        s += v[i];
    return s;
}

De acordo com o mecanismo de chamada de funções (descrito na Secção 3.3.1), e uma vez que o vector é passado por valor, é evidente que a chamada da função soma() implica a criação duma nova variável do tipo vector<int> que é inicializada com uma cópia do vector passado como argumento.  Se o vector passado como argumento tiver muitos elementos, é evidente que esta cópia pode ser muito demorada, podendo mesmo em alguns casos tornar a utilização da função impossível na prática.  Como resolver o problema?  Se a passagem do vector fosse feita por referência, e não por valor, essa cópia não seria necessária.  Assim, poder-se-ia aumentar a eficiência da chamada da função definindo-a como
#include <vector>

int soma(std::vector<int>& v) // má ideia!
{
    int s = 0;
    for(int i = 0; i != v.size(); i++)
        s += v[i];
    return s;
}

Mas esta nova versão tem uma desvantagem: na primeira versão, o utilizador da função e o compilador sabiam que o vector passado como argumento não poderia ser alterado pela função, já que esta trabalhava com uma cópia.  Na nova versão essa garantia não é feita.  O problema pode ser resolvido se se disser de algum modo que, apesar de o vector ser passado por referência, a função não está autorizada a alterá-lo.  Isso faz-se recorrendo de novo ao qualificador const:
#include <vector>

int soma(const std::vector<int>& v) // boa ideia!
{
    int s = 0;
    for(int i = 0; i != v.size(); i++)
        s += v[i];
    return s;
}

As passagens por referência constante têm uma característica adicional que as distingue das passagens por referência simples: permitem passar qualquer constante (e.g., um valor literal) como argumento, o que não era possível no caso das passagens por referência simples.  Por exemplo:
// Mau código.  Bom para exemplos apenas...
int soma1(int& a, int& b) {
    return a + b;
}

int soma2(const int& a, const int& b) {
    return a + b;
}

int main()
{
    int i = 1, j = 2, res;
    res = soma1(i, j); // válido
    res = soma2(i, j); // válido
    res = soma1(10, 20); // erro!
    res = soma2(10, 20); // válido!
}

* Note-se que se qualificou vector com std::, o que permitiu eliminar a instrução using namespace std; que tem aparecido no início dos exemplos deste texto.  A utilização de espaços nominativos será vista mais tarde.

6.4.4  Destrutores

Da mesma forma que os construtores duma classe são usados para inicializar as suas instâncias quando estas são criadas, também é possível especificar destrutores, i.e., código que deve ser executado quando a instância é destruida.  Os destrutores são extremamente úteis, particularmente quando se utiliza memória dinâmica ou, em geral, quando os construtores da classe reservam algum recurso para uso exclusivo da instância criada (é o caso da memória dinâmica).  I.e., os destrutores servem para "arrumar a casa" quando uma instância é destruída.  Utilizações mais interessantes do conceito ver-se-ão mais tarde, bastando para já apresentar um exemplo ingénuo da sua utilização:
#include <iostream>
using namespace std;

class C {
    public:
        C() {
            cout << "Construí uma instância de C!" << endl;
        }
        ~C() {
            cout << "Destruí uma instância de C!" << endl;
        }
};

int main()
{
    C c; // criação duma instância de C chamada c.
    for(int i = 0; i != 3; i++) {
        C outra; // criação duma instância de C chamada outra.
        cout << i << endl;
        // Quando o bloco de instruções termina, outra é destruída.
    }
    // Quando o bloco de instruções termina, c é destruída.
}

Note-se que os construtores se declaram e definem como os construtores, excepto que se coloca ~ antes do nome da classe.

A execução deste programa resulta em:

Construí uma instância de C!
Construí uma instância de C!
0
Destruí uma instância de C!
Construí uma instância de C!
1
Destruí uma instância de C!
Construí uma instância de C!
2
Destruí uma instância de C!
Destruí uma instância de C!
Recorda-se aqui que variáveis automáticas (variáveis locais sem o qualificador static) são criadas quando a instrução da sua definição é executada, e destruídas quando o bloco de instruções na qual foram definidas termina, e que variáveis estáticas (variáveis globais ou variáveis locais com o qualificador static) só são destruídas no final do programa, sendo as variáveis globais criadas no início do programa e as locais mas estáticas criadas quando a sua instrução de definição é executada.

6.4.5  Membros de classe

Até agora viu-se apenas como definir membros de instância, i.e., membros dos quais cada instância da classe possui uma versão própria.  Como fazer para criar uma variável membro da qual exista apenas uma versão, comum a todas as instâncias da classe, e qual poderá ser a sua utilidade?  E como fazer para criar uma função membro que não precise de ser invocada através de nenhum mebro em particular?

Suponha-se que se criar uma classe C que mantivesse uma contagem do número de instâncias existente em cada instante.  Uma possibilidade seria:

class C {
    static int número_de_instâncias;
  public:
    C() {
        número_de_instâncias++;
    }
    ~C() {
        número_de_instâncias--;
    }
    static int númeroDeInstâncias();
};

int C::número_de_instâncias = 0;

int C::númeroDeInstâncias() {
    return número_de_instâncias;
};

Note-se como os membros de classe (e não de instância) são precedidos do qualificador static. aquando da sua declaração dentro da definição da classe.  Note-se também que a variável membro de classe número_de_instâncias é declarada durante a definição da classe e só é definida (i.e., criada de facto com o valor inicial 0), depois da classe, tal como acontece com as funções membro.

Uma possível utilização da classe seria

#include <iostream>
using namespace std;

class C {
    static int número_de_instâncias;
  public:
    C() {
        número_de_instâncias++;
    }
    ~C() {
        número_de_instâncias--;
    }
    static int númeroDeInstâncias();
};

int C::número_de_instâncias = 0;

int C::númeroDeInstâncias() {
    return númeroDeInstâncias;
};

int main()
{
    {
        C a, b;
        for(int i = 0; i != 3; i++) {
            C x;
            cout << "Existem " << C::númeroDeInstâncias()
                 << " instâncias." << endl;
        }
        cout << "Existem " << C::númeroDeInstâncias()
             << " instâncias." << endl;
    }
    cout << "Existem " << C::númeroDeInstâncias()
         << " instâncias." << endl;
}

Note-se que a invocação da função membro de classe númeroDeInstâncias() se faz não através do operador de selecção de membro (.), o que implicaria a invocação da função através de uma qualquer instância da classe, mas através do operador de rasolução de âmbito ::.  No entanto, é possível invocar funções (ou procedimentos) membro de classe através do operador de selecção de membro.  As mesmas observações se podem fazer no que diz respeito às variáveis membro de classe.

A execução do programa acima teria como resultado:

Existem 3 instâncias.
Existem 3 instâncias.
Existem 3 instâncias.
Existem 2 instâncias.
Existem 0 instâncias.
De aqui em diante utilizar-se-á a expressão "membro" como significando "membro de instância", i.e., membros dos quais cada instância possui uma cópia própria, usando-se sempre a expressão "membro de classe" para os membros partilhados entre todas as instâncias da classe.

6.4.6  Funções amigas

Há casos em que pode ser conveniente definir funções ou procedimentos normais (i.e., não membro) que tenham acesso aos membros privados duma classe.  Por exemplo, suponha-se que se pretendia fazer sobrecarga do operador << para permitir a escrita em canais de saída (ostreams) de instâncias da classe Racional.  I.e., pretendia-se que o código:
Racional r(1, 3);
cout << r << endl;
tivesse como resultado:
1/3
Para isso é necessário fazer a sobrecarga do operador << quando o primeiro operando é do tipo ostream e o segundo é do tipo Racional.  Como se vai escrever no canal, este tem de ser passado por referência.  Assim, definir-se-ia o procedimento:
inline ??? operator << (ostream& saída, Racional r) {
    saída << r.n << '/' << r.d;
    return ???;
}
Mas como aceder às variáveis membro privadas n e d da variável r se esta não é uma função membro da classe *?  Uma solução é, durante a definição da classe Racional, declarar este procedimento (operador) como "amigo" da classe.  Para isso basta colocar a sua declaração (cabeçalho) em qualquer ponto da definição da classe (tanto faz fazer a declaração na parte pública como na privada), precedendo-a do qualificador friend:
class Racional {
    ...
    friend ??? operator << (ostream& saída, Racional r);
    ...
};

...

inline ??? operator << (ostream& saída, Racional r) {
    saída << r.n << '/' << r.d;
    return ???;
}

Sobra no entanto uma dúvida.  O que deve devolver o procedimento?  Recordando que o operador << para saídas usando canais deve poder ser usado em cascata (e.g., cout << r << s << " ola" << endl;), e usando os mesmos argumentos que na Secção Operador ++ prefixo para a classe Racional, torna-se evidente que deve devolver uma referência para o canal de saída, i.e.
inline ostream& operator << (ostream& saída, Racional r) {
    saída << r.n << '/' << r.d;
    return saída;
}
ou simplesmente (porquê?)
inline ostream& operator << (ostream& saída, Racional r) {
    return saída << r.n << '/' << r.d;
}
* Note-se que o operador definido não é membro da classe Racional nem poderia ser.  Se o fosse, o primeiro operando, a instância ímplícita, teria de ser da classe Racional, e não ostream!

Quando usar funções amigas

Em que casos devem ser usadas funções amigas duma classe?  A regra geral é: o mínimo possível.  Se se puder evitar usar funções ou procedimentos amigos tanto melhor, pois evitam-se acessos a membros que se declarou como privados justamente para evitar utilizações erróneas.

Seria possível definir o operador << para escrita em canais de instâncias da classe Racional sem recorrer à declaração do operador como friend?  Sim.  Para isso basta reconhecer que faltam à classe Racional duas funções membro que podem ser muito úteis para o utilizador final.  Afinal, não é útil poder saber qual o numerador e qual o denominador da fracção canónica correspondente a um racional?  Para isso basta definir duas funções membro públicas adicionais numerador() e denominador() que devolvam esses valores.  Note-se que esta solução não põe de todo em causa o princípio do encapsulamento, pois as variáveis membro continuam a ser impossíveis de alterar directamente pelo utilizador da classe.  Assim, uma solução preferível à anterior seria:

class Racional {
    int n;
    int d;
  public:
    ...
    int numerador() const;
    int denominador() const;
    ...
};

...

inline int Racional::numerador() const {
    return n;
}

inline int Racional::denominador() const {
    return d;
}

...

inline ostream& operator << (ostream& saída, Racional r) {
    return saída << r.numerador() << '/' << r.denominador();
}

em que não houve necessidade de declarar qualquer função como amiga da classe Racional.

A definição das duas novas funções membro da classe Racional trará outras consequências benéficas, como se verá mais abaixo.

6.4.7  Construtores por omissão e matrizes de classes

Os tipos definidos pelo utilizador podem ser usados onde quer que um tipo dum tipo básico pode ser usado: por exemplo como elementos de matrizes ou como membros de novas classes.

Construtores por omissão

Todas as classes têm construtores.  Ao construtor que pode ser invocado sem qualquer argumento (porque não tem qualquer parâmetro ou porque todos os parâmetros têm valores por omissão) chama-se "construtor por omissão".  Se o programador não indicar qualquer construtor, é criado implicitamente um construtor por omissão.  Por exemplo, no código
class C {
    Racional r1;
    Racional r2;
    int i;
};

C c; // nova instância, construtor por omissão invocado.

é criado implicitamente um contrutor por omissão que invoca os construtores por omissão da todas as variáveis membro, com excepção das pertencentes a tipos básicos do C++ que, infelizmente, não são inicializadas implicitamente.  Esse construtor por omissão criado implicitamente pode ser invocado explicitamente:
C c = C();
Se o utilizador indicar algum construtor explicitamente, então o construtor por omissão deixa de ser fornecido implicitamente.  Por exemplo, no código seguinte o construtor por omissão faz exactamente o mesmo papel que o construtor por omissão criado implicitamente para a classe C no exemplo anterior:
class C {
    Racional r1;
    Racional r2;
    int i;
  public:
    C() : r1(), r2() {}
};
Mas, como todas as variáveis membro não inicializadas explicitamente na lista de inicializadores (: ... colocado após o cabeçalho do construtor) são inicializadas usando o construtor por omissão, o exemplo pode-se simplificar para:
class C {
    Racional r1;
    Racional r2;
    int i;
  public:
    C() {
    }
};
Note-se que, antes de ser executado o corpo do construtor envolvido numa inicialização, todas as variáveis membro da classe são inicializadas por ordem de definição na classe usando os construtores indicados na lista de inicializadores do construtor (: ... colocado após cabeçalho do construtor na sua definição) ou os construtores por omissão na sua falta (mas as variáveis de tipos básicos do C++ têm de ser inicializadas explicitamente, caso contrário ficam por inicializar).  Por exemplo, no código
class C {
    Racional r1;
    Racional r2;
    int i;
  public:
    C(int n, int d, int ii) : r1(n, d), i(ii) {
        r2 = 3;
    }
};

C c(2, 10, 1);

ao ser criada a variável c é invocado o seu construtor, o que resultará nas seguintes operações:
  1. Chamada do construtor de r1 com argumentos n e d (que neste caso inicializa r1 com 1/5).
  2. Chamada implícita do construtor por omissão de r2 (que inicializa r2 com 0/1).
  3. Inicialização de i com ii (neste caso com 1).
  4. Execução do corpo do construtor de C:
    1. Conversão de 3 de int para Racional.
    2. Atribuição do racional 3/1 a r2.
É de notar que a variável membro r2 é inicializada com 0/1 e só depois lhe é atribuído o racional 3/1, o que é uma perda de tempo.  Seria preferível incluir r2 na lista de inicializadores.

Note-se que a classe C não tem construtor por omissão.  Desse modo, seria um erro escrever:

C c; // erro!

Matrizes de classe

É possível definir matrizes de elementos de classes definidas pelo utilizador, por exemplo da classe Racional:
Racional m1[10] = {Racional()};
Racional m2[10] = {1, 2, Racional(3), Racional(1, 3)};
em que os 10 elementos de m1 e os últimos seis elementos de m2 são inicializados usando o construtor por defeito da classe Racional (que inicializa os racionais com a fracção 0/1), sendo essa inicialização explícita para o primeiro elemento de m1.  Os dois primeiros elementos da matriz m2 são inicializados a partir de inteiros implicitamente convertidos para racionais usando o construtor com um único argumento (i.e., o único construtor com segundo argumento tendo valor por omissão 1).  Essa conversão é explicitada no caso do terceiro elemento de m2.  Já para o quarto elemento, ele é inicializado com um racional construído à custa do construtor completo, com dois argumentos.

Note-se que, se a classe em causa não possuir construtores por omissão, é obrigatório inicializar todos os elementos da matriz explicitamente, e também que as conversões podem precisar de ser explicitadas  Por exemplo:

class C {
    int valor;
  public:
    // Construtor explícito inline que inicializa valor com conteúdo de i:
    explicit C(int i) : valor(i) {}
};

C matriz[5];                      // erro! não tem contrutor por omissão.
C matriz[5] = {C(1), C(2), C(3)}; // erro! inicializadores insuficientes.
C matriz[5] = {C(1), 2, 3, 4, 5}; // erro! conversão implícita impossível.
C matriz[5] = {C(1), C(2), C(3), C(4), C(5)}; // ok!

6.4.8  Conversões para outros tipos

Viu-se já que ao se definir numa classe um construtor passível de ser invocado com um único argumento, se fornece uma forma de conversão implícita de tipo (ver Secção 6.2.6).  Por exemplo, a classe Racional fornece um contrutor que pode ser invocado com um único argumento inteiro, o que possibilita a conversão implícita de tipo entre valores do tipo int e valores da classe Racional:
Racional r(1, 3);
...
if(r < 1) // 1 convertido inplicitamente de int para Racional.
sem haver necessidade de explicitar essa conversão:
Racional r(1, 3);
...
if(r < Racional(1)) // não é necessário...
E se se pretendesse equipar a classe Racional com uma conversão implícita de tipo para double, i.e., tornar o seguinte código válido?
Racional r(1, 2);
double x = r;
cout << r << ' ' << x << endl; // imprime '1/2 0.5'.
cout << double(r) << endl;     // imprime '0.5'.
Nesse caso seria necessário definir um operador de conversão para double, i.e., sobrecarregar operator double.  Este tipo de operadores, de conversão de tipo, têm algumas particularidades:
  1. Só podem ser sobrecarregados através de funções membro a classe a converter.
  2. Não incluem tipo de devolução, pois este é indicado no nome do operador.
Ou seja:
class Racional {
    ...
  public:
    ...
    operator double() const;
    ...
};

...

Racional::operator double() const {
    return double(numerador()) / double(denominador());
}

Note-se que a divisão do numerador pelo denominador é feita depois de ambos serem convertidos para double.  De outra forma seria realizada a divisão inteira, muito longe daquilo que se pretendia...

O problema deste tipo de operadores de conversão de tipo, que devem ser usados com moderação, é que levam frequentemente a ambiguidades.  Por exemplo, definido o operador de conversão para double de valores da classe Racional, como deve ser interpretado o seguinte código:

Racional r(1,3);
...
if(r == 1)
    ...
O compilador deve interpretar a expressão lógica como double(r) == double(1) ou como r == Racional(1)?  As regras do C++ para resolver este tipo de ambiguidades são algo complicadas (ver [2, secção 7.4]) e não resolvem todos os casos.  No exemplo dado o programa é de facto ambíguo e portanto resulta num erro de compilação.  Como é muito mais natural e frequente a conversão implícita dum int num Racional do que a conversão implícita dum Racional num double, a melhor solução é simplesmente não definir o operador Racional::operator double.

Uma aplicação mais útil

Mas há casos em que os operadores de conversão podem ser muito úteis.  Suponha-se que se pretende definir um tipo Palavra que se comporta como um int em quase tudo, mas fornece algumas possibilidades adicionais, tais como acesso individualizado aos seus bits (ver Secção 2.5.4).  Nesse caso, poder-se-ia definir:
class Palavra {
    int valor;
  public:
    Palavra(int v = 0);
    operator int() const;
    bool bit(int n) const;
};

inline Palavra::Palavra(int v) : valor(v) {
}

inline Palavra::operator int () const {
    return valor;
}

inline bool Palavra::bit(int n) const {
    return (unsigned(valor) & (1 << n)) != 0U;
}

O que tornaria possível escrever código como abaixo:
Palavra p = 99996;
p = p + 4;
std::cout << "Valor é: " << p << std::endl;
std::cout << "Em binário: ";
for(int i = 31; i >= 0; i--)
    std::cout << p.bit(i);
std::cout << std::endl;
que resultaria em
Valor é: 100000
Em binário: 00000000000000011000011010100000
Claro que esta classe, para ser verdadeiramente útil, deveria proporcionar outras operações, que ficam como exercício para o leitor.

6.4.9  Novamente os racionais

Apresenta-se abaixo uma nova versão da classe Racional, usando os conceitos descritos nas secções anteriores.  São de notar os seguintes pontos:
  1. Existem agora (de novo) três diferentes construtores.  A separação do construtor único usado anteriormente e três construtores fez-se por usam questão de eficiência, pois se evitam no construtor por omissão e no construtor a partir de um inteiro as verificações que são necessárias no construtor mais geral para garantir que a CII se verifica.
  2. Introduziram-se funções membro públicas que permitem ao utilizador saber o valor do numerador e do denominador da fracção canónica.  Essas funções membro permitiram também transformar em funções normais algumas funções que antes eram membro, com as vantagens daí resultantes.  Por exemplo, sendo o operador == entre racionais definido como função normal (não membro), qualquer das expressões 1 == r e r == 1 é válida quando r é um Racional, coisa que não acontecia se o operador == fosse definido como função membro, pois nesse caso só a segunda expressão seria legal (o operando correspondente à instância implícita na chamada duma função membro nunca é convertido noutro tipo).
  3. Embora as conversões implícitas sejam úteis, é muitas vezes mais eficiente fornecer operadores especializados em trabalhar com determinados tipos.  Por exemplo, é muito mais rápida a soma de uma int com um Racional do que a soma de dois Racionais.  Por isso se fornecem versões especializadas das operações quando envolvem ints.
  4. Quase todas as funções e procedimentos são inline.  Isso deve-se à sua simplicidade (em geral têm um número pequeno de instruções) e ao facto de as operações aritméticas com Racionais serem possivelmente das executadas maior número de vezes no código do utilizador típico (aqui o fabricante não pode fazer melhor que tentar adivinhar as necessidades do utilizador típico do seu produto...).
  5. Note-se que, para encurtar a apresentação do programa, se eliminaram a maior parte dos comentários.  Não tome essa eliminação como exemplo!
#include <iostream>
#include <cstdlib>
using namespace std;

int mdc(int m, int n)
{
    if(m == 0 && n == 0)
        return 1;
    if(m < 0)
        m = -m;
    if(n < 0)
        n = -n;
    while(m != 0) {
        int aux = n % m;
        n = m;
        m = aux;
    }
    return n;
}

class Racional {
private:
    int n;
    int d;
public:
    Racional();
    Racional(int i);
    Racional(int num, int den);
    int numerador() const;
    int denominador() const;
    Racional& operator += (Racional b);
    Racional& operator += (int i);
    Racional& operator ++ ();
    Racional& operator -= (Racional b);
    Racional& operator -= (int i);
    Racional& operator -- ();
    Racional operator - () const;
    Racional& operator *= (Racional b);
    Racional& operator *= (int i);
    Racional& operator /= (Racional b);
    Racional& operator /= (int i);
    friend Racional operator / (int i, Racional a);
};

inline int Racional::numerador() const {
    return n;
}

inline int Racional::denominador() const {
    return d;
}

inline Racional::Racional() {
    n = 0;
    d = 1;
}

inline Racional::Racional(int i) {
    n = i;
    d = 1;
}

inline Racional::Racional(int num, int den) {
    if(den == 0) {
        cerr << "Erro: denominador inválido!" << endl;
        exit(1);
    } else if(den < 0) {
        num = -num;
        den = -den;
    }
    int k = mdc(num, den);
    n = num / k;
    d = den / k;
}

inline Racional& Racional::operator += (Racional b) {
    int l = mdc(numerador(), b.numerador());
    int k = mdc(denominador(), b.denominador());
    int dk = denominador() / k;
    int bdk = b.denominador() / k;
    int termo = (numerador() / l) * bdk + (b.numerador() / l) * dk;
    int m = mdc(termo, k);
    n = (termo / m) * l;
    d = (k / m) * dk * bdk;
    return *this;
}

inline Racional& Racional::operator += (int i) {
    n += denominador() * i;
    return *this;
}

inline Racional& Racional::operator ++ () {
    n += denominador();
    return *this;
}

inline Racional& Racional::operator -= (Racional b) {
    int l = mdc(numerador(), b.numerador());
    int k = mdc(denominador(), b.denominador());
    int dk = denominador() / k;
    int bdk = b.denominador() / k;
    int termo = (numerador() / l) * bdk - (b.numerador() / l) * dk;
    int m = mdc(termo, k);
    n = (termo / m) * l;
    d = (k / m) * dk * bdk;
    return *this;
}

inline Racional& Racional::operator -= (int i) {
    n -= denominador() * i;
    return *this;
}

inline Racional& Racional::operator -- () {
    n -= denominador();
    return *this;
}

inline Racional Racional::operator - () const {
    Racional r;
    r.n = -numerador();
    r.d = denominador();
    return r;
}
 

inline Racional& Racional::operator *= (Racional b) {
    int l = mdc(denominador(), b.numerador());
    int k = mdc(numerador(), b.denominador());
    n = (numerador() / k) * (b.numerador() / l);
    d = (denominador() / l) * (b.denominador() / k);
    return *this;
}

inline Racional& Racional::operator *= (int i) {
    int l = mdc(denominador(), i);
    n *= i / l;
    d /= l;
    return *this;
}

inline Racional& Racional::operator /= (Racional b) {
    if(b.numerador() == 0) {
        cerr << "Erro: divisão por zero!" << endl;
        exit(1);
    }
    int l = mdc(numerador(), b.numerador());
    int k = mdc(denominador(), b.denominador());
    n = (numerador() / l) * (b.denominador() / k);
    d = (denominador() / k) * (b.numerador() / l);
    return *this;
}

inline Racional& Racional::operator /= (int i) {
    if(i == 0) {
        cerr << "Erro: divisão por zero!" << endl;
        exit(1);
    }
    int l = mdc(numerador(), i);
    n /= l;
    d *= i / l;
    return *this;
}

inline Racional operator / (int i, Racional a) {
    if(a.numerador() == 0) {
        cerr << "Erro: divisão por zero!" << endl;
        exit(1);
    }
    int l = mdc(a.numerador(), i);
    a.d = a.numerador() / l;
    a.n = a.denominador() * (i / l);
    return a;
}

inline Racional operator + (Racional a, Racional b) {
    return a += b;
}

inline Racional operator + (Racional a, int i) {
    return a += i;
}

inline Racional operator + (int i, Racional a) {
    return a + i;
}

inline Racional& operator + (Racional a) {
    return a;
}

inline Racional operator ++ (Racional& a, int) {
    Racional r = a;
    ++a;
    return r;
}

inline Racional operator - (Racional a, Racional b) {
    return a -= b;
}

inline Racional operator - (Racional a, int i) {
    return a -= i;
}

inline Racional operator - (int i, Racional a) {
    return -(a - i);
}

inline Racional operator -- (Racional& a, int) {
    Racional r = a;
    --a;
    return r;
}

inline Racional operator * (Racional a, Racional b) {
    return a *= b;
}

inline Racional operator * (Racional a, int i) {
    return a *= i;
}

inline Racional operator * (int i, Racional a) {
    return a * i;
}

inline Racional operator / (Racional a, Racional b) {
    return a /= b;
}

inline Racional operator / (Racional a, int i) {
    return a /= i;
}

inline bool operator == (Racional a, Racional b) {
    return a.numerador() == b.numerador() &&
           a.denominador() == b.denominador();
}

inline bool operator == (Racional a, int i) {
    return a.numerador() == i && a.denominador() == 1;
}

inline bool operator == (int i, Racional a) {
    return a == i;
}

inline bool operator != (Racional a, Racional b) {
    return !(a == b);
}

inline bool operator != (Racional a, int i) {
    return !(a == i);
}

inline bool operator != (int i, Racional a) {
    return !(i == a);
}

inline bool operator < (Racional a, Racional b) {
    int l = mdc(a.numerador(), b.numerador());
    int k = mdc(a.denominador(), b.denominador());
    return (a.numerador() / l) * (b.denominador() / k) <
           (b.numerador() / l) * (a.denominador() / k);
}

inline bool operator < (Racional a, int i) {
    int l = mdc(a.numerador(), i);
    return (a.numerador() / l) < (i / l) * a.denominador();
}

inline bool operator < (int i, Racional a) {
    int l = mdc(a.numerador(), i);
    return (i / l) * a.denominador() < (a.numerador() / l);
}

inline bool operator >= (Racional a, Racional b) {
    return !(a < b);
}

inline bool operator >= (Racional a, int i) {
    return !(a < i);
}

inline bool operator >= (int i, Racional a) {
    return !(i < a);
}

inline bool operator > (Racional a, Racional b) {
    return b < a;
}

inline bool operator > (Racional a, int i) {
    return i < a;
}

inline bool operator > (int i, Racional a) {
    return a < i;
}

inline bool operator <= (Racional a, Racional b) {
    return b >= a;
}

inline bool operator <= (Racional a, int i) {
    return i >= a;
}

inline bool operator <= (int i, Racional a) {
    return a >= i;
}

inline ostream& operator << (ostream& out, Racional r) {
    return cout << r.numerador() << '/' << r.denominador();
}

istream& operator >> (istream& in, Racional& r)
{
    int n;
    if(!(in >> n))
        return in;
    char c;
    if(!in.get(c))
    {
        r = Racional(n, 1);
        return in;
    }
    if(c == '/') {
        int d;
        if(!(in >> d))
            return in;
        r = Racional(n, d);
        return in;
    }
    in.putback(c);
    r = Racional(n, 1);
    return in;
}

Racional raizQuadrada(Racional v)
{
    const Racional tol(1, 100);
    Racional r = v;
    Racional diff;
    do {
        diff = (r - v / r) / 2;
        r -= diff;
    } while(diff >= tol || diff <= -tol);
    return r;
}

int main()
{
    cout << raizQuadrada(Racional(2)) << endl;
}

6.4.10  Exercícios

1.  Construir uma classe Mapa que guarde cadeias de caracteres e inteiros associados.  Cada cadeia de caracteres só pode ocorrer uma única vez.  Esta classe poderia servir para guardar nomes de variáveis e os respectivos valores numa calculadora escrita em C++.  Antes de escrever qualquer código em C++ pense nas operações que devem ser suportadas.  Escreva um pequeno programa que faça uso da classe desenvolvida.

2.  Refaça a classe PilhaInt de modo a usar um vector<int> em vez de uma matriz (array) de int.  Flexibilize a classe permitindo ao utilizador especificar o tamanho máximo da pilha aquando da sua criação.  Veja a Secção 7.1.

3.  Refaça o exercício anterior de modo a não se ter de impor qualquer limite às pilhas criadas.

6.5  Referências (actualização)

[2]  Bjarne Stroustrup, "The C++ Programming Language", terceira edição, Addison-Wesley, Reading, Massachusetts, 1998.