Jiraspiom

Blog Pessoal

Lição 12

Lição 12 – Programação Orientada a Objeto – continuação

POO – continuação


Sobrecarregando Operadores

Em C++ podemos usar operadores padrão para realizar operações com classes em adição a operações com tipos fundamentais. Veja o seguinte código:

struct {
string product;
float price;
} a, b, c;
a = b + c;


Fazermos isto causaria um erro de compilação, já que não definimos o comportamento que nossa classe deve ter com operações de adição. Entretanto, graças ao uso de operadores sobrecarregados (overloaded operators) do C++, podemos criar classes capazes de realizar operações usando operadores padrão. A seguir está uma lista de todos os operadores que podem ser sobrecarregados:

Overloadable operators

+ – * / = < > += -= *= /= << >>
<<= >>= == != <= >= ++ — % & ^ ! |
~ &= ^= |= && || %= [] () , ->* -> new
delete new[] delete[]

Para sobrecarregar um operador de maneira que os usemos com classes, declaramos funções de operadores, que são funções comuns cujos nomes são a palavra-chave do operador seguida pelo símbolo do operador que queremos sobrecarregar. Desta maneira:

[tipo] [operador] [simbolo] (parametros) { /*…*/ }


A seguir temos um exemplo que sobrecarrega o operador de adição (+). Criaremos uma classe para armazenar vetores bidimensionais e então iremos adicionar dois deles: a(3,1) e b(1,2). Para fazermos a adição de dois vetores bidimensionais adicionamos as duas coordenadas em x para obter a coordenada resultante x e adicionamos as duas coordenadas em y para obter o y resultante. Neste caso, o resultado será (3+1,1+2) = (4,3).

#include <iostream>
using namespace std;

class Vetor {
public:
int x,y;
Vetor () {};
Vetor (int,int);
Vetor operador+ (Vetor);
};

Vetor::Vetor (int a, int b) {
x = a;
y = b;
}

Vetor Vetor::operador+ (Vetor param) {
Vetor temp;
temp.x = x + param.x;
temp.y = y + param.y;
return (temp);
}

int main () {
Vetor a (3,1);
Vetor b (1,2);
Vetor c;
c = a + b;
cout << c.x << “,” << c.y;
return 0;
}


, o que resulta:

4,3


Deve ser um pouco confuso ver o identificador Vetor tantas vezes no código, mas considere que alguns deles se referem ao nome da classe (tipo) Vetor e alguns são funções com aquele nome, já que construtores devem ter o mesmo nome da classe. Não os confunda:

Vetor (int, int); // nome da função Vetor (construtor)
Vetor operador+ (Vetor ); // função retorna um Vetor


A função operador+ da classe Vetor é quem está a cargo de sobrecarregar o operador de adição (+). Esta função pode ser chamada tanto implicitamente usando o operador ou explicitamente, usando o nome da função. Ambas as expressões são equivalentes:

c = a + b;
c = a.operador+ (b);


Ainda naquele exemplo anterior, note também que incluímos o construtor vazio (sem parâmetros) e o definimos com um bloco vazio:

Vetor () { };


Isto é necessário, já que declaramos explicitamente outro construtor:

Vetor (int, int);


Deve ser um pouco confuso ver o identificador Vetor tantas vezes no código, mas considere que alguns deles se referem ao nome da classe (tipo) Vetor e alguns são funções com aquele nome, já que construtores devem ter o mesmo nome da classe. Não os confunda:

Vetor (int, int); // nome da função Vetor (construtor)
Vetor operador+ (Vetor ); // função retorna um Vetor


A função operador+ da classe Vetor é quem está a cargo de sobrecarregar o operador de adição (+). Esta função pode ser chamada tanto implicitamente usando o operador ou explicitamente, usando o nome da função. Ambas as expressões são equivalentes:

c = a + b;
c = a.operador+ (b);


Ainda naquele exemplo anterior, note também que incluímos o construtor vazio (sem parâmetros) e o definimos com um bloco vazio:

Vetor () { };


Isto é necessário, já que declaramos explicitamente outro construtor:

Vetor (int, int);

E quando declaramos explicitamente algum construtor, com qualquer número de parâmetros, o construtor padrão sem parâmetros que o compilador pode declarar automaticamente não é declarado, então nós mesmos precisamos declará-los de maneira que possamos construir objetos deste tipo sem parâmetros. Senão, a declaração:

Vetor c;

incluida no main() não seria válida.

De qualquer modo, temos que frisar que um bloco vazio é uma má implementação para um construtor, já que não preenche a funcionalidade mínima que geralmente é esperada de um construtor, que é a inicialização de todas as variáveis membro em sua classe. No nosso caso, este construtor deixa x e y indefinidas. Para isso, uma definição mais aconselhável seria algo assim:

Vetor () { x=0; y=0; };


que, para simplificar e mostrar apenas o ponto do código, não mostramos no exemplo.

Da mesma maneira que uma classe inclui um construtor padrão e um construtor cópia mesmo que eles não são declarados, também inclui uma definição padrão para o operador de atribuição (=) com a classe em si como parâmetro. O comportamento que é definido por default é copiar o conteúdo inteiro dos dados membro do objeto passado como argumento (à direita do sinal) para o do lado esquerdo.

Vetor d (2,3);
Vetor e;
e = d; // operador de atribuição cópia

A função operador de atribuição cópia é a única função operador membro implementada por default. Claro que podemos redefini-las para qualquer funcionalidade que queiramos, como por exemplo, copia apenas certas classes membro ou realizar procedimentos de inicialização adicionais.

A sobrecarga de operadores não força sua operação a suportar uma relação de sentido matemático ou usual do operador, apesar de ser recomendado. Por exemplo, o código não deve ser muito intuitivo se usarmos o operador + para subtrair duas classes ou operador== para preencher com zeros uma classe, apesar de ser perfeitamente possível fazer isto.

Apesar de o protótipo de uma função operador+ poder parecer óbvio já que ele toma o que está do lado direito do operador como parâmetro para a função operador membro do objeto do lado esquerdo, outras operações podem não ser tão óbvias. Aqui temos uma tabela com um sumário de como as diferentes funções operadores membro têm de ser declaradas (troque @ pelo operador em cada caso):

Expressão Operador Função membro Função Global
@a + – * & ! ~ ++ — A::operador@() operador@(A)
a@ ++ — A::operador@(int) operador@(A,int)
a@b + – * / % ^ & | < > == != <= >= << >> && || , A::operador@ (B) operador@(A,B)
a@b = += -= *= /= %= ^= &= |= <<= >>= [] A::operador@ (B) –
a(b, c…) () A::operador() (B, C…) –
a->x -> A::operador->() –


Onde a é um objeto da classe A, b é um objeto da classe C e c é um objeto da classe C.

Como podemos ver, existem duas maneiras de sobrecarregar alguns operadores de classe: como uma função membro e como uma função global. Seu uso é indistinto, todavia devo ressaltar que funções que não são membros de uma classe não podem acessar membros private ou protected daquela classe a menos que a função global seja seu friend (friendship será explicada na lição a seguir).

Friendship e Herança

Funções Friend

A princípio, membros private e protected de uma classe não podem ser acessados de fora de uma mesma classe na qual são declaradas. Entretanto, esta regra não afeta friends. Se quisermos declarar uma função externa a uma classe como friend, deste modo permitindo que esta função acesse membros private e protected desta classe, declaramos um protótipo desta função externa na classe, e a precedemos com a palavra chave friend, como a seguir:

#include <iostream>
using namespace std;

class Retangulo {
int comprimento, altura;
public:
void seta_valores (int, int);
int area () {return (comprimento * altura);}
friend Retangulo copia (Retangulo);
};

void Retangulo::seta_valores (int a, int b) {
comprimento = a;
altura = b;
}

Retangulo copia (Retangulo retangparam)
{
Retangulo retangres;
retangres.comprimento = retangparam.comprimento*2;
retangres.altura = retangparam.altura*2;
return (retangres);
}

int main () {
Retangulo retang, retangb;
retang.seta_valores (2,3);
retangb = copia (retang);
cout << retangb.area();
return 0;
}

, o que retorna:

24

A função copia é friend de Retangulo. Daquela função, somos também capazes de acessar os membros comprimento e altura de objetos diferentes do tipo Retangulo, que são membros private. Note que nem na declaração de copia() nem em seu uso posterior em main() consideramos copia um membro da classe Retangulo. E não é. Simplesmente tem acesso a seus membros private e protected sem ser um membro.

Funções friend podem servir, por exemplo, para conduzir operações entre duas classes diferentes. Geralmente, o uso de funções friend está fora de uma metodologia POO, então sempre que possível é melhor utilizar membros da mesma classe para relizar operações com eles. Assim, no exemplo anterior, seria mais curto integrar copia() na classe Retangulo.

Classes Friend

Podemos ainda definir alguma classe como friend de alguma outra, garantindo que a primeira tenha acesso sos membros protected e private da segunda.

#include <iostream>
using namespace std;

class Praca;

class Retangulo {
int comprimento, altura;
public:
int area ()
{return (comprimento * altura);}
void converte (Praca a);
};

class Praca {
private:
int lado;
public:
void seta_lado (int a)
{lado=a;}
friend class Retangulo;
};

void Retangulo::converte (Praca a) {
comprimento = a.lado;
altura = a.lado;
}

int main () {
Praca sq;
Retangulo retang;
sq.seta_lado(4);
retang.converte(sq);
cout << retang.area();
return 0;
}

, o que retorna:

16

Neste exemplo, declaramos Retangulo como uma classe friend de Praca para que funções membro de Retangulo possam acessar os membros protected e private de Praca, mais concretamente Praca::lado. Vemos também uma declaração vazia de Praca no começo do programa. Isto é necessário pois, na declaração de Retangulo, nos referimos a Praca (como um parâmetro em converte()). A definição de Praca é incluída depois, então se não incluirmos uma declaração vazia anterior para Praca, esta classe não seria visível na definição de Retangulo.

Perceba que friends não são correspondidas se não especificarmos isso explicitamente. No nosso exemplo, Retangulo é considerada uma classe friend por Praca, mas Retangulo não considera Praca uma friend. Com isso, Retangulo pode acessar os membros private e protected de Praca, mas não o caminho contrário. Claro que poderíamos ter declarado também Praca como friend de Retangulo se assim preferirmos.

Outra propriedade de friends é que elas não são transitivas: a friend de uma friend não é considerada friend a não ser que seja especificado.

Herança entre classes
Uma funcionalidade geral da POO é a herança entre classes. Em C++ não é diferente. Herança permite criar classes que são derivadas de outras classes, então elas automaticamente incluem alguns membros “pai”, além de si mesmas. Por exemplo, suponha que queremos declarar uma série de classes que descrevem polígonos como nossa Retangulo, ou como Triangulo. Elas têm certas propriedades em comum, já que ambas podem ser descritas por meio de dois lados: altura e base.

Isto pode ser representada no mundo das classes com uma classe Poligono de onde podemos derivar as outras duas: Retangulo e Triangulo. A Classe Poligono conteria membros que são comuns para ambos os dois tipos de poligonos. No nosso caso: altura e comprimento. E Retangulo e Triangulo seriam suas classes derivadas, que especificam funcionalidades que são diferentes de um tipo de polígono para o outro.

Classes que são derivadas de outras herdam todos os membros acessíveis da classe base (classe mãe, ou classe pai). Isso significa que se uma classe base inclui um membro A e queremos derivá-la a uma outra classe com um outro membro chamado B, a classe derivada conterá ambos A e B.

Para derivar uma classe à outra, usamos o dois-pontos ( : ) na declaração da classe derivada, no seguinte formato:

class [nome_da_classe_derivada]: public [nome_da_classe_base]
{ /*…*/ };

O especificador de acesso public pode ser trocado por qualquer um dos especificadores protected e private. Este especificador de acesso descreve o nível de acesso mínimo para membros que são herdados da classe base.

#include <iostream>
using namespace std;

class Poligono {
protected:
int comprimento, altura;
public:
void seta_valores (int a, int b)
{ comprimento=a; altura=b;}
};

class Retangulo: public Poligono {
public:
int area ()
{ return (comprimento * altura); }
};

class Triangulo: public Poligono{
public:
int area ()
{ return (comprimento * altura / 2); }
};

int main () {
Retangulo retang;
Triangulo trgl;
retang.seta_valores (4,5);
trgl.seta_valores (4,5);
cout << retang.area() << endl;
cout << trgl.area() << endl;
return 0;
}

, o que retorna:

20
10

Os objetos das classes Retangulo e Triangulo contêm, cada, membros herdados de Poligono. São eles: comprimento, altura e seta_valores(). O especificador de acesso protected é similar ao private. A única diferença ocorre, na verdade, com a herança. Quando uma classe herda de outra, os membros dal classe derivada podem acessar membros protected herdados da classe base, mas não seus membros private.

Como queríamos que comprimento e altura fossem acessíveis de membros da classes derivadas Retangulo e Triangulo e não apenas membros de Poligono, temos que usar protected ao invés de private.

Podemos resumir os diferentes tipos de acesso de acordo com quem pode acessá-los da seguinte maneira:

Acesso public protected private
membros da mesma classe yes yes yes
membros de classes derivadas yes yes no
não membros yes no no

Onde “não membros” representam qualquer acesso de fora da classe, como de main(), de uma outra classe ou função. No nosso exemplo, os membros herdados por Retangulo e Triangulo têm as mesmas permissões de acesso que tinham em sua classe base Poligono.

Poligono::comprimento // acesso protected
Retangulo::comprimento // acesso protected

Poligono::seta_valores() // acesso public
Retangulo::seta_valores() // acesso public

Por isso tivemos que usar a palavra-chave public para definir relação de herança a cada uma das classes derivadas:

class Retangulo: public Poligono { … }


Anúncios
%d blogueiros gostam disto: