540 likes | 649 Views
Claudio Esperança Paulo Roma Cavalcanti. Tópicos em C++. Classes e Objetos. C++ é mais do que C com uns poucos sinos e apitos adicionais. O mecanismo básico para atingir programação orientada a objeto em C++ é o conceito de classe .
E N D
Claudio Esperança Paulo Roma Cavalcanti Tópicos em C++
Classes e Objetos • C++ é mais do que C com uns poucos sinos e apitos adicionais. • O mecanismo básico para atingir programação orientada a objeto em C++ é o conceito de classe. • Fornece formas de encapsulamento e proteção de informação. • Permite reutilização de código.
O que é OO? • Programação orientada a objeto foi o paradigma dominante dos anos 90. • Criou o conceito de objeto, que é um tipo de dado com uma estrutura e um estado. • Cada objeto define um conjunto de operações que podem acessar ou manipular esse estado. • Tipos definidos pelo usuário devem se comportar da mesma maneira de tipos pré-definidos (fornecidos pelo compilador).
Tipos de Dados • Um tipo básico de dados de uma linguagem, como inteiro, real, ou caractere, fornece certas coisas: • Podem-se declarar novos objetos, com ou sem iniciação. • Pode-se copiar ou testar quanto à igualdade. • Pode-se executar a entrada e a saída de dados com esses objetos.
Objetos • Um objeto é uma unidade atômica. • Não pode ser dissecado por um programador. • Proteção de informação torna os detalhes de implementação inacessíveis. • Encapsulamento é o agrupamento de dados, e operações que se aplicam a eles, para formar um agregado. • Mas escondendo a os detalhes de implementação. • Uma classe é o mesmo que uma estrutura, mas com os membros protegidos por default.
Encapsulamento • O princípio de atomicidade é conhecido por encapsulamento. • O usuário não tem acesso direto às partes de um objeto ou a sua implementação. • O acesso é feito indiretamente, através de funções fornecidas com o objeto. • É uma forma de imitar a vida real. • Aparelhos eletrônicos possuem o seguinte aviso: “Não abra – não há partes consertáveis por um usuário”. • Pessoas sem treinamento que tentam consertar equipamentos com esses avisos acabam quebrando mais do que consertando.
Reutilização de Código • A idéia é usar os mesmos componentes sempre que possível. • Quando o mesmo objeto é a solução fica fácil. • O difícil é quando se necessita de um objeto ligeiramente diferente. • C++ fornece diversos mecanismos para isso. • Se a implementação for idêntica exceto pelo tipo básico do objeto, pode-se usar uma template. • Herança é o mecanismo que permite estender a funcionalidade de um objeto.
Classes • Classes são usadas para encapsular informação. • Já que as funções que manipulam o estado do objeto são membros da classe, elas são acessadas pelo operador “.”, como em uma estrutura de C. • Quando se chama uma função de uma classe, na realidade se está passando uma mensagem para o objeto. • A diferença básica entre OO de C++ e o velho C é puramente filosófica: em C++ o objeto tem o papel principal.
Para Não Esquecer Jamais • Se você deseja usar OO, acostume-se a esconder todos os dados das classes. • C++ não é uma linguagem para preguiçosos. • Para isto existe uma seção de dados privados. • O compilador se esforçará ao máximo para mantê-los inacessíveis ao “mundo exterior”. • Toda classe possui um construtor e um destrutor, que dizem como uma instância do objeto deve ser iniciada e destruída. • Procure escrevê-los sempre. Evite defaults.
class memoryCell { public: memoryCell ( ) { value = 0; } int read ( ) const { return value; } void write ( int x ) { value = x; } private: int value; }; Construtor value está protegido Exemplo Bobo
main ( ) { memoryCell m; m.write ( 5 ); cout << “Conteúdo da célula é: “ #if 1 << m.read ( ) #else << m.value #endif << ‘\n’; return 0; } Erro!!! value é privado!!! Instancia a classe Armazena um valor Maneira correta Diretiva de pré-processamento Uso da Classe
Interface • A interface descreve o que pode ser feito com o objeto. • Necessita de uma boa convenção de nomes. • Exemplo: primeira letra de nome de classe ou função minúscula, demais palavras do nome com primeira letra maiúscula (sem “_”). • memoryCell. • Dados: ponteiros com prefixo ptr e pode usar “_”. • Constantes e tipos enumeráveis em caixa alta: • GL_FLOAT, GLUT_DOUBLE; • Prefixo com o nome do pacote. • glLoadIdentity(), glVertex3f.
Documentação • É fundamental uma documentação mínima em todo o arquivo de um sistema. • Existem ferramentas muito boas de domínio público, como o Doxygen. • Basta colocar diretivas na forma de comentários na própria interface. • É melhor acrescentar as diretivas durante a confecção da interface para não ter de voltar depois de “má vontade”.
// memoryCell.h // evita incluir a interface de novo se ela já foi // incluída por outro arquivo. #ifndef __MEMORY_CELL__ #define __MEMORY_CELL__ /** * A very simple interface for a generic class. * Doxygen will generate the documentation, * in html, rtf and/or latex automatically. */ class memoryCell { public: /**empty construtor. * Sets the content of this cell to value. * * @param value new value for this cell. */ memoryCell ( int value = 0 ); /// destrutor. Não faz nada. ~memoryCell ( ) { } Interface memoryCell
/**the correct way of returning the content of this cell. * @return the content of this cell. */ int read ( ) const; /** sets a new value for this cell. * @param x new value. */ void write ( int x ); private: ///holds the content of this cell. int cell_value; }; #endif Não altera dados desta classe Altera dados desta classe Interface memoryCell
Implementação • A implementação contém os detalhes de como a interface foi codificada, de forma a atender as especificações do projeto. • As declarações dos nomes das funções ficam na declaração da classe e as implementações são definidas depois, normalmente num arquivo separado (.C, .cpp, ou .cc). • Usam a sintaxe de função mais o nome da classe e o operador de escopo “::”.
// memoryCell.C #include memoryCell.h memoryCell::memoryCell ( int value ) { this->cell_value = value; } int memoryCell::read ( ) const { return this->cell_value; } void memoryCell::write ( int x ) { this->cell_value = x; } getter setter Construtor Membro desta classe Implementação memoryCell
Construtores e Destrutores • O construtor diz como o objeto é declarado e iniciado. • Se a iniciação não casar com nenhum construtor, o compilador reclamará. • O destrutor diz como o objeto será destruído quando sair de escopo. • No mínimo deve liberar a memória que foi alocada por chamadas “new”no construtor. • Se nenhum destrutor for declarado será gerado um default, que aplicará o destrutor correspondente a cada dado da classe.
Construtor de Cópia • O construtor de cópia é chamado sempre que o objeto for passado ou retornado por valor. • Se nenhum for declarado será criado um default, que aplicaráoconstrutor de cópia correspondente a cada dado da classe. • Se a declaração for privada, o construtor de cópia será desativado (não poderá ser invocado). • Ou escreva um construtor de cópia decente ou então desative-o.
memoryCell ( const memoryCell& m ) { *this = m; } Operador de atribuição Construtor de Cópia Exemplo de Construtor de Cópia Uso: memoryCell m1 (4 ); memoryCell m2 ( m1);
Operador de Atribuição • É usado para copiar objetos do mesmo tipo. • O operador de cópia cria um novo objeto, mas o operador “=“ age sobre um objeto já existente. • Operadores de atribuição retornam, em geral, referências constantes. Retorno por valor não é uma boa idéia ... • Retornar uma referência não constante torna (A = B) = C válido, mas sem sentido. • O resultado é atribuir C à referência A, sem nunca ter atribuído à B. • Se não for definido haverá uma cópia membro a membro dos dados da classe por default.
const memoryCell& memoryCell::operator = ( const memoryCell& m ) { this->cell_value = m.cell_value; return *this; } Referência para este objeto Operador de atribuição Exemplo de Operador = Uso: memoryCell m1 (4 ); memoryCell m2 = m1;
Iniciação X Construtor memoryCell ( int value = 0 ) :cell_value ( value ){ } • A seqüência depois de “:” é a lista de iniciação. • Os membros são iniciados na ordem em que são declarados e não na ordem da lista. • O ideal é iniciar cada membro da classe pelo seu próprio construtor. • É preferível iniciar os membros da classe usando listas de iniciação ao invés de atribuir no construtor. • Cada membro não especificado na lista é iniciado pelo seu construtor vazio. Só depois é que as atribuições são feitas (trabalho dobrado). • Se a iniciação não for simples, só aí usa-se o corpo do construtor.
Sobreposição de Operadores • Em C++ todos os operadores podem ser sobrepostos, a exceção de: “.”, “.*”, “?” e “sizeof”. • Precedência e aridade não são alteradas. • Um operador binário retorna, em geral, um objeto por valor, porque o resultado é armazenado em um temporário. • Também pode ser implementado invocando o operador de atribuição.
const memoryCell& memoryCell::operator += ( const memoryCell& m ) { this->cell_value += m.cell_value; return *this; } memoryCell memoryCell::operator + ( const memoryCell& m ) const { memoryCell temp ( *this ); temp += m; return temp; } Referência para este objeto Adiciona o segundo operando Retorna temp por valor Exemplo de Sobreposição
Templates • Templates servem para escrever rotinas que funcionam para tipos arbitrários. • O mecanismo baseado em typedef permite criar rotinas genéricas. • Mas não é suficiente se queremos rotinas que funcionem com dois tipos diferentes. • Uma template não é uma função comum, mas sim um padrão para criar funções. • Quando uma template é instanciada com um tipo particular, uma nova função é criada. • A template é expandida (como uma macro) para prover uma função real. • O compilador gera código a partir da template para cada combinação diferente de parâmetros.
Exemplo de Função Template template <classEtype> inline const Etype&Max ( const Etype& a, const Etype& b ) { return a > b ? a : b; } • Etype define o tipo do parâmetro da template. • O código da template pode ser substituído como uma macro, caso se use a opção inline. • Deve-se retornar uma referência, pois não se sabe de antemão o tamanho do dado retornado. • O operador “>”deve estar definido no tipo Etype.
Classes Template • A sintaxe é similar ao de uma função template. • Não há sentido em gerar uma biblioteca de templates. • Lembre-se que o compilador não sabe que tipos serão necessários. Logo, não gera código algum. • Ou todo o código da template está junto com a interface, ou a template deve ser instanciada de antemão para cada tipo a ser usado na aplicação.
// memoryCell.h #ifndef __MEMORY_CELL__ #define __MEMORY_CELL__ /** *A very simple interface for a template class. */ template <class Etype> class memoryCell { public: /** empty construtor. * Sets the content of this cell to value. */ memoryCell ( const Etype& value = Etype() ) { cell_value = value; } /** the correct way of returning the content of this cell. * @return the content of this cell. */ const Etype& read ( ) const { return cell_value; } /** sets a new value for this cell. * @param x new value. */ void write ( const Etype& x ) { cell_value = x; } private: /// holds the content of this cell. Etype cell_value; }; #endif Referência constante Especificação template antes da classe Pode assumir qualquer tipo TemplatememoryCell
main ( ) { memoryCell<int> m; m.write ( 5 ); cout << “Conteúdo da célula é: “ #if 1 << m.read ( ) #else << m.cell_value #endif << ‘\n’; return 0; } Instancia a template com int Uso da TemplatememoryCell
Herança • Talvez o principal objetivo de programação orientada a objeto seja a reutilização de código. • Templates são apropriadas quando a funcionalidade básica do código é independente de tipo. • Herança serve para estender a funcionalidade de um objeto. • Criam-se novos tipos com propriedades restritas ou estendidas do tipo original.
Polimorfismo • Polimorfismo permite que um objeto possa armazenar vários tipos diferentes de objetos. • Quando uma operação for aplicada a um objeto polimorfo será selecionada automaticamente aquela adequada ao tipo armazenado. • Sobreposição (overload) de funções e operadores é um exemplo de polimorfismo. • Neste caso, a seleção da função é feita em tempo de compilação, o que limita o comportamento polimorfo.
Reutilização • Em C++ é possível postergar a seleção da função até que o programa esteja sendo executado. • O mecanismo básico usa herança. • Freqüentemente, no projeto de uma nova classe, descobre-se que há uma classe similar escrita anteriormente. • Reutilização de código sugere que não se comece do zero, mas que se escreva uma nova classe baseada na classe existente. • Em C++ o mecanismo é criar uma classe abstrata polimorfa que possa armazenar os objetos das classes similares.
Exemplos • Classe Vetor: Vetores limitados. • Classe Data: Calendários diversos (Gregoriano, Hebreu, Chinês). • Impostos: vários tipos de contribuintes (solteiro, casado, separado, cabeça de casal). • Formas: (círculos, quadrados, triângulos).
Três Opções • Definir classes completamente independentes. • Implica em escrever vários pedaços de código idênticos. • Aumenta a chance de erro e cada mudança deve ser replicada. • Usar uma única classe e controlar as funções por if / else e switches. • Manutenção é difícil, pois cada alteração requer recompilar todo o código. • Extensão das classes por um programador não é possível a menos que todo o fonte esteja disponível. • If / else gasta tempo de execução, mesmo que o resultado seja conhecido em tempo de compilação. • Não há segurança de tipos, pois se toda forma está contida em uma única classe, pode-se atribuir um círculo a um triângulo.
Derivação • Derivar classes a partir de uma classe base (herança). • Uma classe derivada herda todas as propriedades da classe base. • Cada classe derivada é uma nova classe. Logo, a classe base não é afetada por mudanças nas classes derivadas. • Uma classe derivada é compatível por tipo com a base, mas o contrário é falso. • Classes irmãs não são compatíveis por tipo.
Análise • As primeiras duas opções são típicas de programação procedural. • A terceira opção é aquela que deve ser adotada em programação orientada a objeto. classDerivada: publicBase { // membros não listados são herdados. public: // construtores e destrutores (em geral // diferentes da base) // membros da base sobrepostos // novos membros públicos private: // dados adicionais (geralmente privados) // funções privadas adicionais // membros da base a serem desativados };
Uso de Ponteiros com Herança • Ponteiro para classe base • classeBase* bPtr = NULL; • classeBase bVar; Objeto do tipo base. • bPtr = &bVar; Trivialmente válido. • bPtr = &dVar; dVar é do tipo classeDerivada. • bPtr->print(); Chama print da classe base. • Ponteiro para classe derivada • classeDerivada* dPtr = NULL; • classeDerivada dVar; Objeto do tipo derivado. • dPtr = &dVar; Trivialmente válido. • dPtr = &bVar; ERRO!! • dPtr->print(); Chama print da derivada.
Apontar Ponteiro Derivado para Classe Base • Gera erro. • Classe derivada pode ter métodos e variáveis que não existem na classe base. • Objeto da classe base não é do mesmo tipo do objeto da classe derivada.
Apontar ponteiro Base para Classe Derivada • Mecanismo válido. • Mas só podem ser chamados métodos da classe base. • Chamada a métodos da classe derivada gera erro. • Tipo de um ponteiro ou referência define os métodos que podem ser invocados.
Funções Virtuais • Objetos (e não os ponteiros) definem os métodos que devem ser invocados. • Suponha círculo, triângulo e retângulo derivados de uma classe forma. • Cada um com seu próprio método de desenho (draw). • Para desenhar qualquer forma, chama-se draw a partir de um ponteiro para forma. • O programa determina, em tempo de execução, qual draw chamar (formas são tratadas de maneira genérica).
Declaração Virtual • Draw deve ser declarada virtual na classe base. • Sobreponha-se draw em cada classe derivada. • As assinaturas devem ser idênticas. • Um vez declarada virtual, será virtual em todas as classes derivadas. • É boa prática manter a declaração virtual nas classes derivadas, embora não seja mandatário.
Tipos de Herança • Pública: todos os membros públicos da classe básica permanecem públicos. • Membros privados permanecem privados. • Relacionamento É-Um (IS-A). • Privada: mesmo membros públicos são escondidos. • Relacionamento Tem-Um (HAS-A). • É preferível usar composição do que herança privada. • É a herança default.
Herança de Construtores • O estado de público ou privado dos construtores, construtor de cópia, e operador de atribuição são herdados. • Se eles forem herdados, mas não definidos na classe derivada, os operadores correspondentes são aplicados a cada membro.
Binding Estático e Dinâmico • No bindingestático a decisão de que função invocar para resolver uma sobreposição é tomada em tempo de compilação. • No bindingdinâmico a decisão é tomada em tempo de execução. • C++ usa binding estático por default, porque se achava no passado que o overhead seria significativo. • Para forçar o binding dinâmico o programador deve especificar a função como virtual.
Porque binding dinâmico? • Quando se declara um ponteiro para uma classe como argumento de uma função, normalmente o tipo do ponteiro é o da classe base. • Assim, pode ser passada qualquer uma das classes derivadas e o tipo do objeto apontado é determinado em tempo de execução. • Virtualidade é herdada. • Funções redefinidas em classes derivadas devem ser declaradas virtuais. • Uma decisão em tempo de execução só é necessária quando o objeto for acessado através de ponteiros.
Construtores e Destrutores Virtuais? • Construtores nunca são virtuais. • Destrutores devem ser virtuais na classe base apenas. • O tipo de um construtor sempre pode ser determinado em tempo de compilação. • O destrutor deve ser virtual para garantir que o destrutor do objeto real é chamado. • A classe derivada pode ter membros adicionais que foram alocados dinamicamente, que só podem ser desalocados pelo destrutor da classe derivada.
Custo • Polimorfismo tem um custo que impacta na performance. • STL não usa polimorfismo por questão de eficiência. • Cada classe com função virtual tem uma tabela vtable. • Para cada função virtual, vtable tem um ponteiro para função apropriada. • Se a classe derivada tiver a mesma função virtual da classe base, o ponteiro da vtable aponta para a função na base.
Casting (compatibilização) • Downcasting • Operador dynamic_cast • Determina o tipo do objeto em tempo de execução. • Retorna 0 se não for o tipo certo (não pode sofrer cast) NewClass *ptr = dynamic_cast < NewClass *> objectPtr; • Pode compatibilizar endereço de objeto do tipo base para ponteiro para classe derivada.
Informação de Tipo • Keyword typeid • Header <typeinfo> • Uso: typeid(object) • Retorna type_info do objeto • Tem informação sobre o tipo do operando, incluindo nome. • typeid(object).name()