(Continuação do resumo anterior.)
É possível usar as operações de uma classe para sobrecarregar operadores.
Como rotina normal:
Como operação:
tipo_devolvido operator nome_operador(lista_de_parâmetros);
Quando a sobrecarga de um operador é feita por intermédio de uma operação de uma classe, o primeiro operando deste operador é a instância da classe que está implícita durante a invocação do operador. Assim, não existe parâmetro correspondente na declaração/definição do operador.
class nome_da_classe {
public:
...
tipo_devolvido operator nome_operador(lista_de_parâmetros);
...
};
Definição da classe Racional
e declaração
de alguns operadores:
/**
Representa números racionais.@invariant 0 <
denominador_
e mdc(numerador_
,denominador_
) = 1.*/
class Racional {public:
Constrói um novo racional com valor
/**numerador
/1.
@pre V.
@post
Racional
=numerador
.*/
Racional(int const numerador = 0);
/**
Constrói um novo racional com valornumerodor
/denominador
.
@pre V.
@post
Racional
=numerador
/denominador
.*/
Racional(int const numerador, int const denominador);
/**
Devolve o numerador do racional.
@pre V.
@post
numerador
= numerador do racional.*/
int numerador();
/**
Devolve o denominador do racional.
@pre V.
@post
denominador
= denominador do racional.*/
int denominador();
...
//
outros membros públicos da classeRacional
.
/**
Devolve a soma do racional com o racional recebido como argumento.
@pre V.
@post
operator+
= soma do racional comoutro_racional
.*/
Repare-se que este operador tem apenas um parâmetro...
Racional const operator+(Racional const outro_racional);
//
private:
int numerador_;
int denominador_;
/**
Reduz a representação do racional, que mantém o seu valor.
@pre
numerador_
= n edenominador_
= d e 0 <denominador_
.@post 0 <
denominador_
e mdc(numerador_
,denominador_
) = 1 e
numerador_
/denominador
= n/d.*/
void reduz();};
int Racional::numerador()
{
return numerador_;
}
int Racional::denominador()
{
return denominador_;
}
Qualquer operador que tenha como primeiro operando uma instância de uma classe pode ser declarado como operação dessa classe. Exemplo: os operadores//
...enquanto este operador tem dois argumentos, apesar de ambos serem
//
utilizados da mesma forma.
Racional const operator-(Racional const um_racional,
Racional const outro_racional);
//
Repare-se que este operador não pode ser membro da classeRacional
porque o primeiro operando não é um
//Racional
.
ostream& operator<<(ostream& out, Racional const um_racional);
+
e -
declarados acima poderiam ser ambos operações da classe.
Qualquer operador
que não tenha como primeiro operando uma instância de uma
classe não pode ser declarado como operação dessa classe. Exemplo:
o operador de inserção <<
declarado acima.
Definição dos operadores:
Tal como para os métodos usuais, a definição dos operadores membro precisa de indicar a sua pertença à classe (através do prefixo
Racional const Racional::operator+(Racional const outro_racional)
{
Racional resultado(numerador() * outro_racional.denominador() +
outro_racional.numerador() * denominador(),
denominador() * outro_racional.denominador());
return resultado;
/*
ou simplesmente:return Racional(numerador() * outro_racional.denominador() +
outro_racional.numerador() * denominador(),
denominador() * outro_racional.denominador());
*/
}
Racional::
).Os operadores membro têm acesso aos membros privados da classe, enquanto os não membro têm de utilizar os membros públicos da classe, tal como acontece, de resto, com quaisquer rotinas. No entanto, é boa ideia recorrer a outras operações da classe para obter os valores do numerador e do denominador, em vez de aceder a eles directamente.
Racional const operator-(Racional const um_racional, Racional const outro_racional)
{
return Racional(um_racional.numerador() * outro_racional.denominador() -
outro_racional.numerador() * um_racional.denominador(),
um_racional.denominador() * outro_racional.denominador());
}
Utilização:ostream& operator<<(ostream& saída, Racional const um_racional)
{
saída << um_racional.numerador();
if(um_racional.denominador() != 1)
saída << '/' << um_racional.denominador();
return saída;
}
Racional a(1, 3), b(2, 3), d, e;
Racional d = a + b;
Racional e = a - b;
cout << d << endl;
cout << e << endl;
cout << b - a << endl;
Os construtores que podem ser invocados com apenas um argumento definem (por omissão) uma conversão implícita de tipos. Por exemplo, o primeiro construtor da classe
Racional r;
r = Racional(1, 3);
Racional
pode ser
chamado com apenas um argumento do tipo int
. Assim, sempre
que esperar um Racional
e encontrar um int
, o compilador
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
tendo o mesmo significado que
Racional x(1, 3);
Racional z = x + 1;
que coloca em
Racional x(1, 3);
Racional z = x + Racional(1);
z
o racional 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
.
Se a classe Racional
estivesse definida como
o compilador assinalaria erro ao encontrar a expressão...
class Racional {
...
public:
...
explicit Racional(int const numerador = 0);
...
};
x + 1
.
Neste caso, no entanto, a conversão implícita de int
para Racional
é útil, pelo que o qualificador explicit
não é desejável.Racional
, a condição invariante
de indica que os racionais estão sempre representados por uma fracção
em termos mínimos (i.e., que mdc(numerador
, denominador
)
= 1) e que o denominador é sempre positivo (i.e., que denominador
> 0):
Neste caso a condição invariante de classe tem a vantagem de garantir que não apenas a cada fracção corresponde um único racional, mas também que a cada racional corresponde uma única fracçãoCIC: 0 <
denominador
e mdc(numerador
,denominador
) = 1.
numerador_
/denominador_
.
I.e., a representação escolhida é única.
Por exemplo, os racionais definidos por:
são todos representados pelos atributos
Racional r1(-2, 6);
Racional r2(4, -12);
Racional r3(-1, 3);
numerador_
= -1
e denominador_
= 3, respectivamente.São os construtores da classe que se encarregam de garantir que os atributos da instância construída cumprem inicialmente a condição invariante de classe. Todas as outras operações públicas da classe
Racional::reduz()
)
podem agir sobre instâncias da classe que não cumprem a condição invariante
de classe, e podem, se isso for do interesse do programador produtor,
não garantir o seu cumprimento quando terminam. A razão é simples: as
operações privadas são auxiliares, fazendo parte da implementação, e não
da interface, da classe.
É possível definir uma operação auxiliar para a classe com o objectivo de devolver a veracidade da condição invariante de classe e usá-la para verificar essa condição para todas as instâncias envolvidas num método correspondente a uma operação pública, quer no seu início, quer no seu fim:
...
class Racional {
...
private:
...
/**
Indica se a instância cumpre a condição invariante de classe.@pre V.
@post
cumpreInvariante =
0 <denominador
e
mdc(
numerador
,denominador
) = 1.*/
bool cumpreInvariante();};
...
bool Racional::cumpreInvariante()
{
return 0 < denominador_ and mdc(numerador_, denominador_) == 1;
}
...
Racional const Racional::operator+(Racional const outro_racional)
{
assert(cumpreInvariante() and
outro_racional.cumpreInvariante());
Racional resultado(numerador() * outro_racional.denominador() +
outro_racional.numerador() * denominador(),
denominador() * outro_racional.denominador());
assert(cumpreInvariante() and
resultado.cumpreInvariante())
return resultado;
}
...
Racional
:
Os atributos devem surgir na lista pela mesma ordem pela qual são definidos na classe. Neste caso o numerador é inicializado com o valor do parâmetro
Racional::Racional(int const numerador)
: numerador_(numerador), denominador_(1)
{
assert(cumpreInvariante());
}
numerador
e o denominador com 1.No primeiro caso é devolvida uma variável temporária (e sem nome, dita anónima) que é uma cópia da variável
int cópia(int& n)
{
return n;
}
int& mesmo(int& n)
{
return n;
}
n
. No segundo é devolvida uma
referência para a variável
n
, ou seja, um seu sinónimo. Ambas as funções
se podem usar como se segue:
ficando as variáveis
int i = 11;
int j = cópia(i);
int k = mesmo(i);
j
e k
com o valor 11.
Mas só a segunda se pode usar do lado esquerdo de uma atribuição:
pois a variável devolvida pela primeira função é temporária, não fazendo sentido atribuir-lhe um novo valor. Ainda que fosse possível, a variável
int i = 11;
erro!
cópia(i) = 15;//mesmo(i) = 15; //
ok! coloca 15 emi
.
i
não seria afectada,
pois a função cópia()
devolve uma cópia
de
i
. A função mesmo()
devolve um
sinónimo de i
, pelo que ao atribuir-lhe 15 está-se
a afectar i
.Racional::denominador()
invocada no código abaixo
o nome
Racional r(1, 3);
cout << r.denominador() << endl;
denominador_
refere-se à variável membro
do mesmo nome da instância implícita, que neste caso
é a variável
r
.
É possível referir explicitamente a instância implícita
(completa) através da construção *this
.
Por exemplo, o operador += para os racionais pode-se definir como:
A devolução de uma referência para a instância implícita, que neste caso é o primeiro operando do operador...
class Racional {
...
/**
Adiciona o racional recebido como argumento à instância
implícita, devolvendo-a por referência.
@pre
*this
= r.@post
*this
= r +outro_racional
eoperator+=
idêntico a*this
.*/
Racional& operator+=(Racional const outro_racional);
...
};
Racional& Racional::operator +=(Racional const outro_racional)
{
assert(cumpreInvariante() and outro_racional.cumpreInvariante());
numerador_ = numerador() * outro_racional.denominador() +
outro_racional.numerador() * denominador();
denominador_ *= outro_racional.denominador();
reduz();
assert(cumpreInvariante());
return *this;
}
+=
,
permite que este operador seja invocado em sequência:
o que coloca ½ + ¼ + ¾ = 3/2 em
Racional r(1, 2), s(1, 4), t(3, 4),
r += s += t;
r
.
Note-se que, depois de definido este operador, o operador de soma simples pode ser definido fora da classe, como uma rotina normal e de forma muito simples:
Racional const operator+(Racional um_racional, Racional const outro_racional)
{
um_racional += outro_racional; // um_racional
é passado por valor, a alteração não//
afecta o argumento!return um_racional;
//
ou simplesmentereturn um_racional += outro_racional;
}
Como as passagens por valor exigem a inicialização por cópia dos parâmetros a partir dos argumentos respectivos, o que pode ser uma operação dispendiosa em tempo e memória, é comum usarem-se passagens de argumentos por referência constante em vez de passagens por valor. Por exemplo:
int i = 10;
int const& rci = i; //
as referências têm de ser sempre inicializadas!int& ri = i;
ri = 12; //
alterai
.rci = 13; //
erro de compilação!
int const j = 14;
int& rj = j; //
são proibidas referências não constantes para uma constante!
...
class Racional {
...
Racional& operator+=(Racional const& outro_racional);
...
};
...
Racional& Racional::operator +=(Racional const& outro_racional)
{
assert(cumpreInvariante() and outro_racional.cumpreInvariante());
numerador_ = numerador() * outro_racional.denominador() +
outro_racional.numerador() * denominador();
denominador_ *= outro_racional.denominador();
reduz();
assert(cumpreInvariante());
return *this;
}
Racional const operator+(Racional um_racional, Racional const& outro_racional)
{
//
Note-se queum_racional
continua passada por valor, para ser uma cópia e poder ser//
alterado localmente!return um_racional += outro_racional;
}
Como é evidente, o C++ deve proibir qualquer tentativa de alterar uma constante. Mas toma uma atitude porventura demasiado conservadora a esse respeito: toda e qualquer operação é suspeita de alterar a instância para a qual é invocada, excepto se declarada explicitamente como constante. Por exemplo, o código
Racional const um = 1; //
o mesmo queum(1);
gera um erro de compilação, pois o compilador admite que a operação
cout << um.denominador() << endl;
Racional::denominador()
altera potencialmente a constante
um
.
Para deixar bem claro que isso não acontece, a operação tem de ser declarada, e
o respectivo método definido, com o qualificador const
,
que se coloca logo após o cabeçalho. Ou seja:
...
class Racional {
...
int denominador() const;
...
};
...
int Racional::denominador() const
{
return denominador_;
}