Jiraspiom

Blog Pessoal

Lição 11

Lição 11 – Programação Orientada a Objeto

Programação Orientada a Objeto – POO

Classes

Uma classe em C++ é um conceito expandido de estrutura de dados: ao invés de conter apenas dados, pode conter dados e funções. Um objeto é uma instanciação de uma classe. Em termos de variáveis, uma classe seria o tipo e um objeto seria a variável. Formato de declaração (usa, obviamente, a palavra-chave class):

class nome_da_classe{
especificador_de_acesso1:
membro1;
especificador_de_acesso1:
membro2;

} nome_dos_objetos;

, onde mebros podem ser dados, declarações de funções ou opcionalmente especificadores de acesso. Os últimos podem ser uma das três palavras-chave: private, public, protected. Estes especificadores modificam os direitos de acesso que os membros que os seguem adquirem:

  • membros private de uma classe são acessíveis somente de outros membros da mesma classe ou de seus friends (explicado em lições posteriores).
  • membros protected são acessíveis de membros da mesma classe e de seus friends, mas também de membros de classes derivadas.
  • membros public são acessíveis de qualquer lugar onde o objeto é visível.

Por padrão, todos os membros de uma classe são declarados com acesso private, a não ser que estejam especificados. Exemplo de uma classe:

class Retangulo{
int x, y;
public:
void seta_valores(int,int);
int area (void);
} retang;

Note que esta classe contém os membros x,y, seta_valores() e area(), sendo os dois primeiros private e os dois últimos public.
Exemplo de acesso aos elementos public:

retang.seta_valores(3,4);
x = retang.area();

Exemplo completo de Retângulo:

#include <iostream>
using namespace std;

class Retangulo{
int x, y;
public:
void seta_valores (int,int);
int area () {return (x*y);}
};

void Retangulo::seta_valores (int a, int b) {
x = a;
y = b;
}

int main () {
Retangulo retang;
retang.seta_valores (3,4);
cout << “Area: ” << retang.area();
return 0;
}

, que retorna na tela:

Area: 12


Note que a definição da função membro area() foi incluída diretamente na definição da classe Retangulo, dada sua simplicidade, onde seta_valores() tem seu protótipo declarado na classe, e sua definição está fora dela. Nesta declaração externa devemos utilizar o operador de escopo ( :: ), para especificar que nós estamos definindo uma função que é um membro da classe Retangulo e não é uma função global usual. Ele especifica, então, a classe a qual o membro que está sendo declarado pertence, garantindo exatamente a mesma propriedade de escopo, como se esta definição de função estivesse diretamente incluída na definição da classe. Por exemplo, na função seta_valores() do código anterior pudemos usar x e y, que são membros private da classe Retangulo (que significa que são acessíveis somente de outros membros da própria classe).

Uma das maiores vantagens de uma classe é que podemos declarar vários objetos dela. Por exemplo, seguindo o exemplo anterior, podemos ter declarado o objeto retang2 em adição a retang:

#include <iostream>
using namespace std;

class Retangulo {
int x, y;
public:
void seta_valores (int,int);
int area () {return (x*y);}
};

void Retangulo::seta_valores (int a, int b) {
x = a;
y = b;
}

int main () {
Retangulo retang, retang2;
retang.seta_valores (3,4);
retang2.seta_valores (5,6);
cout << “Area do Retangulo1: ” << retang.area() << endl;
cout << “Area do Retangulo2: ” << retang2.area() << endl;
return 0;
}


, o que

Area do Retangulo1: 12
Area do Retangulo2: 30


Neste caso concreto, a classe (tipo dos objetos) a que estamos falando é Retangulo, na qual existem duas instâncias ou objetos: retang e retang2. Cada uma delas tem sua própria variável membro e funções membro. Note que chamar retang.area() não nos dá o mesmo reultado que chamar retang2.area(). Isto acontece pois cada objeto da classe Retangulo tem suas próprias variáveis x e y, assim como também têm suas próprias funções membro seta_valores() e area(), cada uma usando suas próprias variáveis de objeto para operar.

Isto é o conceito básico da POO: dados e funções são ambos membros do objeto. Não usamos mais sets de variáveis globais que passamos de uma função para a outra como parâmetros mas, ao invés disso, manipulamos objetos que têm seus próprios dados e funções embutidos como membros. Note que não tivemos que passar nenhum parâmetro em nenhuma das chamadas a retang.area ou retang2.area. Aquelas funções membro usaram diretamente os dados de seus respectivos objetos retang e retang2.

Construtores e Destrutores

Objetos geralmente necessitam inicializar variáveis ou atribuir memória dinamicamente durante seus processos de criação para se tornar operativos e para evitar o retorno de valores inesperados durante suas execuções. Por exemplo, o que aconteceria se no exemplo anterior chamássemos a função membro area() antes de termos chamado a função seta_valores()? Provavelmente teríamos um resultado indeterminado já que não teriam sido atribuídos valores para os membros x e y. Para evitar isso, uma classe pode incluir uma função especial chamada construtor, que é chamada automaticamente sempre que um novo objeto desta classe é criado. Este construtor tem que ter o mesmo nome da classe e não deve ter nenhum tipo de retorno, nem mesmo void.
Exemplo de implementação de Retangulo incluindo um construtor:

#include <iostream>
using namespace std;

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

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

int main () {
Retangulo retang (3,4);
Retangulo retang2 (5,6);
cout << “Area do Retangulo1: ” << retang.area() << endl;
cout << “Area do Retangulo2: ” << retang2.area() << endl;
return 0;
}


, o que retorna:

Area do Retangulo1: 12
Area do Retangulo2: 30

Perceba que o resultado do exemplo é idêntico ao do exemplo anterior. Mas agora removemos a função seta_valores() e incluimos , ao invés dela, um construtor que realiza uma tarefa similar: inicializa os valores x e y com os parâmetros que são passados a eles. Note que estes argumentos são passados ao construtor no mesmo instante que os objetos desta classe são criados:

Retangulo retang (3,4);
Retangulo retang2 (5,6);


Os construtores não podem ser chamados explicitamente como se eles fossem funções membros comuns. Eles somente são executados quando um novo objeto da classe é criado. Já os destrutores têm a funcionalidade oposta: são automaticamente chamados quando um objeto é destruído, até porque seu escopo de existência chegou ao fim (por exemplo, se foi definido como um objeto local de uma função e a função termina) ou porque é um objeto atribuído dinamicamente e é liberado usando o operador delete.

O destrutor deve ter o mesmo nome da classe, mas antecedido por um til (~) e como os construtores, não retorna valor. Seu uso é especialmente adequado quando um objeto aloca memória dinamicamente durante seu tempo de vida e no momento de ser destruído, queremos liberar a memória a que o objeto estava alocado.

#include <iostream>
using namespace std;

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

Retangulo::Retangulo (int a, int b) {
comprimento = new int;
altura = new int;
*comprimento = a;
*altura = b;
}

Retangulo::~Retangulo () {
delete comprimento;
delete altura;
}

int main () {
Retangulo retang (3,4), retang2 (5,6);
cout << “Area do Retangulo1: ” << retang.area() << endl;
cout << “Area do Retangulo2: ” << retang2.area() << endl;
return 0;
}


, o que retorna:

Area do Retangulo1: 12
Area do Retangulo2: 30

Construtores Sobrecarregados

Como qualquer outra função, um construtor também pode ser sobrecarregado com mais de uma função que tenha o mesmo nome mas tipos ou números de parâmetros diferentes. Lembre-se que para funções sobrecarregadas, o compilador chamará aquele cujos parâmetros batam com os argumentos usados na chamada da função. No caso dos construtores, que são chamados automaticamente quando o objeto é criado, o executado será aquele que bata com os argumentos passados na declaração do objeto.

#include <iostream>
using namespace std;

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

Retangulo::Retangulo () {
comprimento = 5;
altura = 5;
}

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

int main () {
Retangulo retang (3,4);
Retangulo retang2;
cout << “Area do Retangulo1: ” << retang.area() << endl;
cout << “Area do Retangulo2: ” << retang2.area() << endl;
return 0;
}


, o que retorna:

Area do Retangulo1: 12
Area do Retangulo2: 25


Neste caso, retang foi declarado sem nenhum argumento então foi inicializado com o construtor que não tem parâmetros, que inicializa ambos comprimento e altura com o valor de 5.

IMPORTANTE: Note que se declaramos um novo objeto e quisermos usar seu construtor padrão (sem parâmetros), não incluímos parênteses ():

Retangulo retang; // certo
Retangulo retang2(); // errado!

Construtor Padrão

Se não declararmos nenhum construtor na definição da classe, o compilador assume que a classe tenha um construtor padrão sem argumentos. Assim, após declarar uma classe como esta:

class CExemplo {
public:
int a,b,c;
void multiplo (int n, int m) { a=n; b=m; c=a*b; };
};

O compilador assume que Exemplo tem um construtor padrão, então podemos declarar objetos desta classe simplesmente declarando-os sem argumentos:

CExemplo ex;

Entretanto, assim que declaramos nosso próprio construtor, o compilador não mais fornece um construtor padrão implícito. Devemos, com isso declarar objetos daquela classe de acordo com o protótipo do construtor que definimos para a classe:

class CExemplo {
public:
int a,b,c;
CExemplo (int n, int m) { a=n; b=m; };
void multiplo () { c=a*b; };
};

Aqui declaramos um construtor que recebe 2 parâmetros do tipo int. Então a seguinte declaração de objeto seria correta:

CExemplo ex (2,3);

mas,

CExemplo ex;

Não seria correto pois declaramos a classe para ter um construtor explícito, deste modo substituindo o construtor padrão. O compilador, entretanto, não apenas cria um construtor padrão se não especificarmos um, como também fornece três funções membro especiais no total que são implicitamente declaradas se não declararmos as nossas. São elas o construtor cópia, a cópia do operador de atribuição e o destrutor padrão.

O construtor cópia e a cópia do operador de atribuição copiam todos os dados contidos em outro objeto para os dados membros do objeto atual. Para CExemplo, o construtor cópia implicitamente declarados pelo compilador seriam algo similar a:

Cexemplo::Cexemplo (const CExample& rv) {
a=rv.a; b=rv.b; c=rv.c;
}

Então, as duas declarações de objetos seguintes seriam corretas:

CExemplo ex (2,3);
CExemplo ex2 (ex); // construtor cópia (dados copiados de ex)

Ponteiros para Classes

Em C++ podemos criar ponteiros que apontam para classes. Simplesmente consideramos que, uma vez declaradas, classes se tornam tipos válidos, então podemos usar o nome da classe como o tipo do ponteiro. Assim:

Retangulo * pretang;

é um ponteiro para um objeto da classe Retangulo

Como acontece com estruturas de dados, para nos referirmos diretamente a um membro de um objeto apontado por um ponteiro, podemos usar o operador flecha (->) de direção. Mostrarei um exemplo com algumas possíveis combinações:

#include <iostream>
using namespace std;

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

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

int main () {
Retangulo a, *b, *c;
Retangulo * d = new Retangulo[2];
b= new Retangulo;
c= &a;
a.seta_valores (1,2);
b->seta_valores (3,4);
d->seta_valores (5,6);
d[1].seta_valores (7,8);
cout << “Area de a : ” << a.area() << endl;
cout << “Area de *b : ” << b->area() << endl;
cout << “Area de *c : ” << c->area() << endl;
cout << “Area de d[0] : ” << d[0].area() << endl;
cout << “Area de d[1] : ” << d[1].area() << endl;
delete[] d;
delete b;
return 0;
}

, o que retorna na tela:

Area de a : 2
Area de *b : 12
Area de *c : 2
Area de d[0] : 30
Area de d[1] : 56

A seguir temos um sumário onde mostramos como podemos ler alguns operadores entre ponteiro e classes que aparecem no exemplo anterior:
expressão pode ser lida como
*x apontado por x
&x endereço de x
x.y membro y do objeto x
x->y membro y do objeto apontado por x
(*x).y membro y do objeto apontado por x (equivalente ao anterior)
x[0] primeiro objeto apontado por x
x[1] segundo objeto apontado por x
x[n] “enésimo mais um” (n+1) objeto apontado por x

Certifique-se de que você entenda a lógica de todas essas expressões antes de avançar às próximas lições. Se tiver dúvidas, leia novamente esta seção e/ou consulte lições anteriores sobre ponteiros e estruturas de dados.

Classes definidas com struct e union

Classes podem ser definidas não apenas com a palavra-chave class, mas também com as palavras0chave struct e union. Os conceitos de classe e estruturas de dados são tão similares que ambas struct e class podem ser usadas em C++ para declarar classes. A única diferença entre ambos é que membros de classes declarados com a palavra struct têm acesso public por default (padrão), enquanto membros de classes declarados com a palavra class têm acesso private.

O conceito de union é diferente do de classe declarada com struct e class, já que unions apenas armazenam um dado membro por vez. Todavia são também classes e com isto também podem carregar funções membro. O acesso default em classes union é public.


%d blogueiros gostam disto: