6  Classes

Até agora, apenas se viu como expandir a linguagem acrescentando-lhe operações (com excepção dos tipos enumerados, uma versão simples da expansão da linguagem através da construção de novos tipos).  Viu-se que isso se conseguia construindo funções e procedimentos resolvendo pequenos problemas não resolvidos pela linguagem básica nem pela biblioteca padrão.  Foi o caso, por exemplo do cálculo do mdc ou do factorial.  Para realizar essas tarefas viu-se ser fundamental dominar os conceitos de função, procedimento, parâmetro e argumento, instrução de selecção e iterativa, e metodologias de desenvolvimento de ciclos, para não falar do conhecimentos sobre os tipos básicos e respectivas operações, variáveis e construção de expressões.

A partir deste ponto o ênfase será posto na construção de novos tipos.  De início construir-se-ão novos tipos relativamente simples e independentes uns dos outros.  Quando se iniciar o estudo da programação orientada por objectos ver-se-á como desenhar hierarquias de tipos e quais as suas aplicações na resolução de problemas de maior escala.

6.1  Introdução às classes

A construção de novos tipos em C++ faz-se usando o conceiro de classe.  Suponha-se que se desejava, no âmbito do desenvolvimento dum programa de gestão de uma escola, equipar a linguagem C++ com um novo tipo chamado Aluno.  Uma vez que um aluno consiste, no mínimo, de informação relativa ao seu nome e número, a forma mais simples de o fazer seria escrever:
class Aluno {
  public:
    string nome;
    int número;
};
A sintaxe de definição de um tipo é, portanto
class nome_do_tipo {
    declaração_de_membros
};
sendo importante notar que este é um dos poucos locais onde o C++ exige um ; depois da } final.

6.1.1  Definição de classes

A definição de uma classe é, portanto, a declaração dos seus membros.  No caso apresentado, as variáveis do tipo Aluno, quando forem criadas, consistem em dois membros: um nome do tipo string (que guarda sequências arbitrárias de caracteres, está definido na biblioteca padrão e é utilizável fazendo #include <string> no início do programa), e um número do tipo int.  Tal como as matrizes, as classes permitem guardar agregados de informação (ou seja, agregados de variáveis, chamados elementos no caso das matrizes e membros no caso das classes), com a diferença de que, no caso das classes, essa informação pode ser de tipos diferentes.

6.1.2  Selecção de membros

Como se pode usar a nova classe?  Por exemplo, o código
Aluno aluno1;

aluno1.nome = "Zacarias Zagalo";
aluno1.número = 123;

cria uma variável do tipo Aluno e inicializa-a com o nome Zacarias Zagalo e o número 123.  Assim, a sintaxe de acesso aos membros duma classe corresponde à escrita do nome do membro precedida de um ponto (.) *.  Para todos os efeitos, os membros da classe Aluno  funcionam como variáveis guardadas na variável aluno1.

É importante perceber que cada variável do tipo Aluno possui as suas próprias variáveis membro.  Por exemplo:

Aluno aluno1;

aluno1.nome = "Zacarias Zagalo";
aluno1.número = 123;

Aluno aluno2;

aluno2.nome = "Xisto Ximenes";
aluno1.número = 321;
 

cria duas variáveis aluno1 e aluno2 com informação relativa a dois diferentes alunos.  Por outro lado, os nomes dos membros duma classe só têm visibilidade dentro dessa classe, pelo que poderia existir um variável de nome número no exemplo acima:
int número = 1000;
Aluno aluno1;

aluno1.nome = "Zacarias Zagalo";
aluno1.número = 123;

Aluno aluno2;

aluno2.nome = "Xisto Ximenes";
aluno1.número = 321;

sem que isso causasse qualquer problema.

* Em rigor, o ponto (.) é o operador de selecção de membro, como se pode ver na Tabela Precedência de operadores.

6.1.3  Alguma nomenclatura

A uma variável de uma dada classe é usual chamar-se, no jargão da programação orientada para os objectos, uma "instância" da classe.  Esta palavra, já existindo em português, foi importada do inglês recentemente com o novo significado de "exemplo concreto" (cf. a expressão "for instance", com o significado de "por exemplo").  Assim, uma variável duma classe é uma instância ou exemplo concreto dum espécimen dessa classe, tal como uma mulher ou um homem são instâncias da classe dos humanos.  Assim, às variáveis membro é vulgar chamar-se variáveis de instância.

Ver-se-há mais tarde que também podem existir funções ou procedimentos membro duma classe.  A essas funções ou procedimentos é comum chamar-se "métodos".  Por vezes também se ouve o termo "passar uma mensagem" em vez de invocar uma função membro.

Neste texto adoptar-se-ão apenas alguns destes termos.  Assim, uma classe é constituída por um conjunto de membros.  Estes membros podem ser funções (ou procedimentos) ou variáveis.  Os membros, ver-se-á mais tarde, podem ser de instância ou de classe, consoante cada instância da classe possua a sua própria cópia do membro ou exista apenas uma cópia partilhada entre todas as instâncias da classe.

6.1.4  Operações suportadas

Ao contrário do que se passa com as matrizes, as instâncias (variáveis) duma classe podem-se atribuir livremente entre si.  O efeito de uma atribuição é o de copiar todos as variáveis de instância da variável.  Assim,
Aluno aluno1;

aluno1.nome = "Zacarias Zagalo";
aluno1.número = 123;

Aluno aluno2;

aluno2 = aluno1;

Da mesma forma estão bem definidas as devoluções de valores duma classe e a passagem de argumentos de uma dada classe por valor.  Assim, as variáveis duma classe definida pelo utilizador podem ser usadas exactamente da mesma forma que as variáveis dos tipos básicos do C++.

6.1.5  Membros públicos e privados

No definição da classe Aluno apresentada apareceu uma construção que até agora não foi descrita: public:.  Os membros de uma classe em C++ podem pertencer a uma de três categorias de acesso: público, protegido e privado.  Para já apenas se descreverão a primeira e a última.  Membros públicos, introduzidos pelo especificador de acesso public:, são acessíveis sem qualquer restrição.  Membros privados, introduzidos pelo especificador de acesso private:, são acessíveis apenas por membros da mesma classe (ou, alternativamente, por funções "amigas" da classe, que serão vistas mais tarde).  As vantagens de definir membros privados ficarão claras mais tarde.

6.2  Construção de classes

O exemplo que se segue é algo académico, mas tem a vantagem de mostrar claramente os mecanismos de construção de um novo tipo proporcionados pelo C++.

6.2.1  Os números racionais

Suponha-se que se pretende acrescentar ao C++ um novo tipo para representar números racionais sem qualquer perda de precisão.  Seja esse novo tipo chamado Racional.

Qualquer número racional pode ser representado por uma fracção, que é um par ordenado de números inteiros (n, d), em que n e d são os termos da fracção *.  Ao segundo termo dá-se o nome de denominador (é o que dá o nome à fracção) e ao primeiro numerador (diz a quantas fracções nos referimos).  Por exemplo, (3, 4) significa "três quartos".  Normalmente os racionais representam-se graficamente usando uma notação diferente: n/d.

Uma fracção n/d pode ou não representar um número racional: só se d for não nulo se pode dizer que a fracção corresponde a um número racional.  Outra questão importante tem a ver com unicidade de representação.  Será que fracções diferentes representam racionais diferentes?  A afirmação inversa é verdadeira (racionais diferentes têm forçosamente representações diferentes [ou n ou d o são]), mas -4/2, 2/-1 e -2/1 são fracções que correspondem a um único racional (por acaso também é um inteiro).  Para se obter uma representação única de cada racional, é necessário introduzir algumas restrições adicionais à fracções.

Em primeiro lugar, é necessário usar apenas o numerador ou o denominador para conter o sinal do número racional.  Como já se impôs uma restrição ao denominador, viz. d <> 0, é natural impor uma restrição adicional: d deve ser não negativo.  Assim, d > 0.  Mas é necessária uma restrição adicional.  Para que a representação seja única, é necessário que n e d não tenham qualquer divisor comum diferente de 1.  Ou seja, que mdc(|n|, d) = 1 (uma fracção com mdc(|n|, d) = 1 diz-se nos termos mínimos).

Assim, se n/d for uma fracção em que d > 0 e mdc(n, d) = 1, dir-se-á que a fracção está no formato canónico.

* Há representações alternativas [1].

Operações elementares

As operações elementares (adição, subtracção, multiplicação e divisão) estão bem definidas para os racionais (com excepção da divisão por 0, ou melhor, por 0/1).  Assim, em termos da representação dos racionais como fracções:
n1/d1 + n2/d2 = (n1d2 + n2d1)/(d1d2)
n1/d1 - n2/d2 = (n1d2 - n2d1)/(d1d2)
n1/d1 * n2/d2 = (n1n2)/(d1d2)
n1/d1 / n2/d2 = (n1d2)/(n2d1)
Mas, tal como definidas, estas transformações de fracções não garantem que o resultado esteja no formato canónico, mesmo que as fracções operando o estejam.  No que se segue, assume-se que as fracções operando estão no formato canónico.
Adição e subtracção
Ao observar o resultado da soma de fracções acima, verifica-se facilmente que, se k = mdc(d1, d2), e se l = mdc(n1, n2) *, então, dividindo ambos os termos da fracção por k e colocando l/k em evidência:
n1/d1 + n2/d2 = (l * (n'1d'2 + n'2d'1))/(kd'1d'2)
onde
k d'1 = d1
k d'2 = d2
l n'1 = n1
l n'2 = n2
Mas o resultado, apesar da divisão por k de ambos os termos da fracção, pode não estar no formato canónico.  Repare-se no exemplo:
1/10 + 1/15
em que k = mdc(15, 10) = 5.  Aplicando a equação acima:
 
1/10 + 1/15 = (l * (n'1d'2 + n'2d'1))/(k d'1d'2) = (5)/(5 * 6)
Para reduzir a fracção aos termos mínimos é necessário dividir ambos os termos da fracção por 5.  Ou seja, no caso geral, é necessário calcular m = mdc(|n'1d'2 + n'2d'1|, k) e a versão reduzida da fracção soma é
n1/d1 + n2/d2 = (l * (n'1d'2 + n'2d'1) / m)/((k / m) d'1d'2)
As mesmas observações se podem fazer quanto à subtracção.

* Para simplificar, admite-se que mdc(0, 0) = 1.

Multiplicação
Relativamente à multiplicação de fracções
n1/d1 * n2/d2 = (n1n2)/(d1d2)
também é possível que o resultado não esteja no formato canónico, bastando para isso que existam divisores não unitários comuns a n1 e d2 ou a d1 e n2.  Mas é fácil verificar que, sendo k = mdc(|n1|, d2) e l = mdc(d1, |n2|), a fracção
n1/d1 * n2/d2 = ((n1 / k) (n2 / l))/((d1 / l) (d2 / k))
está, de facto, colocada em termos mínimos.
Divisão
O caso da divisão de fracções
n1/d1 / n2/d2 = (n1d2)/(n2d1)
é muito semelhante ao da multiplicação, sendo mesmo possível usar os métodos acima para a calcular.  Mas, primeiro, é necessário verificar se n2 é zero.  Se o for, a divisão não está definida.  Caso contrário, para se poder usar a multiplicação, ainda é necessário verificar se n2 é positivo.  Se o for, a divisão é calculada multiplicando as fracções canónicas n1/d1 e d2/n2.  No caso contrário multiplicam-se as fracções canónicas n1/d1 e -d2/-n2.

Operações de igualdade e desigualdade

Sendo duas fracções canónicas f1 = n1/d1 e f2 = n2/d2, é evidente que f1 = f2 se e só se n1 = n2 e d1 = d2.  Ou, o que é o mesmo, f1 <> f2 se e só se n1 <> n2oud1 <> d2.

Operações relacionais

Finalmente, como se pode definir uma relação de ordem entre os racionais, podem-se definir as operações relacionais habituais (maior, maior ou igual, menor, e menor ou igual).  Nesse caso tem-se
n1/d1 > n2/d2 <=> n1d2 > n2d1
pois ambos os denominadores são positivos.

Podem-se reduzir os valores a comparar a

n'1d'2 > n'2d'1
sendo k= mdc(d1, d2), l = mdc(|n1|, |n2|) e l n'1 = n1, l n'2 = n2, kd'1 = d1 e k d'2 = d2.

6.2.2  Implementação em C++

Como se representam os racionais em C++, i.e., como se concretiza o conceito de racional criando um tipo Racional em C++?  Poder-se-ia argumentar que, sendo os racionais representáveis por pares de inteiros, bastaria usar matrizes definidas como int r[2]; para representar qualquer racional.  E seria correcto: é possível usar matrizes neste caso.  Mas é, no mínimo, pouco prático, dadas as restrições às operações sobre matrizes (não se podem atribuir, devolver, passar por valor...).  Uma melhor forma de representar os racionais, como se perceberá no final, é usando o conceito de classe em C++.

Definição da classe

Uma vez que se verificou serem os racionais representáveis por fracções, que são pares ordenados de inteiros, uma possível concretização seria:
class Racional {
  public:
    int n; // numerador
    int d; // denominador
};
É muito importante estar ciente das diferenças entre a concretização do conceito de racional e o conceito em si: em C++ os int são limitados!  Isto significa que não é possível representar qualquer racional numa variável do tipo Racional, tal como não era possível representar qualquer inteiro numa variável do tipo int.

Mas uma classe por si só pouco interesse tem.  É necessário definir também as operações que a classe suporta.  Por exemplo, se se pretendesse somar dois racionais, poder-se-ia escrever

Racional soma(Racional a, Racional b)
{
    Racional r;
    r.n = b.d * a.n + a.d * b.n;
    r.d = a.d * b.d;
    return r;
}
(Por enquanto não se garante a canonicidade do resultado.)

Uma possível utilização seria:

#include <iostream>
using namespace std;

class Racional {
  public:
    int n; // numerador
    int d; // denominador
};

Racional soma(Racional a, Racional b)
{
    Racional r;
    r.n = b.d * a.n + a.d * b.n;
    r.d = a.d * b.d;
    return r;
}

int main()
{
    Racional x, y;
    x.n = 1;
    x.d = 0;
    y.n = 7;
    y.d = 15;

    Racional z = soma(x, y);
    cout << z.n << '/' << z.d << endl;
}

Ao escrever este pedaço de código o programador assumiu dois papeis: fabricante e utilizador.  Quando definiu a classe Racional e a função soma(), que opera sobre variáveis dessa classe, fez o papel de fabricante.  Quando escreveu a função main(), assumiu o papel de utilizador.

Encapsulamento: membros públicos e privados

O leitor mais atento terá reparado que o código acima tem alguns problemas.  Um dos mais graves é de que a classe Racional não tem qualquer mecanismo que impeça o utilizador de atribuir 0 (zero) ao denominador duma fracção!  Isso é claramente indesejável, e tem como origem o facto do fabricante ter tornado públicos os membros n e d da classe.  Tal como o utilizador dum relógio ou de um micro-ondas assume que não precisa de conhecer o funcionamento interno desses aparelhos (cujo mecanismo se encontra escondido por uma caixa) recorrendo apenas a uma interface, também o fabricante da classe Racional deveria ter escondido os pormenores de implementação da classe do utilizador final.

Podem-se resumir estas ideias num princípio básico da programação:

Princípio do encapsulamento:  O fabricante deve esconder do utilizador final 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 fabricadas (aparelhos de cozinha, relógios, funções C++, classes C++, etc.).

Isso consegue-se, no caso das classes, usando o especificador de acesso private: para esconder os membros da classe:

class Racional {
  private:
    int n; // numerador
    int d; // denominador
};
Note-se que, ao se classificar os membros n e d como privados, não se impede o utilizador de, usando mecanismos mais ou menos obscuros, aceder ao seu valor.  O facto de um membro ser privado não coloca barreiras muito fortes quanto ao seu acesso.  Pode-se dizer que funciona como um aviso, esse sim forte, de que o utilizador não deve aceder a eles, para seu próprio bem (a fabricante poderia, por exemplo, decidir alterar os nomes dos membros para numerador e denominador, com isso invalidando código que fizesse uso directo dos membros da classe).  O compilador encarrega-se de gerar erros de compilação por cada acesso ilegal a membros privados duma classe.

Tornados os membros n e d da classe privados, torna-se impossível na função main() atribuir valores directamente aos seus membros.  Com isso evitam-se possíveis erros cometidos pelos utilizador.  Mas essa solução cria alguns problemas.  Como aceder aos membros na função soma()?  E como inicializar as variáveis e escrever o resultado no ecrã?

Construção da interface: definição de funções membro

Uma vez que a membros privados têm acesso quaisquer outros membros da classe, a solução passa por criar uma função membro da classe Racional para escrever os racionais no ecrã (mais tarde se verá que, para estes efeitos, existem soluções alternativas).  O primeiro passo é declarar a função membro escreve():
class Racional {
  private:
    int n; // numerador
    int d; // denominador
  public:
    void escreve();
};
São de notar três pontos importantes.  O primeiro é que, para o utilizador poder invocar a nova função, é necessário que esta seja um membro público da classe.  Daí o especificador de acesso public:, que coloca a nova função membro escreve() na interface da classe com o utilizador.  O segundo é que qualquer função membro duma classe tem de ser declarada dentro da definição da classe e definida fora ou, alternativamente, definida (e portanto também declarada) dentro da definição da classe.  O terceiro é que a função escreve() foi declarada sem qualquer parâmetro.  Onde irá buscar o racional a imprimir?

Em primeiro lugar, lembre-se que o acesso aos membros duma classe se faz usando a notação

variável . nome_do_membro
em que variável é uma qualquer instância da classe.  Mas nesse caso, a instrução para escrever a variável z no ecrã, no programa acima, deveria passar a ser:
z.escreve();
Ou seja, a instância para a qual a função membro escreve() é invocada está explícita na própria invocação, e está implícita durante a execução da função!  Mais, essa instância implícita durante a execução pode ser alterada.  Tudo funciona como se a instância usada para invocar a função membro fosse passada automaticamente por referência.

Assim sendo, uma possível definição da função membro escreve() seria:

void Racional::escreve()
{
    cout << n << '/' << d;
}
Nesta definição, n e d referem-se aos membros (privados) da instância para a qual a função foi chamada.  Assim, nas chamadas
x.escreve();
y.escreve();
z.escreve();
as variáveis n e d referem-se sucessivamente aos membros de x, y, e z.

Mas há um pormenor na definição da função membro escreve() que é novo: o nome da função é precedido de Racional::.  Esta notação serve para indicar que escreve() é uma função membro da classe Racional, e não uma função vulgar.

Equipada a classe dessa função membro, o programa pode-se escrever:

#include <iostream>
using namespace std;

class Racional {
  private:
    int n; // numerador
    int d; // denominador
  public:
    void escreve();
};

// PC:
// CO: aparece no ecrã a representação fraccionária do racional *this, sendo *this a instância da classe implícita na execução da função.
void Racional::escreve()
{
    cout << n << '/' << d;
}
Racional soma(Racional a, Racional b)
{
    Racional r;
    // Erro! Não pode aceder aos membros de a, b e r!
    r.n = b.d * a.n + a.d * b.n;
    r.d = a.d * b.d;
    return r;
}

int main()
{
    Racional x, y;
    // Erro! Não pode aceder aos membros de x e y!
    x.n = 1;
    x.d = 0;
    y.n = 7;
    y.d = 15;

    Racional z = soma(x, y);
    z.escreve();
    cout << endl;
}

Para resolver o problema da função soma() que, não sendo membro da classe, não tem acesso aos membros privados, pode-se usar a mesma técnica: tornar soma() uma função membro.  Ou seja:
class Racional {
  private:
    int n; // numerador
    int d; // denominador
  public:
    void escreve();
    Racional soma(Racional b);
};
// PC:
// CO: r = *this + b, sendo *this a instância da classe implícita na execução da função.
Racional Racional::soma(Racional b)
{
    Racional r;
    r.n = b.d * n + d * b.n;
    r.d = d * b.d;
    return r;
}
A utilização desta nova função membro faz-se como anteriormente em relação à função escreve():
int main()
{
    Racional x, y;
    // Erro! Não pode aceder aos membros de x e y!
    x.n = 1;
    x.d = 0;
    y.n = 7;
    y.d = 15;

    Racional z = x.soma(y);
    z.escreve();
    cout << endl;
}

É certo que a forma de invocar a função membro soma() se tornou, à primeira vista, um pouco estranha.  Mas mais tarde ver-se-á como sobrecarregar o operador soma do C++ (+) de modo que se possa escrever simplesmente Racional z = x + y;.

Inicializando membros privados: construtores

Finalmente, falta resolver o problema da inicialização das variáveis x e y da função main(), tornada impossível quando os membros n e d da classe foram definidos como privados.

Idealmente seria desejável inicializar essas variáveis usando as formas usuais do C++.  Por exemplo, se se pode escrever

int i = 1;
int j(10);
para definir e inicializar as variáveis i e j, do tipo int, com os valores 1 e 10, respectivamente, também seria desejável poder inicializar as variáveis a e b do programa como se segue:
Racional a = 1;
Racional b(7, 15);
Isso é possível desde que se equipe a classe Racional de funções membro especiais chamadas construtores.  Estas funções são especiais por várias razões:
  1. São usadas para inicializar as variáveis membro da classe quando uma instância da classe é criada.
  2. O seu nome é sempre, por definição, igual ao nome da classe a que pertencem.
  3. Não podem devolver qualquer valor em nenhuma circunstância, pelo que no seu cabeçalho não se coloca nunca o tipo de devolução.
Assim, para criar um construtor que aceite um único argumento inteiro que leve à validade da inicialização da variável a acima,
class Racional {
  private:
    int n; // numerador
    int d; // denominador
  public:
    // Normalmente os construtores são as primeiras funções membro a
    // declarar e a definir:
    Racional(int i);
    void escreve();
    Racional soma(Racional b);
};

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

Procede-se da mesma forma se se pretender tornar válida a segunda forma de inicialização (inicialização de b acima).  Mas, especificadas essas duas formas de inicialização, torna-se impossível não usar pelo menos uma delas ao definir uma nova variável da classe Racional.  Por exemplo,
Racional c;
é uma definição inválida.  Isto acontece porque não se criou um construtor sem argumentos que fosse chamado nestas circunstâncias e que atribuisse um valor razoável (por exemplo 0) ao racional.  Assim, definir-se-ão dois novos construtores:
#include <cstdlib> // para poder usar a função exit().

class Racional {
  private:
    int n; // numerador
    int d; // denominador
  public:
    // Normalmente os construtores são as primeiras funções membro a
    // declarar e a definir:
    Racional(int num, int den);
    Racional(int i);
    Racional();
    void escreve();
    Racional soma(Racional b);
};

// Construtor com dois parâmetros (numerador e denominador):
Racional::Racional(int num, int den)
{
    // Se den = 0, então o programa deve assinalar uma falha
    // grave!  Para já terminar-se-á o programa.  Mais tarde
    // se aprenderão formas mais elegantes de lidar com estes
    // casos:
    if(den == 0) {
        // É má ideia escrever no ecrã aqui, mas por enquanto
        // não conhecemos melhores alternativas:
        cerr << "Erro: denominador inválido!" << endl;
        exit(1);
    } else if(den < 0) {
        n = -num;
        d = -den;
    } else {
        n = num;
        d = den;
    }
}

// Construtor com um parâmetro (número inteiro):
Racional::Racional(int i)
{
    n = i;
    d = 1;
}

// Construtor sem parâmetros:
Racional::Racional()
{
    n = 0;
    d = 1;
}

Simplificando os construtores: parâmetros com valores por omissão

O C++ permite a definição de funções em que alguns parâmetros têm valores por omissão.  I.e., se não forem colocados os argumentos respectivos numa chamada à função, os parâmetros serão inicializados com os valores por omissão em vez de serem inicializados com o valor dos argumentos.  Mas com uma restrição: os parâmetros com valores por defeito têm de ser os últimos da função.  Este tipo de facilidade permite reduzir os três construtores anteriores a um único:
 
#include <cstdlib> // para poder usar a função exit().

class Racional {
  private:
    int n; // numerador
    int d; // denominador
  public:
    // Normalmente os construtores são as primeiras funções membro a
    // declarar e a definir:
    Racional(int num = 0, int den = 1);
    void escreve();
    Racional soma(Racional b);
};

// Construtor com dois parâmetros (numerador e denominador):
Racional::Racional(int num, int den)
{
    // Se den = 0, então o programa deve assinalar uma falha
    // grave!  Para já terminar-se-á o programa.  Mais tarde
    // se aprenderão formas mais elegantes de lidar com estes
    // casos:
    if(den == 0) {
        // É má ideia escrever no ecrã aqui, mas por enquanto
        // não conhecemos melhores alternativas:
        cerr << "Erro: denominador inválido!" << endl;
        exit(1);
    } else if(den < 0) {
        n = -num;
        d = -den;
    } else {
        n = num;
        d = den;
    }
}

O programa completo

#include <iostream>
#include <cstdlib>
using namespace std;

class Racional {
  private:
    int n; // numerador
    int d; // denominador
  public:
    // Normalmente os construtores são as primeiras funções membro a
    // declarar e a definir:
    Racional(int num = 0, int den = 1);
    void escreve();
    Racional soma(Racional b);
};

// Construtor com dois parâmetros (numerador e denominador):
Racional::Racional(int num, int den)
{
    // Se den = 0, então o programa deve assinalar uma falha
    // grave!  Para já terminar-se-á o programa.  Mais tarde
    // se aprenderão formas mais elegantes de lidar com estes
    // casos:
    if(den == 0) {
        // É má ideia escrever no ecrã aqui, mas por enquanto
        // não conhecemos melhores alternativas:
        cerr << "Erro: denominador inválido!" << endl;
        exit(1);
    } else if(den < 0) {
        n = -num;
        d = -den;
    } else {
        n = num;
        d = den;
    }
}

// PC:
// CO: aparece no ecrã a representação fraccionária do racional *this, sendo *this a instância da classe implícita na execução da função.
void Racional::escreve()
{
    cout << n << '/' << d;
}

// PC:
// CO: r = *this + b, sendo *this a instância da classe implícita na execução da função.
Racional Racional::soma(Racional b)
{
    Racional r;
    r.n = b.d * n + d * b.n;
    r.d = d * b.d;
    return r;
}

int main()
{
    Racional x = 1, y(7, 15);
    Racional z = x.soma(y);
    z.escreve();
    cout << endl;
}
O leitor mais atento terá reparado que, na CO das duas funções membro escreve() e soma(), aparece a construção *this.  Esta construção refere-se à instância implícita durante a execução da função.  Assim, quando escreve() é invocada através de z.escreve() (em main()), *this refere-se à variável z.  Posto isto, é evidente que o corpo da função escreve() poderia ter sido escrito como
cout << (*this).n << '/' << (*this).d;
onde os parânteses são necessários porque o operador . tem maior precedência que o operador *.  Mas tarde compreenderá o que significa o *: por enquanto não se preocupe.

Notas finais

Em aulas posteriores se verá como alterar a classe racional para que o programa se possa escrever simplesmente:
#include <iostream>
#include "racional"

int main()
{
    Racional x = 1, y(7, 15);
    Racional z = x + y;
    cout << z << endl;
}

ou, ainda mais simples,
#include <iostream>
#include "racional"

int main()
{
    cout << Racional(1) + Racional(7, 15) << endl;
}

6.2.3  Exercícios

1.  Reparou que a classe Racional acima não garante que as fracções estejam em termos mínimos?  Que deveria fazer para o garantir?  Se forem necessárias funções auxiliares, pense se deverão ser funções normais ou funções membro da classe e, no último caso, pense se deverão ser membros públicos ou privados.

2.  Sendo numerador e denominador representados por variáveis membro do tipo int, é possível que a sua gama de valores admissíveis seja excedido durante o cálculo simplório da soma apresentado (e.g., se os inteiros tivessem 10 bits e fossem representados em complemento para dois, a soma 1/500 + 1/500 daria 1000/250000, em que 250000 excede em muito a gama -512 a 511 correspondente a 10 bits, isto apesar de a soma, em termos mínimos, ser 1/250!).  Reescreva a função soma() evitando tanto quanto possível que a gama dos inteiros seja excedida.

3.  Complete a classe Racional apresentada acima:

a)  Implemente, na forma de funções membro, as operações elementares adicionais usando números racionais (i.e., subtracção, multiplicação e divisão).  Implemente também uma função membro para calcular o simétrico.
b)  Implemente, na forma de funções membro, as operações de comparação e relacionais (i.e., igualdade, diferença, maior, maior ou igual, menor, menor ou igual).
c)  Implemente uma função membro void lê() para ler um número racional do teclado.
d)  Escreva um programa que teste a classe desenvolvida.

6.2.4  Corrigindo a classe Racional

Uma observação da classe Racional tal como definida anteriormente revela que, na sua implementação, se violou uma das regras que se estabeleceram inicialmente: as fracções devem estar no formato canónico.  Verdade seja dita, esta restrição tem apenas parcialmente a ver com a utilização da classe.  É que, para o utilizador final, a representação interna dos racionais é irrelevante, muito embora ele espere que a função membro escreve() resulte numa representação canónica dos racionais.  Poderia então parecer que o problema poderia ser resolvido alterando apenas a função membro escreve(), e deixando o restante código tal como está.

Mas a verdade é que, sendo os int limitados em C++, a utilização duma representação não canónica das fracções põe alguns problemas graves de implementação.  Suponha-se o seguinte código:

int main()
{
    Racional x(1/50000), y(1/50000);
    Racional z = x.soma(y);
    z.escreve();
    cout << endl;
}
No ecrã deveria aparecer
1/25000
mas a verdade é que, ao se calcular o denominador do resultado, multiplicam-se os dois denominadores, de que deveria resultar 50 000 x 50 000 = 2 500 000 000.  Mas, em máquinas em que os int têm 32 bits, esse valor não é representável, pelo que se obtem um valor errado (em Linux i386 obtem-se -1794967296!).  Isto apesar de a fracção resultado ser perfeitamente representável.

É pois desejável não só usar uma representação canónica para os racionais, o que de resto simplifica a escrita de operações de comparação, por exemplo, como tentar garantir que os resultados de cálculos intermédios são tão pequenos quanto possível.

Uma nova versão da função mdc()

A versão mais eficiente da função para cálculo do mdc de dois inteiros apresentada até agora é (ver Secção 3.2.11)
// PC: m >= 0 e n > 0 e m = m e n = n
// CO: n = mdc(m, n)
int mdc(int m, int n)
{
    while(m != 0) {
        int auxiliar = n % m;
        n = m;
        m = auxiliar;
    }
    return n;
}
É fácil verificar que a PC se pode relaxar de modo a admitir n >= 0, muito embora se tenha de continuar a impor que pelo menos uma dos valores é diferente de zero, sem que isso afecte o funcionamento da função:
// PC: m >= 0 e n >= 0 e (m <> 0 ou n <> 0) em = m e n = n
// CO: n = mdc(m, n)
int mdc(int m, int n)
{
    while(m != 0) {
        int auxiliar = n % m;
        n = m;
        m = auxiliar;
    }
    return n;
}
Mas seria de todo o interesse que a função aceitasse valores negativos e mesmo os dois valores nulos, nesse caso convencionando-se que mdc(0, 0) = 1, por exemplo.  A função abaixo resolve o problema:
// Cálculo do mdc de dois números inteiros estendido de modo a funcionar
// para argumentos negativos ou nulos.
// PC: n = n e m = m
// CO: (n = 0 e m = 0 en = 1) ou ((m <> 0 ou n <> 0) en = mdc(|m|, |n|))
int mdc(int m, int n)
{
    if(m == 0 && n == 0)
        return 1;
    if(m < 0)
        m = -m;
    // Aqui forçosamente m = |m| e m >= 0
    if(n < 0)
        n = -n;
    // Aqui m = |m|, m >= 0, e n = |n|, n >= 0, sendo pelo menos um de m e n
    // diferente de zero.
    // CI: mdc(m, n) = mdc(|m|, |n|)
    while(m != 0) {
        int aux = n % m;
        n = m;
        m = aux;
    }
    return n;
}

Uma nova versão da função membro soma()

Usando a nova função mdc() e as ideias apresentadas em Adição e Subtracção, a função membro soma() pode-se reescrever como
// PC:
// CO: r = *this + b, sendo *this a instância da classe implícita na execução
//    da função.
Racional Racional::soma(Racional b)
{
    int l = mdc(n, b.n);
    int k = mdc(d, b.d);
    int termo = (n / l) * (b.d / k) + (b.n / l) * (d / k);
    int m = mdc(termo, k);
    Racional r;
    r.n = (termo / m) * l;
    r.d = (k / m) * (d / k) * (b.d / k);
    return r;
}
As mesmas ideias podem ser aplicadas a qualquer outra operação com os racionais.  Por exemplo, uma função membro para verificação se um racional é menor que outro poderia ser:
// PC:
// CO: devolve valor lógico de *this < b, sendo *this a instância da classe implícita
//        na execução da função.
Racional Racional::menor(Racional b)
{
    int l = mdc(n, b.n);
    int k = mdc(d, b.d);
    return (n / l) * (b.d / k) < (b.n / l) * (d / k);
}
Uma função para verificar da igualdade poderia ser mais simples, dada a certeza da canonicidade de representação das fracções:
// PC:
// CO: devolve valor lógico de *this = b, sendo *this a instância da classe implícita
//        na execução da função.
Racional Racional::igual(Racional b)
{
    return n == b.n && d == b.d;
}

Revisão da função construtora

Mas, para que a canonicidade de representação das fracções seja real, é necessário que a função (membro) construtora garanta que a fracção esteja inicialmente em termos mínimos, coisa que não era feita até aqui.  Assim:
// Construtor com dois parâmetros (numerador e denominador):
Racional::Racional(int num, int den)
{
    // Se den = 0, então o programa deve assinalar uma falha
    // grave!  Para já terminar-se-á o programa.  Mais tarde
    // se aprenderão formas mais elegantes de lidar com estes
    // casos:
    if(den == 0) {
        // É má ideia escrever no ecrã aqui, mas por enquanto
        // não conhecemos melhores alternativas:
        cerr << "Erro: denominador inválido!" << endl;
        exit(1);
    } else if(den < 0) {
        n = -num;
        d = -den;
    } else {
        n = num;
        d = den;
    }
    // Redução da fracção:
    int k = mdc(n, d);
    n /= k;
    d /= k;
}

Versão completa

O programa completo seria então:
#include <iostream>
#include <cstdlib>
using namespace std;

// PC: n = n e m = m
// CO: (n = 0 e m = 0 en = 1) ou ((m <> 0 ou n <> 0) en = mdc(|m|, |n|))
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; // numerador
    int d; // denominador
  public:
    Racional(int num = 0, int den = 1);
    void escreve();
    Racional soma(Racional b);
    bool igual(Racional b);
    bool menor(Racional b);
};

// Construtor com dois parâmetros (numerador e denominador):
Racional::Racional(int num, int den)
{
    if(den == 0) {
        cerr << "Erro: denominador inválido!" << endl;
        exit(1);
    } else if(den < 0) {
        n = -num;
        d = -den;
    } else {
        n = num;
        d = den;
    }
    int k = mdc(n, d);
    n /= k;
    d /= k;
}

// PC:
// CO: aparece no ecrã a representação fraccionária do racional *this, sendo *this
//    a instância da classe implícita na execução da função.
void Racional::escreve()
{
    cout << n << '/' << d;
}

// PC:
// CO: r = *this + b, sendo *this a instância da classe implícita na execução
//    da função.
Racional Racional::soma(Racional b)
{
    int l = mdc(n, b.n);
    int k = mdc(d, b.d);
    int termo = (n / l) * (b.d / k) + (b.n / l) * (d / k);
    int m = mdc(termo, k);
    Racional r;
    r.n = (termo / m) * l;
    r.d = (k / m) * (d / k) * (b.d / k);
    return r;
}

// PC:
// CO: devolve valor lógico de *this = b, sendo *this a instância da classe implícita
//        na execução da função.
bool Racional::igual(Racional b)
{
    return n == b.n && d == b.d;
}

// PC:
// CO: devolve valor lógico de *this < b, sendo *this a instância da classe implícita
//        na execução da função.
bool Racional::menor(Racional b)
{
    int l = mdc(n, b.n);
    int k = mdc(d, b.d);
    return (n / l) * (b.d / k) > (b.n / l) * (d / k);
}

int main()
{
    Racional x = 1, y(7, 15);
    Racional z = x.soma(y);
    z.escreve();
    cout << endl;
    if(x.menor(y))
        cout << "menor" << endl;
    else
        cout << "maior ou igual" << endl;
    if(x.igual(y))
        cout << "igual" << endl;
    else
        cout << "diferente" << endl;
}

Que, depois de executado, produz no ecrã:
22/15
maior
diferente

6.2.5  Usando sobrecarga de operadores

Tal como definida, a classe Racional obriga o utilizador a usar uma notação desagradável e pouco intuitiva para fazer operações com inteiros.  Em particular, seria desejável que a função main(), no programa acima, se pudesse escrever simplesmente como:
int main()
{
    Racional x = 1, y(7, 15);
    Racional z = x + y;
    z.escreve();
    cout << endl;
    if(x < y)
        cout << "menor" << endl;
    else
        cout << "maior ou igual" << endl;
    if(x == y)
        cout << "igual" << endl;
    else
        cout << "diferente" << endl;
}
Se se pudesse escrever o programa como acima, claramente a classe Racional, uma vez equipada com os operadores restantes dos tipos aritméticos, passaria a funcionar para o utilizador como qualquer outro tipo básico do C++.

A solução para este problema passa pela sobrecarga dos operadores do C++ de modo a terem novos significados quando aplicados ao novo tipo Racional, da mesma forma que se tinha visto antes relativamente aos tipos enumerados (ver Secção 5.1).  Mas, ao contrário do que se fez então, agora as funções de sobrecarga têm de continuar membros da classe Racional de modo a poderem aceder aos seus membros privados (alternativamente poder-se-iam usar funções membro amigas da classe, ver Secção 6.4.6).  Ou seja, a solução é simplesmente:

#include <iostream>
#include <cstdlib>
using namespace std;

// PC: n = n e m = m
// CO: (n = 0 e m = 0 en = 1) ou ((m <> 0 ou n <> 0) en = mdc(|m|, |n|))
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; // numerador
    int d; // denominador
  public:
    Racional(int num = 0, int den = 1);
    void escreve();
    Racional operator + (Racional b);
    bool operator == (Racional b);
    bool operator < (Racional b);
};

// Construtor com dois parâmetros (numerador e denominador):
Racional::Racional(int num, int den)
{
    if(den == 0) {
        cerr << "Erro: denominador inválido!" << endl;
        exit(1);
    } else if(den < 0) {
        n = -num;
        d = -den;
    } else {
        n = num;
        d = den;
    }
    int k = mdc(n, d);
    n /= k;
    d /= k;
}

// PC:
// CO: aparece no ecrã a representação fraccionária do racional *this, sendo *this
//    a instância da classe implícita na execução da função.
void Racional::escreve()
{
    cout << n << '/' << d;
}

// PC:
// CO: r = *this + b, sendo *this a instância da classe implícita na execução
//    da função.
Racional Racional::operator + (Racional b)
{
    int l = mdc(n, b.n);
    int k = mdc(d, b.d);
    int termo = (n / l) * (b.d / k) + (b.n / l) * (d / k);
    int m = mdc(termo, k);
    Racional r;
    r.n = (termo / m) * l;
    r.d = (k / m) * (d / k) * (b.d / k);
    return r;
}

// PC:
// CO: devolve valor lógico de *this = b, sendo *this a instância da classe implícita
//        na execução da função.
bool Racional::operator == (Racional b)
{
    return n == b.n && d == b.d;
}

// PC:
// CO: devolve valor lógico de *this < b, sendo *this a instância da classe implícita
//        na execução da função.
bool Racional::operator < (Racional b)
{
    int l = mdc(n, b.n);
    int k = mdc(d, b.d);
    return (n / l) * (b.d / k) < (b.n / l) * (d / k);
}

int main()
{
    Racional x = 1, y(7, 15);
    Racional z = x + y;
    z.escreve();
    cout << endl;
    if(x < y)
        cout << "menor" << endl;
    else
        cout << "maior ou igual" << endl;
    if(x == y)
        cout << "igual" << endl;
    else
        cout << "diferente" << endl;
}

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 uná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.2.6  Construtores: conversões implícitas e valores literais

Valores literais

Já se viu que a construção de classes corresponde a acrescentar à linguagem C++ novos tipos que funcionam praticamente como os seus tipos básicos.  Mas haverá equivalente aos valores literais?  Recorde-se que, num programa em C++, 10 e 100.0 são valores literais dos tipos int e double, respectivamente.  Será possível especificar uma forma para, por exemplo, escrever valores literais do novo tipo racional?  Infelizmente isso é impossível em C++.  Por exemplo, a tentativa
Racional r;
r = 1/3;
redunda num programa aparentemente funcional mas com um comportamento inesperado.  Acontece que a expressão 1/3 é interpretada como a divisão inteira, com resultado zero, que deve ser convertida para um Racional e atribuída à variável r.  Logo, r, depois da atribuição, conterá o racional zero!

Mas existe uma alternativa elegante, proporcionada pelos construtores das classes, e que funciona quase como se de valores literais se tratasse: os construtores podem ser chamados explicitamente para criar um novo valor dessa classe.  Assim, o código anterior deveria ser corrigido para:

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

Conversões implícitas

Os construtores que possam ser chamados com apenas um argumento definem automaticamente uma conversão implícita de tipos.  Por exemplo, o construtor da classe Racional pode ser chamado com apenas um argumento int, o que significa que, sempre que o compilador esperar um Racional e encontrar um int, converte o int implicitamente para um valor Racional.  Por exemplo, estando definido um operator + com operandos do tipo Racional, o seguinte pedaço de código é perfeitamente legal
Racional x(1, 3);
Racional z = x + 1;
tendo o mesmo significado que
Racional x(1, 3);
Racional z = x + Racional(1);
que por sua vez, tendo em conta os valores por omissão dos parâmetros do construtor dos Racional, significa o mesmo que
Racional x(1, 3);
Racional z = x + Racional(1, 1);
que coloca em z o racional (4, 3) (ou 4/3).

Em casos em que esta conversão implícita de tipos é indesejável, pode-se preceder o respectivo construtor da palavra chave explicit.  Assim, se a classe Racional estivesse definida como

class Racional {
    ...
  public:
    explicit Racional(int num = 0, int den = 1);
    ...
};
o compilador assinalaria erro ao encontrar a expressão x + 1.  Neste caso, no entanto, a conversão implícita de int para Racional é útil, pelo que o qualificador explicit é desnecessário.

6.2.7  Exercícios

1.  Complete a classe Racional apresentada acima (use a classe que desenvolveu nos exercícios anteriores), convertendo as operações desenvolvidas de modo a que sejam possíveis expressões usando os operadores usuais no C++ (i.e., +, -, *, /, -, ==, !=, >, >=, <, <=, etc.).  Use sobrecarga de operadores C++.

2.  Use sobrecarga de operadores C++ para implementar os operadores de atribuição e incrementação e decrementação típicos (i.e., +=, -=, *=, /=, ++, --) para a classe Racional.

3.  O cálculo da raiz quadrada de um valor v pode ser feito, duma forma algo ineficiente, procurando a solução da equação f(x) = v, onde f(x) := x2 (em que se usou a notação := com o significado "é definida por"), usando o método de Newton.  Este método advoga que se deve construir uma sequência r0, r1, ... de raízes definida, duma forma recorrente, por rn+1 = rn - (rn2 - v) / (2rn).  Esta sequência converge para a raiz quadrada (positiva) de v quando n tende para infinito, desde que o valor inicial r0 seja positivo.  Escreva uma função Racional raizQuadrada(Racional v) que calcule um valor racional que aproxime a raiz quadrada do racional v.  Considere a aproximação razoável quando a diferença entre termos sucessivos da sequência for inferior a 1/100 (ou seja, quando |rn+1 - rn| = |(rn2 - v) / (2rn)| < 1/100).

Se procurar a raiz quadrada de 2 usando o método sugerido, chegará à surpreendente fracção 577/408, que é uma excelente aproximação (577/408 = 1,41421568..., cujo quadrado é 2,000006007...).

4. [difícil]  Os inteiros são limitados, pelo que, em máquinas em que os int são representados em complemento para 2 e têm 32 bits, o valor racional mais pequeno (em módulo) representável pela classe Racional tal como desenvolvida é 2-32 (aproximadamente 10-10) e o maior (em módulo) 232 (aproximadamente 1010).  Se se pretendesse tornar a classe dos racionais verdadeiramente útil, seria necessário estender a gama dos int consideravelmente.  Não sendo isso possível sem mudar de linguagem, compilador, sistema operativo e/ou computador, a solução pode passar por construir uma nova classe Inteiro, à custa da qual a classe Racional pode ser construída, e que represente inteiros de precisão (virtualmente) arbitrária.  Construa essa classe, usando sequências de int para representar os inteiros de precisão arbitrária.  Pode ter de usar memória dinâmica, a ensinar posteriormente.

6.3  Desenho de classes

Como se desenha uma classe? Quando e porquê o fazer?  Não há respostas taxativas para estas questões, mas tentar-se-á apresentar nesta secção um exemplo de resolução de um problema que sirva para as clarificar um pouco.

Suponha-se que se pretende escrever um programa que leia 10 valores inteiros do teclado e os escreva pela ordem inversa no ecrã.  Um pouco de reflexão sobre o problema revela que ele pode ser resolvido com matrizes C++.  Mas, antes de aceitar a solução como evidente, convém pensar um pouco no problema.

Ponha-se no lugar do computador e admita que lhe são passadas 10 folhas de papel, com número escritos, e que as deve devolver pela ordem inversa à da entrega.  Como organizaria as folhas de papel?  A resposta usual a esta pergunta é que os papeis deveriam ser organizados numa pilha (com os números voltados para cima) porque, numa pilha de papeis, o último papel a entrar é o primeiro a sair.

Não há equivalente a uma pilha de papeis na linguagem C++ (embora exista na biblioteca padrão...).  Surge então a ideia de construir uma classe para acrescentar esse conceito ao C++.  Mas resta uma dúvida: sendo o problema tão simples e facilmente resolúvel usando matrizes directamente, porquê definir uma classe nova?

Mais uma vez não existem respostas taxativas a esta questão.  A questão deve ser analisada sob diversos pontos de vista e uma decisão tomada com base nos resultados dessa análise.  Por exemplo:

  1. O desenvolvimento da nova classe exige substancialmente mais esforço que uma solução ad hoc?
  2. A nova classe pode vir a ser útil no futuro, no mesmo ou noutros programas?
  3. A modularização inerente à construção de uma classe vai trazer vantagens para o desenvolvimento e correcção do programa?
Neste caso, é evidente que um classe representando o conceito de pilha de inteiros pode vir a ser útil no futuro, sobretudo se for facilmente adaptável para representar pilhas de entidades de outro tipo tais como pilhas de char ou pilhas de Racional (mais tarde se verá como se podem construir classes genéricas, ou meta-classes).

6.3.1  Conceito de pilha

Uma pilha de inteiros é uma entidade na qual se podem inserir e remover inteiros, sendo que os inteiros são removidos pela ordem inversa à da inserção.

Operações com pilhas

As pilhas têm de suportar pelo menos as duas operações elementares de inserção de um valor (colocar mais um papel no topo da pilha) e remoção de um valor (tirar o papel que está no topo da pilha).  Mas existe uma outra operação importante: saber que valor está no topo da pilha (ou seja, olhar para o papel no topo da pilha).  É importante notar que as duas últimas operações apenas estão definidas para pilhas que não esteja vazias.  Este facto é perfeitamente normal: para quase todos os tipos de dados à determinadas operações que não estão bem definidas em algumas circunstâncias.  Por exemplo para os inteiros a divisão não está definida se o divisor for zero.  Assim, é conveniente definir uma operação que verifique se uma pilha está vazia ou não.

Note-se que, ao contrário das operações mais usuais com inteiros, como a soma, subtracção, etc., as operações de inserção e remoção de valores duma pilha afectam a própria pilha.  Seria possível definir as operações de tal modo que a inserção de um valor numa pilha resultasse numa nova pilha, idêntica à original mais com um valor adicional.  As vantagens práticas dum tal conceito são poucas, no entanto.

Pilhas limitadas

Na prática qualquer pilha tem um tamanho limite (mesmo uma pilha de papeis deixa de ser manejável quando atinge o tecto...).  Assim sendo, pode ser útil definir uma operação que verifique se a pilha comporta ou não mais algum elemento, i.e., se está ou não cheia.

Outras operações

Finalmente, pode ser útil definir uma operação que calcule o número de valores na pilha num dado instante (ou seja, o correspondente à simples contagem de folhas de papel numa pilha).

6.3.2  Concretização em C++

Uma vez definido o conceito de pilha, o próximo passo consiste concretização do conceito na forma duma classe C++.  Esta concretização é, normalmente, feita em duas fases.  Na primeira fase constrói-se a interface da classe e só na segunda se passa à implementação propriamente dita.  A construção da interface consiste em declarar aquilo que é público na classe, i.e., aquilo a que o utilizador final tem acesso (na analogia da aparelhagem de som, seria a definição dos botões e fichas a colocar no amplificador, por exemplo).  A implementação propriamente dita consiste em definir todos os membros privados e em definir os membros públicos entretanto declarados na interface da classe (na analogia da aparelhagem de som, seria o desenho dos circuitos internos que efectivamente proporcionam a amplificação e todas as outras funcionalidades do amplificador).

A interface

No caso da concretização do conceito de pilha, devem fazer parte da sua interface funções e procedimentos que representem cada uma das operações vistas na secção anterior, bem como os construtores necessários para inicializar as instâncias da classe.  Assim,
class PilhaInt {
    ....
  public:
    PilhaInt();      // construtor da classe.
    void põe(int v); // coloca v no topo da pilha.
    void tira();     // retira valor do topo da pilha.
    int topo();      // devolve valor no topo da pilha.
    bool vazia();    // devolve true sse a pilha estiver vazia.
    bool cheia();    // devolve true sse a pilha estiver cheia.
    int tamanho();   // devolve número de valores na pilha.
};

A implementação

A fase seguinte da concretização em C++ é a implementação de cada uma das operações definidas na interface.  Mas isso implica pensar antes em como guardar os valores presentes na pilha em cada instante.

Como é óbvio, sendo a construção de novos tipos um processo incremental de acrescento de funcionalidade à linguagem C++, é evidente que, para guardar os valores inteiros na pilha, ter-se-á de recorrer a conceitos previamente definidos (alternativamente poder-se-ia construir outro novo tipo, embora correndo o risco de entrar num processo recursivo de definição de novos tipos...).  Neste caso pretende-se de algum modo guardar um conjunto de valores todos do mesmo tipo (int).  A forma mais simples de o fazer, em C++, é usando o conceito de matriz.  Assim, os valores guardados na pilha vão, na realidade, ser guardados numa matriz.  Mas as matrizes em C++ têm de ter um tamanho constante.  O tamanho escolhido para a matriz é importante porque corresponde ao limite de valores que as pilhas suportarão.  Para que se possa facilmente alterar o tamanho limite das pilhas, definir-se-á uma constante para representar esse limite.

Assim,

const int limite = 100;

class PilhaInt {
    int valores[limite];
  public:
    PilhaInt();      // construtor da classe.
    void põe(int v); // coloca v no topo da pilha.
    void tira();     // retira valor do topo da pilha.
    int topo();      // devolve valor no topo da pilha.
    bool vazia();    // devolve true sse a pilha estiver vazia.
    bool cheia();    // devolve true sse a pilha estiver cheia.
    int tamanho();   // devolve número de valores na pilha.
};

Mas é também necessário saber em cada instante quantos elementos estão na pilha e em que posição.  Usando a simples convenção de que o primeiro elemento a entrar na pilha (e o último a sair) é o que está na posição 0 da matriz, encontrando-se todos os outros nos índices subsequentes, é óbvio que uma única variável indica não só o número de elementos na pilha como a posição do próximo elemento a inserir na pilha.  Assim:
const int limite = 100;

class PilhaInt {
    int valores[limite];
    int quantos;
  public:
    PilhaInt();      // construtor da classe.
    void põe(int v); // coloca v no topo da pilha.
    void tira();     // retira valor do topo da pilha.
    int topo();      // devolve valor no topo da pilha.
    bool vazia();    // devolve true sse a pilha estiver vazia.
    bool cheia();    // devolve true sse a pilha estiver cheia.
    int tamanho();   // devolve número de valores na pilha.
};

Por exemplo, se quantos tiver valor 4, isso significa que existem 4 elementos na pilha, nas posições 0 a 3 da matriz, e que o próximo valor a colocar ficará na posição 4 da matriz.

É agora possível definir cada uma das funções e procedimentos membro da classe.  Como é usual, começa-se pelos construtores (neste caso apenas existe um).  O construtor limite-se a colocar quantos a zero, o que é o mesmo que dizer que as pilhas, quando criadas, estão vazias:

PilhaInt::PilhaInt()
{
    quantos = 0;
}
Note-se que não é necessário inicializar os valores na matriz.  Basta que se convencione que, dos limite elementos da matriz, apenas os quantos primeiros são valores colocados na pilha, contendo os restantes limite - quantos valores que são irrelevantes (vulgo lixo).

Em seguida definem-se as funções membro vazia() e cheia(), por exemplo.  Como é óbvio, uma pilha está vazia sse quantos = 0 e está cheira sse quantos = limite.  Assim:

bool PilhaInt::vazia()
{
    return quantos == 0;
}

bool PilhaInt::cheia()
{
    return quantos == limite;
}

A função membro tamanho() é trivial:
int PilhaInt::tamanho()
{
    return quantos;
}
As restantes funções e procedimentos nem sempre estão bem definidas.  Que fazer quando o procedimento membro tira() é invocado para uma pilha vazia?  É claramente um erro, pelo que, e não se tendo ainda aprendido melhores mecanismos para lidar com este tipo de situação, o melhor é abortar imediatamente o programa.  Assim:
void PilhaInt::põe(int v)
{
    if(cheia()) {
        cerr << "Erro: pilha cheia." << endl;
        exit(1);
    }
    valores[quantos++] = v;
}

void PilhaInt::tira()
{
    if(vazia()) {
        cerr << "Erro: pilha vazia." << endl;
        exit(1);
    }
    quantos--;
}

int PilhaInt::topo()
{
    if(vazia()) {
        cerr << "Erro: pilha vazia." << endl;
        exit(1);
    }
    return valores[quantos - 1];
}

É de notar a utilização típica do operador ++ no procedimento membro poe().  O código é equivalente a
void PilhaInt::põe(int v)
{
    if(cheia()) {
        ...
    }
    valores[quantos] = v;
    quantos = quantos + 1;
}
mas apenas porque se usou o operador ++ sufixo!  A utilização do operador ++ prefixo teria um significado diferente e conduziria a um erro, pois
valores[++quantos] = v;
é o mesmo que
quantos = quantos + 1;
valores[quantos] = v;

Class e struct

Na definição da classe PilhaInt acima, não se utilizou o especificador de acesso private:.  Isso deve-se ao facto de, por omissão, os membros duma classe serem privados.  Existe uma construção alternativa a class com uma semântica muito semelhante: struct.  A única diferença está em que, quando se usa struct, os membros são públicos por omissão.

Constantes membro

Ao contrário do que poderia parecer lógico, não é possível escrever:
class PilhaInt {
    const int limite = 100;  // erro!
    int valores[limite];
    ....
}
A razão para a proibição é simples.  Sendo limite uma constante membro (de instância), cada instância da classe possuirá a sua cópia privada dessa constante.  Mas isso significa que, para ser verdadeiramente útil, essa constante deverá poder tomar valores diferentes para cada instância da classe, i.e., para cada variável dessa classe criada.  Daí que não se possam inicializar membros constantes (de instância) na própria declaração.  Como, por definição de constante em C++, não é possível atribuir um valor (através do operador =), o C++ proporciona uma forma algo estranha de inicializar membros constantes: colocando os inicializadores após o cabeçalho do construtor (separados por vírgulas e após um :).  Por exemplo:
class Aluno {
    const int número;
    int nota;
    ...
  public:
    Aluno(int n);
};

Aluno::Aluno(int n) : número(n) {
    ...
}

Este tipo de inicializadores é extremamente útil, sendo utilizado para inicializar não só membros constantes como também membros referência e membros de classes sem construtores por omissão (i.e., que exijam uma inicialização explícita).

6.3.3  O programa completo

Uma vez concretizado o conceito de pilha (limitada) em C++, é possível utilizá-lo para escrever pela ordem inversa dez valores inteiros lidos do teclado: basta definir uma pilha e colocar os valores lidos sucessivamente no topo dessa pilha, e depois ir retirando os valores da pilha, um a um, escrevendo-os no ecrã.  Ou seja, o programa completo seria:
#include <iostream>
using namespace std;

const int limite = 100;

class PilhaInt {
    int valores[limite];
    int quantos;
  public:
    PilhaInt();      // construtor da classe.
    void põe(int v); // coloca v no topo da pilha.
    void tira();     // retira valor do topo da pilha.
    int topo();      // devolve valor no topo da pilha.
    bool vazia();    // devolve true sse a pilha estiver vazia.
    bool cheia();    // devolve true sse a pilha estiver cheia.
    int tamanho();   // devolve número de valores na pilha.
};

PilhaInt::PilhaInt()
{
    quantos = 0;
}
bool PilhaInt::vazia()
{
    return quantos == 0;
}

bool PilhaInt::cheia()
{
    return quantos == limite;
}

int PilhaInt::tamanho()
{
    return quantos;
}
void PilhaInt::põe(int v)
{
    if(cheia()) {
        cerr << "Erro: pilha cheia." << endl;
        exit(1);
    }
    valores[quantos++] = v;
}

void PilhaInt::tira()
{
    if(vazia()) {
        cerr << "Erro: pilha vazia." << endl;
        exit(1);
    }
    quantos--;
}

int PilhaInt::topo()
{
    if(vazia()) {
        cerr << "Erro: pilha vazia." << endl;
        exit(1);
    }
    return valores[quantos - 1];
}

int main()
{
    PilhaInt pilha;

    // Coloca 10 valores na pilha:
    for(int i = 0; i != 10; i++) {
        int v;
        cin >> v;
        pilha.põe(v);
    }

    // Escreve-os pela ordem inversa:
    while(!pilha.vazia()) {
        cout << pilha.topo() << endl;
        pilha.tira();
    }
}

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++

6.4.1  Funções e procedimentos inline

A classe PilhaInt tal como definida acima é muito simples, mas é também algo ineficiente: todas as operações são realizadas através da invocação de funções ou procedimentos, o que poderá resultar, em alguns casos, num peso computacional excessivo (ver Secção 3.3.1).  Sempre que uma função ou um procedimento membro duma classe for muito simples, é um bom candidato a ser inline, i.e., que a sua invocação resulte na substituição directa do código da função, em vez de se usar o mecanismo usual de invocação.  A razão para a parcimónia na utilização de funções e procedimentos inline é que podem conduzir a um aumento considerável do programa executável (depois da compilação).  No caso presente, como todas as operações realizadas sobre pilhas são triviais, todas as funções e procedimentos podem e devem ser definidos como inline.  Mas não o faça, em geral, duma forma arbitrária. Seleccione criteriosamente como candidatos a inline as funções e procedimentos membro que: a) possam vir a ser intensivamente usadas, b) sejam muito simples.

Para definir uma função ou procedimento membro como inline, podem-se fazer uma de duas coisas:

  1. Ao definir a classe definir logo a função ou procedimento membro (em vez de a declarar apenas).
  2. Ao definir a função ou procedimento membro declarada na definição da classe, preceder o seu cabeçalho do qualificador inline.
Assim, para definir como inline a função membro tamanho(), pode-se usar ou
class PilhaInt {
    ...
    int tamanho() {
        return quantos;
    }
    ...
}
ou, alternativamente,
class PilhaInt {
    ...
    int tamanho();
    ...
}
...
inline int PilhaInt::tamanho()
{
    return quantos;
}
Em geral a segunda alternativa é preferível à primeira, pois torna mais evidente a separação entre a interface e a implementação da classe.

Note-se que não só as funções ou procedimentos membro duma classe podem ser inline: qualquer função ou procedimento pode ser definido como inline, bastando para isso usar o qualificador inline.  Note-se ainda que a definição de uma função ou procedimento como inline não altera a semântica da sua chamada, tendo apenas consequências em termos da tradução, pelo compilador, para código máquina.

Nova versão do programa das pilhas

Usando os conceitos acima, pode-se escrever:
#include <iostream>
using namespace std;

const int limite = 100;

class PilhaInt {
    int valores[limite];
    int quantos;
  public:
    PilhaInt();      // construtor da classe.
    void põe(int v); // coloca v no topo da pilha.
    void tira();     // retira valor do topo da pilha.
    int topo();      // devolve valor no topo da pilha.
    bool vazia();    // devolve true sse a pilha estiver vazia.
    bool cheia();    // devolve true sse a pilha estiver cheia.
    int tamanho();   // devolve número de valores na pilha.
};

inline PilhaInt::PilhaInt()
{
    quantos = 0;
}
inline bool PilhaInt::vazia()
{
    return quantos == 0;
}

inline bool PilhaInt::cheia()
{
    return quantos == limite;
}

inline int PilhaInt::tamanho()
{
    return quantos;
}

inline void PilhaInt::põe(int v)
{
    if(cheia()) {
        cerr << "Erro: pilha cheia." << endl;
        exit(1);
    }
    valores[quantos++] = v;
}

inline void PilhaInt::tira()
{
    if(vazia()) {
        cerr << "Erro: pilha vazia." << endl;
        exit(1);
    }
    quantos--;
}

inline int PilhaInt::topo()
{
    if(vazia()) {
        cerr << "Erro: pilha vazia." << endl;
        exit(1);
    }
    return valores[quantos - 1];
}

int main()
{
    PilhaInt pilha;

    // Coloca 10 valores na pilha:
    for(int i = 0; i != 10; i++) {
        int v;
        cin >> v;
        pilha.põe(v);
    }

    // Escreve-os pela ordem inversa:
    while(!pilha.vazia()) {
        cout << pilha.topo() << endl;
        pilha.tira();
    }
}

6.4.2  Funções e procedimentos membro constantes

Para que as funcionalidades dos novos tipos sejam equivalentes às dos tipos básicos, é necessário que se possam definir constantes dos novos tipos.  Por exemplo, deve ser possível escrever:
const Racional um = 1;
const Racional zero = 0;
e de facto é-o!  Podem-se definir constantes de qualquer tipo definido pelo utilizador.  O único problema está na sua utilização.  Por exemplo:
Racional z = um + zero;
gera um erro de compilação.  Isto deve-se a que o compilador não pode adivinhar que a função membro operator +() da classe Racional não altera a instância que é passada implicitamente por referência, neste caso uma instância constante.  Como pode haver a possibilidade de a constante um ser alterada, o que é um contra-senso, o compilador simplesmente proíbe a expressão.   Relativamente à constante zero, sendo passada por valor para a função operator +(), o compilador não se queixa.

Para obviar a este comportamento, que impede a soma de uma constante com qualquer outro Racional, é necessário indicar ao compilador que a função membro operator +() é bem comportada, i.e., não altera a instância passada por referência implicitamente.  Para isso coloca-se o qualificador const após quer a declaração quer a definição da função membro:

class Racional {
    ...
 public:
    Racional operator + (Racional b) const;
    ...
};
...
Racional Racional::operator + (Racional b) const
{
    ...
}
Claro está que, se dentro duma função ou procedimento membro declarada como const se tentar alterar alguma variável membro, o compilador gerará uma mensagem de erro.

É uma boa regra declarar como const todas as funções e procedimentos membro que não efectem a instância implícita.  Assim, apresentam-se abaixo versões melhoradas dos programas desenvolvidos:

Nova versão do programa com racionais

(Aproveitou-se para definir como inline as funções membro mais simples.)
#include <iostream>
#include <cstdlib>
using namespace std;

// PC: n = n e m = m
// CO: (n = 0 e m = 0 en = 1) ou ((m <> 0 ou n <> 0) en = mdc(|m|, |n|))
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; // numerador
    int d; // denominador
  public:
    Racional(int num = 0, int den = 1);
    void escreve() const;
    Racional operator + (Racional b) const;
    bool operator == (Racional b) const;
    bool operator < (Racional b) const;
};

// Construtor com dois parâmetros (numerador e denominador):
Racional::Racional(int num, int den)
{
    if(den == 0) {
        cerr << "Erro: denominador inválido!" << endl;
        exit(1);
    } else if(den < 0) {
        n = -num;
        d = -den;
    } else {
        n = num;
        d = den;
    }
    int k = mdc(n, d);
    n /= k;
    d /= k;
}

// PC:
// CO: aparece no ecrã a representação fraccionária do racional *this, sendo *this
//    a instância da classe implícita na execução da função.
inline void Racional::escreve() const
{
    cout << n << '/' << d;
}

// PC:
// CO: r = *this + b, sendo *this a instância da classe implícita na execução
//    da função.
Racional Racional::operator + (Racional b) const
{
    int l = mdc(n, b.n);
    int k = mdc(d, b.d);
    int termo = (n / l) * (b.d / k) + (b.n / l) * (d / k);
    int m = mdc(termo, k);
    Racional r;
    r.n = (termo / m) * l;
    r.d = (k / m) * (d / k) * (b.d / k);
    return r;
}

// PC:
// CO: devolve valor lógico de *this = b, sendo *this a instância da classe implícita
//        na execução da função.
inline bool Racional::operator == (Racional b) const
{
    return n == b.n && d == b.d;
}

// PC:
// CO: devolve valor lógico de *this > b, sendo *this a instância da classe implícita
//        na execução da função.
bool Racional::operator < (Racional b) const
{
    int l = mdc(n, b.n);
    int k = mdc(d, b.d);
    return (n / l) * (b.d / k) > (b.n / l) * (d / k);
}

int main()
{
    Racional x = 1, y(7, 15);
    Racional z = x + y;
    z.escreve();
    cout << endl;
    if(x < y)
        cout << "menor" << endl;
    else
        cout << "maior ou igual" << endl;
    if(x == y)
        cout << "igual" << endl;
    else
        cout << "diferente" << endl;
}

Nova versão do programa com pilhas

(Aproveitando para fazer uso da função membro tamanho() nas outras funções membro.)
#include <iostream>
using namespace std;

const int limite = 100;

class PilhaInt {
    int valores[limite];
    int quantos;
  public:
    PilhaInt();          // construtor da classe.
    void põe(int v);     // coloca v no topo da pilha.
    void tira();         // retira valor do topo da pilha.
    int topo() const;    // devolve valor no topo da pilha.
    bool vazia() const;  // devolve true sse a pilha estiver vazia.
    bool cheia() const;  // devolve true sse a pilha estiver cheia.
    int tamanho() const; // devolve número de valores na pilha.
};

inline PilhaInt::PilhaInt()
{
    quantos = 0;
}
inline int PilhaInt::tamanho() const
{
    return quantos;
}

inline bool PilhaInt::vazia() const
{
    return tamanho() == 0;
}

inline bool PilhaInt::cheia() const
{
    return tamanho() == limite;
}

inline void PilhaInt::põe(int v)
{
    if(cheia()) {
        cerr << "Erro: pilha cheia." << endl;
        exit(1);
    }
    valores[quantos++] = v;
}

inline void PilhaInt::tira()
{
    if(vazia()) {
        cerr << "Erro: pilha vazia." << endl;
        exit(1);
    }
    quantos--;
}

inline int PilhaInt::topo() const
{
    if(vazia()) {
        cerr << "Erro: pilha vazia." << endl;
        exit(1);
    }
    return valores[tamanho() - 1];
}

int main()
{
    PilhaInt pilha;

    // Coloca 10 valores na pilha:
    for(int i = 0; i != 10; i++) {
        int v;
        cin >> v;
        pilha.põe(v);
    }

    // Escreve-os pela ordem inversa:
    while(!pilha.vazia()) {
        cout << pilha.topo() << endl;
        pilha.tira();
    }
}

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 membro 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 resoluçã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 construtor por omissão que invoca os construtores por omissão de 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 construtor 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 construtor 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.  Crie um novo tipo em C++ que concretize a noção de fila (de espera) de valores int.  Ao contrário do que se passa para no caso das pilhas, o primeiro valor a entrar numa fila é também o primeiro valor a sair.  Use a metodologia sugerida na Secção 6.3 e os conceitos sobre classes em C++ aprendidos nas Secções  6.2 e 6.4.  Pense seriamente como usar matrizes para implementar o conceito antes de passar à implementação propriamente dita.

2.  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.

3.  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.

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

6.5  Referências

[1]  Ronald L. Graham, Donald E. Knuth e Oren Patashnik, "Concrete Mathematics: A Foundation for Computer Science", segunda edição, Addison-Wesley, 1994.

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