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.