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.
class Aluno {A sintaxe de definição de um tipo é, portanto
public:
string nome;
int número;
};
class nome_do_tipo {sendo importante notar que este é um dos poucos locais onde o C++ exige um ; depois da } final.
declaração_de_membros
};
Aluno aluno1;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.aluno1.nome = "Zacarias Zagalo";
aluno1.número = 123;
É importante perceber que cada variável do tipo Aluno possui as suas próprias variáveis membro. Por exemplo:
Aluno aluno1;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:aluno1.nome = "Zacarias Zagalo";
aluno1.número = 123;Aluno aluno2;
aluno2.nome = "Xisto Ximenes";
aluno1.número = 321;
int número = 1000;sem que isso causasse qualquer problema.
Aluno aluno1;aluno1.nome = "Zacarias Zagalo";
aluno1.número = 123;Aluno aluno2;
aluno2.nome = "Xisto Ximenes";
aluno1.número = 321;
* Em rigor, o ponto (.) é o operador de selecção de membro, como se pode ver na Tabela Precedência de operadores.
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.
Aluno 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++.aluno1.nome = "Zacarias Zagalo";
aluno1.número = 123;Aluno aluno2;
aluno2 = aluno1;
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].
n1/d1 + n2/d2 = (n1d2 + n2d1)/(d1d2)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.
n1/d1 - n2/d2 = (n1d2 - n2d1)/(d1d2)
n1/d1 * n2/d2 = (n1n2)/(d1d2)
n1/d1 / n2/d2 = (n1d2)/(n2d1)
n1/d1 + n2/d2 = (l * (n'1d'2 + n'2d'1))/(kd'1d'2)onde
k d'1 = d1Mas 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:
k d'2 = d2
l n'1 = n1
l n'2 = n2
1/10 + 1/15em 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.
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.
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.
n1/d1 > n2/d2 <=> n1d2 > n2d1pois ambos os denominadores são positivos.
Podem-se reduzir os valores a comparar a
n'1d'2 > n'2d'1sendo 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.
class Racional {É 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.
public:
int n; // numerador
int d; // denominador
};
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)(Por enquanto não se garante a canonicidade do resultado.)
{
Racional r;
r.n = b.d * a.n + a.d * b.n;
r.d = a.d * b.d;
return r;
}
Uma possível utilização seria:
#include <iostream>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.
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;
}
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 {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.
private:
int n; // numerador
int d; // denominador
};
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ã?
class Racional {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?
private:
int n; // numerador
int d; // denominador
public:
void escreve();
};
Em primeiro lugar, lembre-se que o acesso aos membros duma classe se faz usando a notação
variável . nome_do_membroem 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()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
{
cout << n << '/' << d;
}
x.escreve();as variáveis n e d referem-se sucessivamente aos membros de x, y, e z.
y.escreve();
z.escreve();
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)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:
{
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;
}
class Racional {
private:
int n; // numerador
int d; // denominador
public:
void escreve();
Racional soma(Racional b);
};
// PC:A utilização desta nova função membro faz-se como anteriormente em relação à função escreve():
// 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()É 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;.
{
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;
}
Idealmente seria desejável inicializar essas variáveis usando as formas usuais do C++. Por exemplo, se se pode escrever
int i = 1;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:
int j(10);
Racional a = 1;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:
Racional b(7, 15);
class Racional {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,
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;
}
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;
}
#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;
}
}
#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()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
{
Racional x = 1, y(7, 15);
Racional z = x.soma(y);
z.escreve();
cout << endl;
}
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.
#include <iostream>ou, ainda mais simples,
#include "racional"int main()
{
Racional x = 1, y(7, 15);
Racional z = x + y;
cout << z << endl;
}
#include <iostream>
#include "racional"int main()
{
cout << Racional(1) + Racional(7, 15) << endl;
}
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.
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()No ecrã deveria aparecer
{
Racional x(1/50000), y(1/50000);
Racional z = x.soma(y);
z.escreve();
cout << endl;
}
1/25000mas 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.
// PC: m >= 0 e n > 0 e m = m e n = 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:
// CO: n = mdc(m, n)
int mdc(int m, int n)
{
while(m != 0) {
int auxiliar = n % m;
n = m;
m = auxiliar;
}
return n;
}
// PC: m >= 0 e n >= 0 e (m <> 0 ou n <> 0) em = m e n = nMas 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:
// CO: n = mdc(m, n)
int mdc(int m, int n)
{
while(m != 0) {
int auxiliar = n % m;
n = m;
m = auxiliar;
}
return n;
}
// 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;
}
// PC: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:
// 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:Uma função para verificar da igualdade poderia ser mais simples, dada a certeza da canonicidade de representação das fracções:
// 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);
}
// 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;
}
// 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;
}
#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:Que, depois de executado, produz no ecrã:
// 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;
}
22/15
maior
diferente
int main()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++.
{
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;
}
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: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).
// 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;
}
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:
Racional r;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!
r = 1/3;
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);
Racional x(1, 3);tendo o mesmo significado que
Racional z = x + 1;
Racional x(1, 3);que por sua vez, tendo em conta os valores por omissão dos parâmetros do construtor dos Racional, significa o mesmo que
Racional z = x + Racional(1);
Racional x(1, 3);que coloca em z o racional (4, 3) (ou 4/3).
Racional z = x + Racional(1, 1);
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 {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.
...
public:
explicit Racional(int num = 0, int den = 1);
...
};
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.
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:
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.
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.
};
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;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: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.
};
const int limite = 100;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.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.
};
É 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()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).
{
quantos = 0;
}
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()A função membro tamanho() é trivial:
{
return quantos == 0;
}bool PilhaInt::cheia()
{
return quantos == limite;
}
int PilhaInt::tamanho()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:
{
return quantos;
}
void PilhaInt::põe(int v)É de notar a utilização típica do operador ++ no procedimento membro poe(). O código é equivalente a
{
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];
}
void PilhaInt::põe(int v)mas apenas porque se usou o operador ++ sufixo! A utilização do operador ++ prefixo teria um significado diferente e conduziria a um erro, pois
{
if(cheia()) {
...
}
valores[quantos] = v;
quantos = quantos + 1;
}
valores[++quantos] = v;é o mesmo que
quantos = quantos + 1;
valores[quantos] = v;
class PilhaInt {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:
const int limite = 100;// erro!
int valores[limite];
....
}
class Aluno {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).
const int número;
int nota;
...
public:
Aluno(int n);
};Aluno::Aluno(int n) : número(n) {
...
}
#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();
}
}
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++).
Para definir uma função ou procedimento membro como inline, podem-se fazer uma de duas coisas:
class PilhaInt {ou, alternativamente,
...
int tamanho() {
return quantos;
}
...
}
class PilhaInt {Em geral a segunda alternativa é preferível à primeira, pois torna mais evidente a separação entre a interface e a implementação da classe.
...
int tamanho();
...
}
...
inline int PilhaInt::tamanho()
{
return quantos;
}
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.
#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();
}
}
const Racional um = 1;e de facto é-o! Podem-se definir constantes de qualquer tipo definido pelo utilizador. O único problema está na sua utilização. Por exemplo:
const Racional zero = 0;
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 {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.
...
public:
Racional operator + (Racional b) const;
...
};
...
Racional Racional::operator + (Racional b) const
{
...
}
É 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:
#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;
}
#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();
}
}
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 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.
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 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:
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 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!
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. 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.
[2] Bjarne Stroustrup, "The C++ Programming Language", terceira edição, Addison-Wesley, Reading, Massachusetts, 1998.