Classes e Objetos - FACOM
Transcrição
Classes e Objetos - FACOM
Universidade Federal de Mato Grosso do Sul Facom - Faculdade de Computação Linguagem de Programação Orientada a Objetos Prof. Me. Liana Duenha Fundamentos da Programação Orientada a Objetos Neste tópico da disciplina, iniciaremos o estudo sobre o paradigma de programação orientada a objetos utilizando linguagem C++ para implementação dos exemplos e primeiros programas. Esse material é baseado nas notas de aula dos professores Paulo Pagliosa e Anderson Bessa, e livro texto How To Program (Deitel). 1 Motivação Em meados de 1970, pesquisadores da XEROX PARC desenvolveram a linguagem Smalltalk, a primeira totalmente orientada a objetos. No inı́cio da década de 80, a AT&T lançou a Linguagem C++, uma evolução da linguagem C com recursos para programação orientada à objetos. Atualmente, a grande maioria das linguagens incorpora caracterı́sticas de OO, como Java, Object Pascal e C#. A análise, projeto e programação orientadas a objetos são respostas ao aumento da complexidade dos ambientes computacionais que se caracterizam por sistemas heterogêneos, distribuı́dos em redes, em camadas e baseados em interfaces gráficas. A programação orientada a objetos é uma evolução de práticas que são recomendadas, mas não formalizadas, na programação estruturada. A grande dificuldade para compreender a programação OO é a diferença de abordagem do problema. Enquanto a programação estruturada tem como principal foco as ações (procedimentos e funções), a programação OO se preocupa com os objetos e seus relacionamentos; As principais vantagens da programação OO são: facilitar a modelagem do problema, extensibilidade, reusabilidade, produtividade e recursos para proteção dos dados. Os recursos da programação OO que serão estudados nessa disciplina são: encapsulamento, herança, sobrecarga de operadores, polimorfismo, e boas práticas de programação. Utilizamos linguagem C++ para exemplificar os conceitos de OO e, na segunda parte da disciplina, aprenderemos como obter os mesmos recursos usando a Lingaugem Java e as diferenças fundamentais entre as duas linguagens. 1 2 Classes e Objetos Um objeto é um modelo computacional de uma entidade concreta ou abstrata, definido por um conjunto de atributos (eventualmente vazio) e capaz de executar um conjunto de operações ou métodos. Os atributos de um objeto representam os dados que caracterizam a estrutura da entidade; os métodos executam sobre os atributos e definem o comportamento da entidade. Uma classe é uma descrição dos atributos e dos métodos de um determinado tipo de objeto. Definida uma classe pode-se, a partir dela, instanciar objetos daquele tipo. Um objeto é uma instância de uma classe. Em uma classe, declaram-se atributos e métodos que descrevem, respectivamente, as caracterı́sticas e o comportamento dos objetos instanciados. 2.1 Definindo uma classe Iniciamos com um exemplo de definição de uma classe para descrever ou modelar números complexos. Um número complexo z é um número que pode ser escrito na forma z = a + ib, onde a e b são números reais e i denota a unidade imaginária com propriedade i2 = −1. a e b são chamados, respectivamente, parte real e parte imaginária de z. O conjunto dos números complexos contém o conjunto dos números reais. Dentre as possı́veis operações sobre números complexos, escolhemos as mais simples para especificação deste nosso primeiro exemplo. Segue, então, a descrição da classe Complex. class Complex { public: float a; // parte real float b; // parte imaginária // construtor default Complex() { a = 0; b = 0; } Complex(float a1, float b1 = 0) { a = a1; b = b1; } 2 // construtor de cópia (rasa) Complex(const Complex& c) { a = c.a; b = c.b; } Complex add (const Complex& c) const; Complex sub (const Complex& c) const; // ... demais métodos da classe Complex }; // Complex Para definir uma classe utilizamos a palavra reservada class seguida do nome da classe. Por convenção, o nome de uma classe definida pelo usuário inicia com uma letra maiúscula e, por legibilidade, cada palavra subsequente no nome da classe inicia com uma letra maiúscula. O corpo de uma classe é colocado entre um par de chaves esquerda e direita. A definição da classe termina com ponto e vı́rgula. Um erro bastante comum durante a programação é esquecer do ponto e vı́rgula após a definição da classe. Especificadores de acesso Logo após a linha onde definimos o nome da classe utilizamos um especificador de acesso public: para indicar que os atributos ou métodos descritos em seguida podem ser utilizados por outros métodos do programa ou funções membro de outras classes. Um outro especificador de acesso que não está sendo utilizado nesse exemplo é denominado private:. Esse especificador de acesso informa que os atributos e/ou métodos que seguem esse especificador são acessı́veis apenas aos métodos da classe em que foram definidas. Se nenhum especificador de acesso for usado, todos os membros de dados e métodos são declarados como privados (private) por padrão. Estes e outros especificadores de acesso serão estudados mais profundamente em seções seguintes. Atributos No exemplo, após o especificador de acesso, há a descrição dos atributos ou membros de dados. Os atributos definidos são dois números reais, denominados a e b , que representam a parte real e imaginária, respectivamente, do objeto sendo instanciado. Métodos Construtores Seguindo a descrição da classe no exemplo, há dois métodos com o mesmo nome da classe denominados construtores da classe. Um construtor é um método que é invocado automaticamente sempre que um objeto da classe for criado. Geralmente, utilizamos um construtor para realizar a inicialização dos atributos da classe em 3 questão, porém pode-se também realizar no construtor quaisquer ações que se fizerem necessárias, tais como alocação de memória, abertura de arquivos, envio de mensagens via rede, impressão na tela, etc. Um construtor tem obrigatoriamente o mesmo nome da classe e não tem tipo de retorno (nem mesmo void). Quando o programador não descreve um construtor default, o compilador fornece um. Uma classe pode declarar vários construtores, desde que distinguidos pelo número e/ou tipo de parâmetros formais. Um construtor que é declarado sem quaisquer parâmetros é chamado construtor default. No nosso exemplo, o primeiro construtor não possui parâmetros de entrada e apenas atribui 0 aos atributos a e b do objeto que está sendo criado. O segundo construtor diferencia-se do primeiro apenas por ter dois parâmetros ou argumentos. O primeiro argumento é um valor real que será atribuı́do ao atributo a do objeto. O segundo parâmetro é um valor real que, quando informado, será atribuı́do ao atributo b . A sintaxe float y=0 informa para o compilador que quando o segundo parâmetro for omitido, o valor 0 deve ser atribuı́do a y e posteiormente, ao atributo b . O último dos construtores do exemplo é chamado construtor de cópia que é caracterizado por ter apenas um parâmetro cujo tipo é uma referência para um objeto constante da própria classe. O parâmetro é constante pois seus atributos não são alterados dentro do corpo do construtor. O propósito de um construtor de cópia é inicializar atributos do ojbeto recém-criado com uma cópia dos atributos do objeto referenciado pelo parâmetro. Estudaremos procedimentos de cópia rasa e profunda em seções posteriores. Demais Métodos Os métodos declarados na classe representam o comportamento de um objeto desta classe. Em C++, a implementação, ou seja, a definição do corpo, de um método declarado em uma classe pode ser efetuada dentro da classe ou fora da classe. Na classe Complex, os construtores foram implementados dentro da classe e os métodos add e sub foram apenas declarados e serão implementados fora da classe. Um método implementado fora da classe no qual foi declarado deve ser identificado através de seu nome qualificado. O nome qualificado de um método m() declarado em uma classe X é X::m(), onde o nome X é chamado qualificador, o nome m é chamado nome simples e :: é o operador de escopo de C++. Desta forma, a implementação do método add fora da classe Complex teria a seguinte sintaxe: Complex Complex::add (const Complex& c) const { ... } 4 3 Ciclo de Vida de um Objeto Em programação orientada a objetos (POO) a computação ocorre em um universo constituı́do de objetos que trocam mensagens entre si. Dentro desse universo, objetos são criados, possuem um tempo de vida útil e são destruı́dos ao longo da computação. O ciclo de vida de um objeto consiste na criação (ou instanciação), uso e destruição do objeto. 3.1 Criação de objetos Em C++, um objeto pode ser criado de duas maneiras: estaticamente e dinamicamente. Para se criar um objeto de forma estática, basta declarar uma variável cujo tipo é a classe do objeto. Seguem formas diferentes de instanciar estaticamente um objeto da classe Complex: • Complex c1; // invoca construtor default • Complex c2(); // idem • Complex c3(5); // invoca Complex(float,float), c3.a=5 e c3.b=0 • Complex c4(3,4); // invoca Complex (float,float), c4.a=3 e c4.b=4 • Complex c5(c4); // invoca construtor de cópia • Complex c6=c5; //idem Um objeto pode ser alocado dinamicamente utilizando o operador new, oferecido pelo compilador C++. A diferença fundamental é que o objeto ocupará espaço na área de heap do programa e será acessado por meio de um ponteiro. No caso da classe Complex, podemos citar alguns exemplos de uso do operador new para criação dinâmica do objeto. Na declaração Complex *p = new Complex();, p é um ponteiro para um objeto da classe Complex, cujo valor é o endereço de uma instância de Complex, dinamicamente criada com o operador new. Também podemos utilizar as várias “versões” dos construtores disponı́veis para criar objetos dinamicamente. Alguns exemplos: • Complex *p = new Complex; // new retorna um ponteiro para um objeto Complex • Complex *p = new Complex(3,4); // new retorna um ponteiro para uma inst^ ancia de Complex, criada por meio do construtor Complex(float, float) 5 • int *x = new int(); // new retorna um ponteiro para inteiro • int *vet = new int[10]; // new retorna um ponteiro para um array de 10 inteiros A criação de um objeto, seja estática ou dinâmica, envolve: 1. Alocação de espaço na memória para armazenamento dos valores dos atributos do objeto, atributos esses declarados na classe do objeto. Um objeto criado estaticamente tem memória reservada em tempo de compilação, no seguimento de dados ou no segmento de pilha do programa, caso o objeto tenha sido criado com uma declaração de variável global ou local (ou seja, em um bloco de função), respectivamente. Um objeto criado dinamicamente tem memória alocada em tempo de execução na área de heap do programa. 2. Inicialização do objeto realizado por meio da execução do código descrito em um dos métodos construtores da classe. Além da leitura sobre destruição de objetos, não deixe de ler a seção 13 para entender como liberar memória alocada previamente para um objeto. 3.2 Uso de objetos O uso de um objeto pode ser por meio ao acesso aos seus atributos ou aos seus métodos. A sintaxe utilizada para usar objetos depende da forma como foram instanciados. Seguem exemplos de acesso aos atributos dos objetos c e p da classe Complex criados, respectivamente, de maneira estatica e dinamica: • float x = c.a; // acesso ao atributo a do objeto c • float y = c.b; // acesso ao atributo b do objeto c • float t = p->a; // acesso ao atributo a do objeto p • float v = p->b; // acesso ao atributo b do objeto p Declarados os métodos que um objeto pode executar, outra maneira de usar um objeto é enviar a este uma mensagem. Uma mensagem é uma solicitação feita a um objeto para que este execute uma determinada operação. Para exemplificar, seja um ojbeto x da classe X, criado estaticamente. O envio de uma mensagem a x tem a seguinte sintaxe em C++: x.m(1,2,3); 6 onde o objeto x é chamado receptor da mensagem, m é o seletor da mensagem e (1,2,3) é a lista de argumentos da mensagem. Em resposta a uma mensagem, o objeto deve executar um método. No exemplo acima, o método a ser executado será aquele declarado na classe X cujo nome é m e cujo número e tipo de parâmetros são compatı́veis com o número e tipo dos argumentos da mensagem. O mecanismo de seleção de um método, em resposta a uma mensagem enviada a um objeto, é chamado acoplamento mensagem/método. • Complex c3 = c1.add(c2); // c1 é receptor, add é seletor e c2 é argumento da mensagem • Complex c6 = c4.sub(c5); // c4 é receptor, sub é seletor e c5 é argumento da mensagem Nesses dois casos, o argumento do construtor de cópia usado na inicialização dos objetos c3 e c6 são resultados das mensagens c1.add(c2) e c4.sub(c5), respectivamente. 3.3 Destruição de Objetos Após o uso os objetos são destruı́dos. A destruição envolve as seguintes operações (inversas das operações realizadas na criação): finalização do objeto e liberação da memória utilizada pelos atributos do objeto. A finalização do objeto é efetuada por um método especial declarado na classe do objeto chamado destrutor. O nome do destrutor de uma classe é o caractere “til”seguido pelo nome da classe. O código do destrutor deve ser responsável pela limpeza da memória utilizada (por exemplo, fechamento de arquivos, liberação de memória e dos demais recursos alocados pelo objeto na criação ou ao longo da vida útil, etc.). O destrutor em si não libera a memória do objeto. Esse método especial não recebe argumentos e não retorna valor algum. Só pode haver um destrutor declarado em uma classe. Em C++, o compilador provê um destrutor se um não for declarado, cujo corpo é vazio, ou seja, que não faz nada. O momento de destruição de um objeto depende de como este foi criado. Objetos criados estaticamente são automaticamente destruı́dos quando cessa o tempo de vida do escopo no qual foram criados. Ou seja, objetos automáticos locais são destruı́dos quando o fluxo de execução é transferido para fora do bloco onde foram declarados; objetos globais são automaticamente destruı́dos ao término da execução do programa. Objetos criados dinamicamente têm que ser explicitamente destruı́dos com o operador delete (em C++ não há coleta de lixo para destruição automática desses objetos). A seção 13 mostra como liberar memória previamente alocada de forma dinâmica. 7 Os destrutores não são chamados para objetos automáticos se o programa terminar com uma chamada à função exit() ou abort(). 4 Arquivos de cabeçalho e arquivos de código-fonte Ao construir um programa orientado a objetos utilizando a linguagem C++, é comum definir as classes em um arquivo que, por convenção, tem uma extensão de nome de arquivo .h (conhecido como arquivo de cabeçalho). No nosso exemplo, a definição da classe Complex está em um arquivo com nome Complex.h . Por convenção, descrição dos métodos destas classes (ou seja, a implementação destes métodos em si) deve ficar em um arquivo separado, cujo nome é o mesmo do arquivo de cabeçalho porém com extensão .cpp (no nosso exemplo, Complex.cpp). Este arquivo deve conter a diretiva de pré-processador #include para incluir os arquivos de cabeçalho necessários. No caso da inclusão do arquivo de cabeçalho Complex.h a diretiva correta seria #include "Complex.h" (arquivo de cabeçalho criado pelo usuário entre aspas). Além da descrição das classes e da implementação dos métodos da classe, temos ainda o código-cliente, ou seja, a função main que de fato declara e utiliza os objetos requeridos pela aplicação em questão. É comum que esse código fique em um terceiro arquivo com extensão .cpp. Desta forma, temos código-fonte reutilizável, já que a definição das classes e a implementação de seus métodos estão em arquivos independentes do código-cliente, ou seja, do código que define como a aplicação deve comportar-se. 5 Processo de Compilação e Link-edição Um programador responsável por criar a classe Complex reutilizável cria o arquivo de cabeçalho Complex.h e o arquivo de código-fonte Complex.cpp que inclui o arquivo de cabeçalho (#include) e, depois, compila o arquivo de código-fonte para criar códigoobjeto de Complex. O programador do código-cliente pode nem ter conhecimento do arquivo de código-fonte e ainda assim utilizar todas as funcionalidades da classe Complex sem dificuldade. O código-cliente só precisa conhecer a interface de Complex para utilizar a classe e deve ser capaz de linkar ou ligar seu código-objeto. A saı́da do link-editor ou linker é o aplicativo executável. O diagrama da Figura 1 mostra o processo de compilação e link-edição que resulta em um aplicativo Complex executável. 8 Arquivo de implementacao arquivo.cpp Interface da Classe arquivo.h Funcao main codigo cliente main.cpp compilador compilador Codigo objeto da classe Codigo objeto da funcao main linker Aplicativo executavel Figura 1: Processo de compilação e link-edição de para produzir um aplicativo executável. 6 Utilizando o ponteiro this Vimos que os métodos de uma classe podem manipular dados de um objeto instanciado. Porém, como indicamos sobre qual objeto o método em questão deve manipular estes dados? O ponteiro this é passado pelo compilador como um argumento implı́cito para cada um dos métodos do objeto. Os objetos utilizam o ponteiro this implicitamente ou explicitamente para referenciar seus atributos e métodos. Segue um exemplo de uso do ponteiro this. Complex(const Complex& c) { this->a = c.a; this->b = c.b; } Nesse caso, o uso do ponteiro this não é obrigatório, já que não há ambiguidade quanto aos dados que devem ser acessados. Já no caso seguinte, uma modificação do construtor da classe Complex, o uso de this é essencial pois diferencia os atributos a e b do objeto receptor da mensagem (objeto recém-criado) dos argumentos a e b. 9 Complex(float a, float b = 0) { this->a = a; this->b = b; } 7 Sobrecarga de Operadores Em C++, o nome de um método pode ser baseado em um operador da linguagem (a maioria dos operadores pode ser usada para este fim, com exceção dos operadores ., .*, :: e ?:). Este recurso sintático é chamado sobrecarga de operador. Um operador sobrecarregado é um método cujo nome é formado pela palavra reservada operator seguida do sı́mbolo do operador que se deseja sobrecarregar. Os parâmetros formais do método devem ser compatı́veis com o tipo de operador, obrigatoriamente. O valor de retorno do método deve opcionalmente ser compatı́vel com a semântica do operador. Na classe Complex previamente definida, os métodos de adição e subtração já estão implementados, mas podem ser substituı́dos pelos operadores + e - sobrecarregados. class Complex { ... Complex operator +(const Complex &c) const // add { return Complex(a + c.a, b + c.b); } Complex operador -(const Complex &c) const // sub { return Complex(a - c.a, b - c.b); } ... }; Uma vez sobrecarregados os operadores + e - na classe Complex pode-se escrever: Complex c4 = c1.operator +(c2); Complex c5 = c3 + c4; // Complex c5 = c3.operator +(c4); Na última declaração acima, c3 + c4 parece ser uma expressão aritmética do tipo adição cujos operandos são dois números complexos. Porém, a expressão é, de fato, um 10 envio da mensagem cujo seletor é operator + e cujo argumento é c4 ao receptor c3. Em resposta, o método Complex::operator +(const Complex &) const é acoplado e então invocado. Observe que o operador + é binário, ou seja, requer dois operandos. No método acima, estes dois operandos são: o objeto *this, receptor da mensagem (sendo this implicitamente passado como argumento pelo compilador), e o objeto c cuja referência deve ser explicitamente passada como argumento na mensagem. Em C++, um método const é aquele que não altera os atributos do objeto apontado por this, ou seja, do receptor da mensagem. O modificador const faz parte da assinatura do método, ou seja, dois métodos com o mesmo nome simples e mesmo número e tipo de parâmetros, um deles const e outro não, são métodos distintos. 8 Métodos inline Um método implementado dentro de sua classe é inline por definição; um método declarado fora da classe não é inline a não ser que o modificador inline preceda a assinatura do método. Uma função ou método m() inline informa ao compilador para que este tente, na geração de código nas chamadas de m(), substituir a chamada por uma expansão ”em linha”do corpo de m(). A chamada de uma função ou método inline (caso o compilador consiga fazer a expansão em linha), elimina o overhead da passagem de argumentos e saltos de ida e de volta de uma chamada de função ou método não inline, mas, em contrapartida, se o código do corpo do método for muito grande e houver muitas chamadas a m(), o código final tende a ficar maior. Portanto, funções ou métodos inline devem ser usados com critério. Como o corpo de uma função ou método inline deve ser conhecido no momento de uma chamada da função ou método, a implementação deste é comumente efetuada em um arquivo de cabeçalho (.h). Por exemplo, os operadores sobrecarregados podem ser implementados como inline no arquivo Complex.h, após a definição da classe. class Complex { ... Complex operator + (const Complex &); ... }; inline Complex Complex::operator +(const Complex& c) const { return Complex(a + c.a, b + c.b); } 11 9 Versão Final da Classe Complex Como o que estudamos até agora, podemos finalizar o arquivo Complex.h com a descrição da classe Complex e implementação dos seus métodos (todos inline). #ifndef __Complex_h #define __Complex_h class Complex { public: float a; // parte real float b; // parte imaginária // construtor Complex(float a1 = 0, float b1 = 0) { a = a1; b = b1; } // construtor de cópia (rasa) Complex(const Complex& c) { a = c.a; b = c.b; } // Cópia (rasa) Complex& operator =(const Complex&); // Complex add (const Complex& c) const; // Complex sub (const Complex& c) const; Complex operator +(const Complex&) const; Complex operator -(const Complex&) const; Complex& operator +=(const Complex&); Complex& operator -=(const Complex&); 12 bool operator ==(const Complex&) const; bool operator !=(const Complex&) const; void print() const; }; // Complex inline Complex& Complex::operator =(const Complex& c) { a = c.a; b = c.b; return *this; } inline Complex Complex::operator +(const Complex& c) const { return Complex(a + c.a, b + c.b); } inline Complex Complex::operator -(const Complex& c) const { return Complex(a - c.a, b - c.b); } inline Complex& Complex::operator +=(const Complex& c) { a += c.a; b += c.b; return *this; } inline Complex& Complex::operator -=(const Complex& c) { a -= c.a; b -= c.b; return *this; } inline bool Complex::operator ==(const Complex& c) const 13 { return a == c.a && b == c.b; } inline bool Complex::operator !=(const Complex& c) const { return !operator ==(c); } #endif // __Complex_h 10 Primeiro Projeto Nosso primeiro projeto consiste na modelagem e implementação de um vetor de números complexos. De acordo com a implementação a seguir, um vetor é definido pela quantidade de elementos que possui (numberOfElements), sua capacidade máxima, (capacity), um valor denominado delta, que representa o valor que será acrescentado à capacidade do vetor (redimensionamento da estrutura quando esta chegar ao seu limite máximo) e a sequência de números complexos em si (data). Segue abaixo a definição da classe Vector, contendo a implementação dos construtores, destrutor e assinatura dos métodos. Implemente todos os métodos declarados e não implementados. #define DEFAULT_V_SIZE 10 class Vector { private: int capacity; int delta; int numberOfElements; Complex* data; void resize(); public: // construtor Vector(int capacity = DEFAULT_V_SIZE, int delta = DEFAULT_V_SIZE) { this->capacity = capacity > 0 ? capacity : DEFAULT_V_SIZE; 14 this->delta = delta > 0 ? delta : DEFAULT_V_SIZE; this->numberOfElements = 0; this->data = new Complex[this->capacity]; } // construtor de cópia (profunda) Vector(const Vector& v) { this->capacity = v.capacity; this->numberOfElements = v.numberOfElements; this->delta = v.delta; this->data = new Complex[this->capacity]; for (int i=0; i<this->numberOfElements; i++) this->data[i]=v.data[i]; } // destrutor ~Vector() { delete []data; } // Cópia (profunda) Vector& operator =(const Vector&); int getCapacity() const; int indexOf(const Complex&) const; bool contains(const Complex&) const; bool isEmpty() const; int size() const; void print() const; bool equals(const Vector&) const; bool operator ==(const Vector&) const; bool operator !=(const Vector&) const; void void void bool bool bool bool addHead(const Complex&); addTail(const Complex&); add(const Complex&); removeAt(int); removeLast(); removeHead(); removeValue(const Complex&); }; // Vector 15 11 Acesso adequado às propriedades privadas de um Objeto As propriedades privadas de um Objeto são os campos mantidos em um trecho private da classe especificada. Esse conceito é utilizado para garantir o encapsulamento e a integridade dos dados, garantindo assim que só os métodos da classe possam manipular tais campos. Contudo, muitas vezes precisamos que o usuário tenha acesso de alguma forma a esses atributos. De forma geral, chamamos tais métodos de getters e setters. Como exemplo, podemos utilizar a classe Vector modelada em sala de aula.Um objeto desta classe possui um atributo chamado numberOfElements. Esse atributo não pode ser alterado por métodos ou funções que não sejam da classe Vector; é útil, contudo, que o usuário saiba o valor possa saber o valor atributo. Sendo assim, podemos definir um método público que informa ao usuário sobre esse valor, da seguinte maneira: inline int Vector::getNumberOfElements() const { return numberOfElements; } Dependendo de decisões de projeto, pode ser útil ter disponı́vel um método que permita o usuário modificar o valor dos atributos privados delta ou capacity. Nesse caso, poderı́amos disponibilizar os dois seguintes métodos: Obviamente, as implementações dependem das necessidades da aplicação. Se todos puderem ler e alterar os valores indiscriminadamente, não há motivos para deixar os atributos com visibilidade privada. Pela sensibilidade do conteúdo de ponteiros, uma boa pratica é sempre deixá-los com visibilidade privada. inline void Vector::setDelta(int d) { this->delta = d; } inline void Vector::setCapacity(int c) { 16 this->capacity = c; } 12 Objetos const, métodos const e argumentos const Ainda sem falar de orientação a objetos, podemos definir algumas variáveis como const. As opções são: char* ptr; //(1) const char* ptr; //(2) char *const ptr; //(3) const char* const ptr; //(4) Em (1) temos a declaração de um ponteiro para caracter. Desta forma, podemos mudar tanto o endereço armazenado na variável ponteiro, como seu conteúdo. Os seguintes exemplos são válidos: char* ptr; char c = ’c’; ptr = &c; //correto *ptr = ’c’; //correto Em (2) temos a declaração de um ponteiro para caracter de tal forma que o ponteiro pode ser alterado mas não o seu conte. O trecho de código abaixo mostra o comportamento da palavra reservada const nesse caso: const char* ptr; char c = ’c’; ptr = &c; //correto *ptr = ’c’; //incorreto Em (3), o compilador permite alterar o conteúdo do ponteiro, mas não o ponteiro. char* const ptr; //incorreto char c = ’c’; //correto ptr = &c; 17 Em (4), a declaração do ponteiro foi feita de tal forma a não deixar a possibilidade de alteração do ponteiro e tampouco o seu conteúdo. Isso é útill para ponteiros prédefinidos, como cadeias de caracteres. const char* const ptr = "Hello World"; char c = ’c’; ptr = &c; //incorreto *ptr = ’c’; //incorreto Podemos estender esse pensamento para objetos e métodos. Alguns dos objetos utilizados em um programa podem ser modificados e outros não. O programador pode utilizar a palavra chave const para especificar que um objeto não é modificável e que qualquer tentativa de modifica-lo deve resultar em um erro de compilação. A instruçãp abaixo declara um objeto const chamado objectName da classe className. class className{ . . . }; const className objectName; O exemplo acima é um exemplo de instantiação de um objeto constante. Note que a definição da classe não tem a declaração const. Nesse caso, as tentativas de modificar o objeto objectName são capturadas em tempo de compilação, ao invés de causar erros em tempo de execução. Os compiladores C++ não permitem chamadas de métodos para objetos const a menos que os próprios métodos também sejam declarados como const. Além disso, o compilador não permite que métodos declarados const modifiquem o objeto. Um método é especificado como const tanto em seu protótipo como em sua definição inserindo a palavra-chave const depois da lista de parâmetros do método. A linha abaixo define o método methodName como const, tanto em seu protótipo quanto na sua implementação. Esse método tem como retorno um dado do tipo tRret. // no arquivo .h, na classe className onde o método é definido tRet methodName () const; // no arquivo .cpp onde os métodos da classe className s~ ao implementados 18 tRet className::methodName () const { ... } Podemos ainda definir um argumento de um método como const, para garantir que o argumento não será alterado dentro do método. No exemplo, podemos atualizar o ponteiro para char denominado myName para apontar para o mesmo endereço recebido como argumento name, mas não podemos modificar o seu conteúdo dentro do método. void setName(const char* name) { myName = name; } Como o ponteiro é acessado somente para leitura, podemos incrementar o método usando const char* const. void setName(const char* const name) { myName = name; } A lógica é a mesma quando aplicada a valores de retorno de métodos. 13 Gerenciamento de memória utilizando operadores new e delete Para controlar a alocação e liberação de memória dinamicamente em um programa, o compilador C++ oferece dois operadores denominados new e delete. O operador new é responsável por alocar memória para um objeto e o operador delete é responsável por liberar a memória previamente alocada. Mostramos a seguir um exemplo de uso destes operadores. No primeiro código, temos a alocação de memória para um novo objeto chamado objectName da classe className. O operador new retorna o endereço da instância criada (ponteiro para o novo objeto). Seguem abaixo alguns outros exemplos de uso do operador new para alocação dinâmica de objetos. 19 • Inicializa um double recém-criado com 3,14159 e atribui o ponteiro resultante a ptr e, por último, destrói o objeto apontado por ptr: double *ptr = new double(3,141159); ... delete ptr; • Aloca um vetor de inteiros de 10 elementos, sem inicialização, e atribui o ponteiro para esse vetor à variável array (ponteiro): int *array = new int[10]; • A linha de código delete array libera apenas a memória utilizada pelo primeiro apontado por array, mas não libera a memória previamente alocada para os demais elementos. O seguinte uso do operador delete é o mais adequado para liberação total da memória utilizada pelo objeto. delete [] array; • Cria um objeto objectName da classe className: className* obj = new className(); • Cria um array de objetos da classe className com 100 elementos e após, libera a memória previamente alocada. O destrutor correspondente aos objetos serão invocados e por fim, a memória ocupada por eles será liberada. className* obj = new className[100]; ... delete [] obj; 14 Atributos de Classe - static Os membros de uma classe podem ser static. Quando uma variável é declarada static dentro de uma classe, todas as instâncias de objetos desta classe compartilham a mesma variável. Uma variável static é uma variável global, só que com escopo limitado à classe. A declaração de um campo static aparece na declaração da classe, junto com todos os outros campos. Como a declaração de uma classe é normalmente incluı́da em vários módulos de uma mesma aplicação via arquivos de cabeçalho (arquivos .h), a declaração dentro da classe é equivalente a uma declaração de uma variável global extern. Ou seja, a declaração apenas diz que a variável existe, mas algum módulo precisa defini-la. O exemplo abaixo ilustra a utilização de campos static: 20 class A { int a; static int b; // declara a variável, equivalente ao uso de extern para // variáveis globais }; int A::b = 0; // define a variável criando o seu espaço e a inicializa void main(void) { A a1, a2; a1.a = 0; // modifica a1.b = 1; // modifica a2.a = 2; // modifica a2.b = 3; // modifica o o o o campo campo campo campo a b a b de a1 compartilhado por a1 e a2 de a1 compartilhado por a1 e a2 cout << a1.a << " " << a1.b << " " << a2.a << " " << a2.b; // imprime 0 3 2 3 } Se a definição int A::b = 0 for omitida, o arquivo é compilado mas o linker acusa um erro de sı́mbolo não definido. Como uma variável estática é única para todos os objetos da classe, não é necessário um objeto para referenciar este campo. Isto pode ser feito com o operador de escopo (::). Por exemplo: A :: b = 4; Assim como atributos static comportam-se como variáveis globais com escopo reduzido à classe, métodos static são como funções globais com escopo reduzido à classe. Isto significa que métodos static não têm o parâmetro implı́cito que indica o objeto sobre o qual o método está sendo executado (this), e portanto apenas os campos static podem ser acessados: class A { int a; static int b; static void f(); }; int A::b = 10; void A::f() { a = 10; // errado, a só faz sentido com um objeto b = 10; // ok, b foi declarado static } C++ não suporta diretamente classes estáticas, mas se definirmos todos seus 21 métodos e atributos com essa palavra reservada, como consequência teremos uma classe estática. Como exemplo de classe estática, podemos definir a seguinte classe para operações matemáticas. class Math { private: static static public: static static }; double e = 1; double pi = 3.14; double exp(double x); //return e^x; double area(double r); //return pi*r^2; int main() { Math m; cout << Math::exp(5) << "\n"; cout << m.exp(5) << "\n"; } Reparem que no primeiro uso do método exp, nenhum objeto da classe Math foi utilizado e utilizamos o operador de escopo para informar em que classe foi definido o método exp. 15 Segundo Projeto - Lista duplamente encadeada Para exemplificar o uso dos recursos vistos até agora, vamos iniciar a implementação de uma lista duplamente encadeada, cujos nós armazenam números complexos. A classe Node representa um elemento da lista e armazena um objeto Complex, além de ponteiros para o nó anterior e o próximo nó da lista. class Node { public: Node() { next = prev = 0; 22 } Node(const Complex& el, tNode *ptr=0, tNode *ptr2=0): info(el) { next = ptr; prev = ptr2; } T info; tNode* next; tNode* prev; }; A classe DoubleList representa a lista duplamente encadeada propriamente dita. class DoubleList { public: DoubleList() { head = tail = 0; numberOfNodes = 0; } ~DoubleList(); int isEmpty() const { return head==0; } bool contains(const Complex& element) const { return isInList(element)!=0; } void addHead(const T&); void addTail(const T&); void deleteHead(); void deleteTail(); void deleteNode(const T&); void print() const; int size() const; private: Node* isInList(const Complex&) const; Node* head; Node* tail; int numberOfNodes; }; 23 As classes acima contém atributos e métodos já declarados, porém não implementados. Implemente os métodos sugeridos e outros métodos que considerem necessários. Procure utilizar os recursos vistos até o momento. 24