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