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:
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 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 / lComo é óbvio, estas operações garantem que mdc(d'1, d'2) = 1.
n'2 = n2 / l
d'1 = d1 / k
d'2 = d2 / k
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++).
int incrementa(int& x)é um procedimento que incrementa a variável passado como argumento. Este procedimento pode ser usado como no programa que se segue
{
x++;
return x;
}
#include <iostream>que imprime no ecrã o valor incrementado de y, isto é, 2.
using namespace std;int incrementa(int& x)
{
x++;
return x;
}int main()
{
int y = 1;
incrementa(y);
cout << y << endl;
}
Mas o conceito de referência pode ser usado de formas diferentes. Por exemplo,
int i = 1;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.
int& j = i; // a partir daqui j é sinónimo de i
j = 3;
cout << i << endl;
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().
#include <iostream>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 é
using namespace std;int incrementa(int& x)
{
x++;
return x;
}int main()
{
int y = 1;
incrementa(incrementa(y)); // erro!
cout << y << endl;
}
#include <iostream>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).
using namespace std;int& incrementa(int& x)
{
x++;
return x;
}int main()
{
int y = 1;
incrementa(incrementa(y));
cout << y << endl;
}
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;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:
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.
#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.
}
n/d + 1 = (n + d)/dem 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 {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:
...
public:
...
Racional& operator ++ ();
};...
inline Racional& operator ++() {
n += d;
return *this;
}
Racional r = 1;que, de facto, incrementa a variável r duas vezes.
++ ++r;
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 {Como é óbvio, tendo-se devolvido um valor em vez de uma referência, não é possível escrever
...
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;
}
Racional r;que de resto já era uma construção inválida no caso dos tipos básicos do C++.r++ ++; // erro!
#include <vector>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 comoint soma(std::vector<int> v)
{
int s = 0;
for(int i = 0; i != v.size(); i++)
s += v[i];
return s;
}
#include <vector>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:int soma(std::vector<int>& v) // má ideia!
{
int s = 0;
for(int i = 0; i != v.size(); i++)
s += v[i];
return s;
}
#include <vector>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: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;
}
// Mau código. Bom para exemplos apenas...* 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.
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!
}
#include <iostream>Note-se que os construtores se declaram e definem como os construtores, excepto que se coloca ~ antes do nome da classe.
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.
}
A execução deste programa resulta em:
Construí 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.
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!
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 {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.
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;
};
Uma possível utilização da classe seria
#include <iostream>Note-se que a invocação da função membro de classe númeroDeInstâncias() se faz não através do operador de selecção de membro (.), o que implicaria a invocação da função através de uma qualquer instância da classe, mas através do operador de rasolução de âmbito ::. No entanto, é possível invocar funções (ou procedimentos) membro de classe através do operador de selecção de membro. As mesmas observações se podem fazer no que diz respeito às variáveis membro de classe.
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;
}
A execução do programa acima teria como resultado:
Existem 3 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.
Existem 3 instâncias.
Existem 3 instâncias.
Existem 2 instâncias.
Existem 0 instâncias.
Racional r(1, 3);tivesse como resultado:
cout << r << endl;
1/3Para 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) {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:
saída << r.n << '/' << r.d;
return ???;
}
class Racional {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.
...
friend ??? operator << (ostream& saída, Racional r);
...
};...
inline ??? operator << (ostream& saída, Racional r) {
saída << r.n << '/' << r.d;
return ???;
}
inline ostream& operator << (ostream& saída, Racional r) {ou simplesmente (porquê?)
saída << r.n << '/' << r.d;
return saída;
}
inline ostream& operator << (ostream& saída, Racional r) {* 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!
return saída << r.n << '/' << r.d;
}
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 {em que não houve necessidade de declarar qualquer função como amiga da classe 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();
}
A definição das duas novas funções membro da classe Racional trará outras consequências benéficas, como se verá mais abaixo.
class C {é criado implicitamente um contrutor por omissão que invoca os construtores por omissão da todas as variáveis membro, com excepção das pertencentes a tipos básicos do C++ que, infelizmente, não são inicializadas implicitamente. Esse construtor por omissão criado implicitamente pode ser invocado explicitamente:
Racional r1;
Racional r2;
int i;
};C c; // nova instância, construtor por omissão invocado.
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 {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:
Racional r1;
Racional r2;
int i;
public:
C() : r1(), r2() {}
};
class 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
Racional r1;
Racional r2;
int i;
public:
C() {
}
};
class C {ao ser criada a variável c é invocado o seu construtor, o que resultará nas seguintes operações:
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);
Note-se que a classe C não tem construtor por omissão. Desse modo, seria um erro escrever:
C c; // erro!
Racional m1[10] = {Racional()};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.
Racional m2[10] = {1, 2, Racional(3), Racional(1, 3)};
Note-se que, se a classe em causa não possuir construtores por omissão, é obrigatório inicializar todos os elementos da matriz explicitamente, e também que as conversões podem precisar de ser explicitadas Por exemplo:
class C {
int valor;
public:
// Construtor explícito inline que inicializa valor com conteúdo de i:
explicit C(int i) : valor(i) {}
};
C matriz[5]; // erro! não tem contrutor por omissão.C matriz[5] = {C(1), C(2), C(3)}; // erro! inicializadores insuficientes.
C matriz[5] = {C(1), 2, 3, 4, 5}; // erro! conversão implícita impossível.
C matriz[5] = {C(1), C(2), C(3), C(4), C(5)}; // ok!
Racional r(1, 3);sem haver necessidade de explicitar essa conversão:
...
if(r < 1) // 1 convertido inplicitamente de int para Racional.
Racional r(1, 3);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?
...
if(r < Racional(1)) // não é necessário...
Racional r(1, 2);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:
double x = r;
cout << r << ' ' << x << endl; // imprime '1/2 0.5'.
cout << double(r) << endl; // imprime '0.5'.
class Racional {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...
...
public:
...
operator double() const;
...
};...
Racional::operator double() const {
return double(numerador()) / double(denominador());
}
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);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.
...
if(r == 1)
...
class Palavra {O que tornaria possível escrever código como abaixo:
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;
}
Palavra p = 99996;que resultaria em
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;
Valor é: 100000Claro que esta classe, para ser verdadeiramente útil, deveria proporcionar outras operações, que ficam como exercício para o leitor.
Em binário: 00000000000000011000011010100000
#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;
}
2. Refaça a classe PilhaInt de modo a usar um vector<int> em vez de uma matriz (array) de int. Flexibilize a classe permitindo ao utilizador especificar o tamanho máximo da pilha aquando da sua criação. Veja a Secção 7.1.
3. Refaça o exercício anterior de modo a não se ter de impor qualquer limite às pilhas criadas.