No Slide Title - Páginas Pessoais

Propaganda
Perspectivas baseadas em procedimentos e orientadas por
objectos
Conceitos principais: Encapsulamento, Herança,
Polimorfismo
(Encapsulation, Hierarchy, Polymorphism)
A evolução das linguagens pode analisar-se através dos paradigmas
de programação que suportam
Geralmente uma linguagem que suporta um dado paradigma de
programação suporta também os paradigmas anteriores
Podemos distinguir cinco gerações de linguagens, que são: 1) não
estrutural (Cobol, Fortran, Basic); 2) procedimental (C, Pascal);
3) modular (Modula II); 4) com abstracção de tipos de dados
(ADA); 5) programação orientada por objectos (C++, JAVA, etc.)
O que é importante:
1. Perceber o que são paradigmas de programação.
2. Perceber mais detalhes sobre encapsulamento.
3. Perceber o que é herança.
4. Perceber o que é um polimorfismo.
5. Perceber as regras da linguagem C++.
6. Estar apto a construir programas triviais que utilizem
encapsulamento, herança e polimorfismo.
O paradigma da programação procedimental é:
Decidir quais os procedimentos que se pretendem:
usar os melhores algoritmos que se puder encontrar
Devemos-nos concentrar no processamento que é necessário
para efectuar a computação desejada
Ao longo dos anos o ênfase dado à escrita de programas passou
do desenvolvimento de procedimentos para a organização
dos dados. Ao conjunto de procedimentos que se relacionam
com os dados que manipulam é normalmente
chamado um módulo
De notar, que nós já consideramos como o módulo pode ser
organizado e construído
O paradigma de programação modular passa a ser:
Decidir quais os módulos que se pretendem:
dividir o programa para esconder os dados em módulos
Agora a técnica para escrever "bons procedimentos”
aplica-se a cada procedimento num módulo
O exemplo mais comum da definição de um módulo é um módulo
de stack
Os principais problemas que têm de ser resolvidos são:
1. Fornecer ao utilizador um interface para o stack
(por exemplo funções push() e pop()).
2. Assegurar que a representação do stack (por exemplo, um array
de elementos) possa ser acedida apenas a partir dessa interface.
3. Assegurar que o stack é inicializado antes de ser usado
pela primeira vez.
O paradigma de programação para tipos abstractos é:
Decidir quais os tipos que se pretendem:
fornecer um conjunto de operações para cada tipo
Problemas com a abstracção de dados
Um tipo abstracto define um tipo de caixa preta
Uma vez definido ele não interage realmente
com o resto do programa
Não há maneira de o adaptar a novas situações excepto
se modificarmos a sua definição
Consideremos a definição de um tipo de forma para usar
em sistemas gráficos. Vamos assumir por enquanto que
o sistema deve suportar circunferências, triângulos e quadrados.
Vamos assumir também que temos:
class point { ... };
class color { ... };
Por
isso
as seguintes
class
point
{ ... }; definições do tipo point são equivalentes:
y
class color
{ ... };
x
class point
{
public:
int x:
int y; };
De notar que as palavras chave struct e class são equivalentes, com a
excepção
acessibilidade
atribuída aos seus membros. Na ausência
structdapoint
{
de classificador
int x: de acesso, os membros de uma struct são públicos
int y; e};os de uma class são privados.
class color
{
int col;
};
As classes point e color vão ser
usadas como membros da
classe shape
enum kind { circle, triangle, square };
y
x
Podemos definir uma forma desta maneira:
y
x
class shape {
point center;
color col;
kind k;
// representação de “shape”
public:
point where() { return center; }
void move(point to) { center = to; draw(); }
void draw();
void rotate();
// mais operações
};
A função draw() pode ser definida da seguinte forma:
void shape::draw()
{
O tipo de shape
switch (k)
{
que para nosso
case circle:
exemplo pode ser:
// desenho a
circunferência
// circunferência
break;
triângulo
case triangle:
quadrado
// desenho o triângulo
break;
case square:
// desenho o quadrado
break;
};
Problema: Não podem adicionar uma nova forma ao sistema se
não tiverem acesso ao código fonte de cada operação
Não existe distinção entre as propriedades gerais
de qualquer forma (a forma tem uma cor, pode ser desenhada, etc.) e
as propriedades de uma forma específica (uma circunferência possui
raio, é desenhada por uma função própria para esse efeito, etc.)
A programação orientada por objectos define-se por expressar
esta distinção e tirar partido dela
Uma linguagem com construções que permitam expressar e usar
esta distinção suportam a programação orientada por objectos.
As outras linguagens não.
O mecanismo de herança do C++ (herdado da Simula)
fornece a solução
Primeiro vamos declarar uma classe que vai definir
as propriedades gerais para todos os shapes:
class shape {
point center;
color col;
// representação de “shape”
public:
point where() { return center; }
void move(point to) { center = to; draw(); }
virtual void draw();
virtual void rotate();
virtual
// mais operações
};
As funções cujo interface pode ser definido mas que a implementação
é específica de cada forma foram marcadas com a palavra chave
"virtual". Isto significa que "pode ser redefinida mais tarde numa
classe derivada desta"
Para esta definição podemos escrever a função geral que vai
manipular os shapes:
void rotate_all(shape v[], int size, int angle)
//A função gira todos os membros de array “v”
// (de tamanho “size”) em “angle” graus
{
int i=0;
while (i<size) {
v[i].rotate(angle);
i+=1;
}
}
Para definir o shape concreto nós devemos dizer que isto é o shape
e indicar das propriedades concretas para este shape (incluindo
as funções virtuais), por exemplo:
:
:
Neste exemplo a classe
class circle : public shape
{
circle é derivada da classe
int radius;
shape. A classe shape
public:
chama-se
a classe base
O seguinte
fragmento
apresenta
formais
draw()
{ser
... considerado
}; as regras
O atributo
davoid
classe
base (vai
posteriormente)
e a classe circle chama-se
pararotate(int)
a declaração
void
{ ...da} herança:
a classe derivada
};
:
class circle public shape
Neste caso a circunferência (circle) tem os componentos geral
tãis como: e os componentos especial tãis como:
private:
point center;
color col;
public:
point where();
void move(point to);
A classe derivada
private:
int radius;
public:
void draw()
{ ... };
void rotate(int) { ... }
A classe base
O paradigma de programação é:
Decidir quais são as classes que pretendemos;
fornecer um conjunto de operações para cada classe;
explicitar os aspectos comuns através da utilização de herança
Quando não existem aspectos comuns a abstracção de dados é
suficiente
Encontrar aspectos comuns entre os vários tipos num sistema
não é um processo trivial
Vamos abordar o exemplo
Assumimos que se pretende declarar uma classe
que se chama “pessoa” (de notar que uma classe semelhante foi
considerada na aula anterior):
class pessoa
Este é o construtor
{
unsigned short Idade;
char* Nome;
public:
pessoa(unsigned short Id=0, char* No="");
virtual ~pessoa();
Este é o destrutor
virtual void print_me();
const pessoa& operator =(const pessoa&);
};
Esta funçãao será utilizada para a visualização dos dados no monitor
Nós precisamos de usar esta função mas ela não é significativa
neste contexto e por isso vai ser considerada
posteriormente
Vamos assumir que nós criámos uma classe derivada da classe pessoa,
por exemplo:
class aluno : public pessoa
Este é o número do grupo do aluno
{
int grupo;
public:
void print_me();
Idade
aluno(unsigned short, char*, int);
Nome
virtual ~aluno();
const aluno& operator =(const aluno& pes);
};
Esta função será utilizada para a visualização dos dados no monitor
A classe aluno foi derivada da classe pessoa
Consideremos o seguinte programa constituído por duas funções
main e print_all:
O programa main realiza das seguintes acções:
1. Declara dois objectos, que são p do tipo pessoa e a do tipo aluno;
2. Executa a função p.print_me para o objecto p;
3. Declara array que é composto por ponteiros para objectos
da classe pessoa
int main(int argc, char* argv[])
4. O primeiro
“muito[0]”
vai ser preenchido com
{ ponteiro
pessoa
p(25,"Paulo");
o valor &p que é o aluno
ponteiro
para
o objecto6251);
“p” do tipo pessoa.
a(21,
"Ricardo",
p.print_me();
5. O segundo ponteiro
“muito[1]” vai ser preenchido com
pessoa* muito[Quanto];
o valor &a que é o ponteiro
para o objecto “a” do tipo aluno.
muito[0]=&p;
“Quanto” é uma
muito[1]=&a;
constante que, por exemplo,
print_all(muito,Quanto);
pode ser definida da
return 0;
seguinte forma:
}
3
6. Executa a função print_all #define
que vai Quanto
ser considerada
na próxima página
int main(int argc, char* argv[])
{
pessoa p(25,"Paulo");
aluno a(21, "Ricardo", 6251);
p.print_me();
pessoa* muito[Quanto];
muito[0]=&p;
muito[1]=&a;
print_all(muito,Quanto);
return 0;
}
void print_all(pessoa** ponteiro_para_pessoa, int size)
{
int i=0;
while (i<size) { ponteiro_para_pessoa[i++]->print_me(); }
}
** significa
Array de ponteiros
ponteiro_para_pessoa
Podem ser dos
objectos derivados
da classe pessoa
ponteiro0
ponteiro1
objecto do tipo pessoa
objecto do tipo pessoa
ponteiro2
objecto do tipo pessoa
objecto do tipo pessoa
etc.
void print_all(pessoa** ponteiro_para_pessoa, int size)
{
int i=0;
while (i<size) { ponteiro_para_pessoa[i++]->print_me(); }
}
Esta função imprime os dados para todos os objectos que podem
ser determinados através dos respectivos ponteiros.
Idade,
Nome
Idade,
Nome,
grupo
Isto pode ser feito com a ajuda da função print_me
Já vimos que o ponteiro_para_pessoa pode ser ou
um ponteiro para pessoa ou um ponteiro para aluno que é o tipo
derivado do tipo pessoa
Vocês devem compreender que:
1. A variável ponteiro_para_pessoa[índice] é um nome que pode
possuir valores diferentes.
2. Se ponteiro_para_pessoa[índice] tem um valor do ponteiro para o
objecto do tipo pessoa, ele permite aceder a este objecto na memória,
por exemplo:
memória
Nome
Idade
O objecto
do tipo pessoa
3. Se ponteiro_para_pessoa tem um valor do ponteiro para o objecto
do tipo aluno (que foi derivado da pessoa), ele permite o acesso
a este objecto na memória, por exemplo:
Nome
Idade
grupo
Memória
O objecto
do tipo aluno
void print_all(pessoa** ponteiro_para_pessoa, int size)
{
int i=0;
while (i<size) { ponteiro_para_pessoa[i++]->print_me(); }
}
Vamos abordar o seguinte código:
pessoa p(25,"Paulo");
aluno a(21, "Ricardo", 6251);
p.print_me();
pessoa* muito[Quanto];
muito[0]=&p;
muito[1]=&a;
print_all(muito,Quanto);
Idade,
Nome
Idade,
Nome,
grupo
1. i=0;
2. ponteiro_para_pessoa[0];
3. print_me() para pessoa;
4. i=1;
5. ponteiro_para_pessoa[1];
6. print_me() para aluno;
Agora vamos considerar a função print_me:
void pessoa::print_me()
Nome = “Paulo”
{
cout << "Nome - " << Nome
Idade = 25
<< "; Idade - " << Idade << endl;
}
pessoa p(25,"Paulo");
aluno a(21, "Ricardo", 6251);
void aluno::print_me()
saltar na próxima linha
{
pessoa::print_me();
cout << "grupo - " << grupo << endl;
grupo = 6251
}
Nome - Paulo; Idade - 25
Nome - Ricardo; Idade - 21
grupo - 6251
Podemos concluir o seguinte:
1. A função print_all pode ser usada para a visualização no
monitor dos dados de qualquer objecto da classe pessoa (ou
da classe derivada da classa pessoa, por exemplo da classe aluno).
2. Se declaramos qualquer nova classe derivada da classe
pessoa (por exemplo empregado) podemos imprimir os respectivos
dados sem redefinição da função print_all.
3. Isto permite expandir as possibilidades do programa sem
redefinição do seu código. Por outras palavras, podemos usar
o mesmo código mesmo em tarefas novas .
4. Isto é impossível sem a utilização de herança.
Download