Universidade
Federal de Minas Gerais

Instituto de Ciências Exatas

Departamento de Ciência da Computação

Algoritmos e
Estruturas de Dados I

Definição
e uso de classes

As classes na linguagem C++ nos
permitem definir tipos. Para poder
formar um modelo mental inicial (é apenas um auxílio mental!)
podemos imaginar que as classes são como conjuntos e as
instâncias/objetos são como elementos destes conjuntos. Este
modelo mental só será útil se a pessoa não
confundir os conceitos da Matemática com os conceitos da
Computação. Estamos falando aqui do conceito de classe da
Computação, mais especificamente o conceito de classe da
linguagem C++

OBS: na gênese de C++ houve uma decisão de estender o elemento
“struct” da linguagem C para definir o
elemento class de C++. Mas não é o caso
de poder falar que o termo “class” de C++
possa sempre ser substituido pelo termo “struct”. De forma bem simplificada a
concepção inicial de C++ consistiu em como estender o conceito de
struct (que traduz a noção de
“estado”) de forma a incluir a definição de
funções (que traduz a noção de
“comportamento”).

O conceito de classe, categoria, conjuntos da Matemática são
bastante distintos do conceito de classe na Computação. Para
perceber um aspecto bem distinto de (i)classe C++ e (ii) conjunto, considere o problema da categoria dos
elementos: só podem ser objetos da classe X objetos do tipo X (a classe
define um tipo) mas na matemática um conjunto pode envolver elementos de
qualquer tipo, você pode definir de forma extensiva um conjunto Y
constituído de 3 elementos: um livro, uma idéia
e um barulho. Conforme veremos abaixo as classes C++ têm uma
relação muito forte com a questão da tipagem.
Se você gosta de Filosofia então é interessante notar que
existem diversos aspectos filosóficos relacionados a
categorização e cognição no fato de que em algumas
linguagens de programação existem tentativas de reproduzir
não só representações de coisas do “mundo-real”  como também tentativas de
reproduzir “abstrações” . O livro

As classes servem para definir estrutura e comportamento. Na
linguagem C++ as classes constituem um dos mecanismos de
definição de tipos. O programador através da
definição de classes define novos tipos. As classes e seus objetos(suas instâncias) nos permite compor
programas dentro do paradigma denominado Programação Orientada a
Objetos. A estrutura corresponde, tipicamente, a
variáveis ou campos e o comportamento corresponde a
métodos (funções).Na linguagem C++ é comum referir
aos elementos de uma classe como sendo seus “membros”, e os métodos referidos como
“funções”. Nossos exemplos aqui são
artificiais e simples para simplificar os conceitos. Também para efeito
de simplificação, discutimos apenas algumas opções
de definição e uso de classes.

No programa abaixo temos a definição de uma classe Dado (die/dice) com uma estrutura bem simples (um atributo do tipo int que descreve o estado relativo à face ) e dois comportamentos: obterFace()
e rolar().

#include <cstdlib>

#include <iostream>

#include <ctime>

using namespace
std;

class Dado{

private:
//default

int face;

public:

Dado(){srand(time(0));rolar();};

const static int FACES=6;

int obterFace();

void rolar();

};

int Dado::obterFace(){return face;}

void Dado::rolar(){face=rand()%FACES + 1;}

int main(int argc, char *argv[])

{

Dado d1;

d1.rolar();

cout<< d1.obterFace()<<"\n";

system("PAUSE");

return EXIT_SUCCESS;

}

Exercício: Utilize o programa acima como base para escrever um
programa que testa a frequência de valores relativos ao lançamento
de dois dados.

Um possível esquema para definição e
utilização de uma classe é portanto
(de forma sintética):

classe NomeClasse{/*definição*/}

...

NomeClasse variavel;//alguns autores querem
diferenciar variável e instância

Devemos introduzir as classes envolvendo
significado ou de forma sintética?

O mecanismo de classe define um tipo e podemos usar este tipo para definir
instâncias (variáveis). Quando o fluxo de controle chega
até uma definição de variável que utiliza uma
classe: (i) o código de todas as classes envolvidas será
carregado na memória e (ii)
é alocada e inicializada a memória para as variáveis de
classe das classes envolvidas; por fim a memória para as
instâncias de classes é alocada e são feitas as
inicializações. No caso da classe Dado
existe uma única variável de instância a variável
(campo) particular/privada face
e cada variável tera um campo face. Devemos acessar os
elementos de C (variáveis e funções) usando o operador de
seleção (ponto). Podemos usar o operador de seleção
no nome da instância e selecionar o campo ou função de
instância.

Na definição da classe Dados temos ainda a
definição de uma constante “estática”
identificada por FACES.
A constante FACES
não é replicada em cada instância! Existe
apenas uma constante FACES
na classe Dado, compartilhada por todas as instâncias. O trecho:

class Dado{… int face; … const static int FACES=6;}

...

Dado d1, d2;

Corresponde ao seguinte desenho:

Em memória temos a classe Dado (código e dados) indicado pelo
retângulo à esquerda, e os dois objetos correspondentes às
variáveis d1 e d2 indicados à direita. As setas azuis indicam que
d1 e d2 sabem que são instâncias da classe Dado. Observe que uma
definição de estrutura (struct) na
linguagem C normalmente não envolve espaço de memória em
tempo de execução, mas as classes C++ normalmente ocupam a
memória. A ocupação de memória das classes envolve
funções e variáveis de classe (estáticas).

Construtores

Além de métodos e campos uma classe pode definir elementos denominados construtores (Constructors).
Se o programador não definir um construtor de forma explícita o
compilador irá prover um construtor default. Um possível esquema
para definição de construtores é:

class NomeClasse{ ... NomeClasse(){...}
...};

Os construtores são elementos de uma classe “parecidos”
com funções mas que têm o mesmo
nome da classe. Uma classe pode definir vários construtores, todos eles
devem ter o nome da classe e diferem entre sí
pelo número e tipo dos parâmetros:

class NomeClasse{ ... NomeClasse(){...};
NomeClasse(int p){...}; NomeClasse(double d){...}; ...};

O construtor gerado pelo compilador (ver abaixo) ou um construtor definido
pelo programador que não exija parâmetros, são todos
referidos como “construtor default”. O “construtor
default”  é
“chamado/invocado” de forma similar a um método (é
criado um registro de ativação!) de forma IMPLÍCITA no
momento em que um objeto é criado (definições sem
parâmetros). O programador pode invocar o construtor de forma
EXPLÍCITA e neste caso o compilador não gera a chamada
implícita do construtor default. Observe que na classe Dado acima o
construtor default atribui a semente para geração de valores
randômicos a partir da hora do sistema, em seguida faz a primeira
invocação do método rolar() para
a instância correspondente.

Construtores no caso de arranjos

Quando definimos um arranjo de instâncias de classe, cada elemento
terá seu construtor (eventualmente um construtor default!!)  invocado de
maneira automática:

#include <cstdlib>

#include <iostream>

using namespace
std;

class C{ public:
C(){cout<<"oi!"<<endl;}};

int main(int argc, char *argv[]){

C v1, v2;

C ac[5];

system("PAUSE");

return EXIT_SUCCESS;

}

No programa acima o construtor C() é invocado duas vezes para a construção
das instâncias v1 e v2 e invocado cinco vezes para a
construção do arranjo ac.

Se o programador não definir construtores o compilador provê um
construtor chamado “default constructor”.
Por exemplo a classe X a seguir

class X{};

não tem nenhum construtor definido e
portanto o compilador irá definir um “construtor default”. A
classe X a seguir

class X{public: X(int
p){}};

definiu um construtor e neste caso o compilador
não vai definir um construtor default. Observe que uma
definição de instância desta última classe X da
forma

X v(2);

Compila de maneira esperada. Mas uma definição da form
X v; dá
erro de compilação e a mensagem informa que não há
construtor default para ser invocado. Um construtor sem parâmetros ou um
construtor com parâmetros inicializados com valores “default”
são considerados construtores default.

class X{public: X(){cout<<"sem!"<<endl;}

X(int i=3){cout<<i<<endl;}};

A definição desta classe X acima é aceita pelo
compilador, mas uma definição que envolva a chamada do construtor
default tal como X v;  causa erro e a
mensagem informa que existe ambiguidade com relação à
escolha do construtor default.

Uma vez que podemos ter vários construtores com diferentes tipos e
números de argumentos, devemos lembrar que existe uma espécie de
fato notável sobre a definição de instâncias.
Considere as seguintes definições:

X v1(a,b,c), v2(d,e), v3(f), v4;

Podemos considerar que a classe X tem um construtor com valores default ou
então que a classe X tem vários construtores;  alguém menos familiarizado com
C++ poderia querer definir a variável v4 com abre e fecha
parênteses:

X v4();

Mas esta expressão corresponde a declarar um protótipo de
função com nome correspondente a v4 e que devolve valores do tipo
X. Este tipo de confusão é agravado em função da
sintaxe do uso explícito de construtores.

A linguagem C++ define os operadores new e delete
para alocar e desalocar memória, conforme o
seguinte trecho de programa:

int *pi=new
int;

*pi=123;

cout<<*p1<<endl;

A linguagem C++ tem uma sintaxe de definição e
inicialização:

int *pi=new
int(123);

cout<<*p1<<endl;

A devolução da area de memória obtida via new é feita
utilizando o operador delete:

int *pi=new
int(123);

cout<<*p1<<endl;

delete pi;

X *p1, *p2, *p3, *p4;

p1=new X(a,b,c);

p2=new X(d,e);

p3=new X(f);

p4=new X(); // não podemos usar abre/fecha parênteses para
declaração direta

// mas podemos para declaração anônima

São estas idiossincrasias que tornam o aprendizado de
programação mais complexo, houve aí um “choque
sintático”.

Operador resolução de
escopo (::)

A definição de comportamento em uma classe se faz
através da definição de métodos e a
definição de métodos em uma classe pode ser feita dentro
do corpo da classe ou fora dele:

(a) class
NomeClasse{... void comportamento(){<<definição>>};
.. };

(b) class NomeClasse{...
void comportamento(); ...};

void
NomeClasse::comportamento(){<<definição>>};

Para definir um método fora do corpo de uma classe temos que fazer
uso do operador :: (operador de escopo ou operador de
resolução de escopo ou operador dois pontos dois pontos) observe
o seu uso na definição dos métodos obterFace()
e rolar() da classe Dado. Este operador pode ser utilizado em várias
diferentes situações.

Podemos também definir um construtor fora da classe:

class NomeClasse{...
NomeClasse(); ...};

NomeClasse::NomeClasse(){<<definição>>};

Na definição dos elementos de uma classe podemos utilizar os chamados rótulo de acesso:

·
private

·
protected

·
public

As recomendações sobre quais rótulos utilizar em quais
ocasiões serão vistas em disciplinas futuras. Nesta disciplina
vamos discutir de forma introdutória o significado destes
rótulos.

Acesso privado

Na classe Dado acima o atributo face é privado. isto
significa que um trecho de programa que utiliza a classe Dado não pode
acessar este atributo:

Dado d1;

d1.face=3; //não compila

Acesso publico

Na classe Dado acima os métodos mostrarFace()
e rolar() são públicos e podem ser invocados a partir das
instâncias.

Acesso protegido: iremos discutir mais à frente.

Finalmente observe o uso do fluxo (stream) cout:

cout <<
exp1<<exp2<<exp3;

--------

A linguagem C++ permite a chamada sobrecarga de operadores e neste
caso o operador de deslocamento de bits << foi sobrecarregado para
indicar o uso de recursos de entrada e saída. A linguagem C++ de forma
semelhante à variáveis stdin, stdout e stderr disponibiliza as variáveis (objetos) cin, cout e cerr
para dar suporte a operações de entrada e saída.

Considere agora a seguinte variação do programa acima:

#include <cstdlib>

#include <iostream>

#include <ctime>

using namespace
std;

class Dado{

private:

int
face;

static
int cntDados; //declaracao

public:

static
void mostrarQuantosDados(){

cout<< "Temos "<<cntDados<<"
dados\n";

}

Dado(){cntDados++;
srand(time(0));rolar();};

const
static int FACES=6;

int obterFace();

void
rolar();

};

int Dado::cntDados=0;
//definicao

int Dado::obterFace(){return face;}

void Dado::rolar(){face=rand()%FACES + 1;}

int main(int argc, char *argv[])

{

Dado::mostrarQuantosDados();

Dado d1;

Dado::mostrarQuantosDados();

Dado d2;

Dado::mostrarQuantosDados();

system("PAUSE");

return
EXIT_SUCCESS;

}

Elementos de classe (variáveis de classe e métodos de
classe) são indicados pelo uso do modificador static.Neste programa
além da constante FACES e da variável de instância face
temos a variável de classe cntDados.
Além dos métodos de instância obterFace() e rolar() temos o método de classe
mostrarQuantosDados(). As variáveis e
métodos de classe são normalmente acessados utilizando o nome da
classe seguido do operador de escopo seguido do nome do elemento, isto em
contraposição ao acesso aos elementos de instância que
podem ser acessados na forma nome da variável operador (ponto) de seleção seguido de nome do elemento. Apesar do
acesso aos membros estáticos serem feito, normalmente, utilizando o nome
da classe, podemos também utilizar as instâncias para este acesso,
conforme ilustrado no trecho de programa abaixo:

class X{public: static int vs; int vi; };

int X::vs;

int main(int argc, char *argv[]){

X v1, v2;

v1.vi=1; v1.vs=1;

cout<<"X::vs="<<X::vs<<endl;

v2.vi=2; v2.vs=2;

cout<<"X::vs="<<X::vs<<endl;

cout<<"v1.vi="<<v1.vi<<" v1.vs="<<v1.vs<<endl;

cout<<"v2.vi="<<v2.vi<<" v2.vs="<<v2.vs<<endl;

system("PAUSE");

return EXIT_SUCCESS;

}

--------------

Destruidores

Quando o sistema de execução
cria um objeto é feita uma chamada para um dos construtores, este
construtor deverá fazer as inicializações e o objeto
passará a existir de forma adequada. Na linguagem C++ é provido
também o destruidor ou demolidor ou “desfazedor” que será
invocado quando um objeto for destruído (por exemplo
quando houver a destruição do registro de ativação
onde o objeto foi alocado). Os destruidores são indicados através
de um til antes do nome da classe.

Verifique o seguinte trecho de programa:

class C{ public: C(){cout<<”ola!!!\n”;};
~C(){cout<<”adeus\n”;}};

void f(){ C v; }

...

main() { ... f(); ...}

Quando um arranjo de instâncias
é destruido o destruidor de cada
instância é invocado. No programa abaixo os construtores
são invocados na construção do registro de
ativação de f() e quando o registro de ativação de
f() é destruiído anteriormente a isso
são invocados os destruidores de cada instância (em ordem
inversa!).

#include <cstdlib>

#include <iostream>

using namespace
std;

class C{private:
int id; static int cntId;

public: C(){id=cntId++;
cout<<"Criado:"<<id<<endl;}

~C(){cout<<"adeus de
"<<id<<endl;}};

int C::cntId=0;

//class C{ public: C(){cout<<"oi!"<<endl;}};

void f(){

C v1, v2;

C ac[5];

}

int main(int argc, char *argv[]){

f();

system("PAUSE");

return EXIT_SUCCESS;

}

Ponteiros para objetos, new
& delete

Quando
utilizamos variáveis da forma acima a
alocação do objeto é automática. Mas quando
utilizamos ponteiros para objetos podemos utilizar para a criação
de objetos o operador new.
Quando quisermos retornar para o sistema a área de memória de um
objeto instanciado via new
usamos o operador delete:

#include <cstdlib>

#include <iostream>

using namespace
std;

class C{ char
eu;

public:

C(char
c){eu=c; cout<<"ola!
sou "<< eu <<"\n";};

~C(){cout<<"adeus de "<< eu
<<"\n";}};

void f(){

C v('v'), *p;

p=new C('p');

/*...*/

delete
p;

}

int main(int argc, char *argv[]){

f();

system("PAUSE");

return EXIT_SUCCESS;

}

O programa acima ilustra que podemos
selecionar um construtor utilizando abre e fecha parênteses logo
após o nome da variável e dentro do abre parênteses
parâmetros reais que identifiquem o construtor. No programa acima a
criação do objeto correspondente a v é feita no contexto
do registro de ativação da função f. A criação
do objeto correspondente a p é feita em um espaço/escopo
conhecido como “espaço de alocação
dinâmica”. Quando termina a execução da
função f seu registro de ativação é
destruído e a destruição de v é efetivada
concomitantemente. O término da execução da
função f destroi a variável p mas não destroi o objeto
apontado por p. Se removermos a remoção do objeto apontado por p
(se deletarmos a linha “delete
p;”) o objeto não será mais acessível e
ocorre o que é denominado um “vazamento de memória”.
Na linguagem C++ o programador deve ter cuidado de não deixar acontecer
“vazamentos”.

A palavra chave “this”

A
linguagem C++ disponibiliza para o programador de uma classe um campo
denominado “this” onde fica um ponteiro
para o objeto que está sendo instanciado. Este campo (que só faz
sentido nos coonstrutores e nos chamados
“métodos de instância” ou  “métodos não
estáticos”)  pode ter
vários usos, um deles é para, por exemplo, desambiguar nomes:

class X{public:

class X{public:

int a;

X(int a){this->a=a;};

int obtera(){return a;};};

/* . . . */

X
v(20);

cout<<v.obtera()<<"\n";

No
programa acima o “this” desambigua o
parâmetro de nome “a”  e o membro de mesmo nome. A
linguagem C++ disponibiliza também um campo denominado “super” onde fica um ponteiro para o objeto

---

Na
linguagem C++ definição dos métodos de uma classe permite
o uso de um mesmo nome desde que a “assinatura” (signature), ou seja, número e tipo dos argumentos seja diferente. A definição de métodos
dentro de um mesmo escopo com mesmo nome é conhecido como sobrecarga (overload). O programa abaixo exemplifica uma classe com
sobrecarga do método f:

#include <cstdlib>

#include <iostream>

using namespace
std;

class C{public:

int f(){cout<<"f()"<<endl;}

int f(int i){cout<<"f(int)"<<endl;}

int f(double d){cout<<"f(double)"<<endl;}

};

int main(int argc, char *argv[])

{

system("PAUSE");

return EXIT_SUCCESS;

}

O
programador da classe C sobrecarregou o método f. Exercício:
escreva o trecho da função main() que utiliza C e invoca as diferentes assinaturas de f.

No
material à frente, sobre relação de herança entre
classes, veremos que uma classe pode ter uma função que
“passa por cima” (override)  ou sobrescreve
uma função de mesmo nome herdada de uma classe mãe. Os
conceitos de sobrecarregar e sobrescrever têm semelhança
(definições de funções com mesmo nome) mas certamente são bem distintos (sobrecarga: mesmo
nome no mesmo escopo de nomes & sobrescrita: mesmo nome em escopos
diferentes). A definição de vários construtores
sobrecarrega o nome da classe.

Vários
tipos de operações são prédefinidas
para as classes e sua instâncias. Em particular
é possível fazer a atribuição de instâncias:

class C{/* definição de C*/};

...

C v1,v2;

v1=v2;

Se
o programador não definir de forma explícita o significado da
atribuição será executada uma
função/operação default ( o
termo correspondente a este conceito é “copy
assignment operator”)
que simplesmente faz atribuição para os campos da instância
v1 dos valores dos campos correspondentes da instância v2. Este tipo de
atribuição default é as vezes
referida como sendo “atribuição membro a membro” (memberwise assignment). O
programador pode definir uma função (operator=)
que implementa uma semântica de atribuição
diferente da atribuição membro a membro. A
inicialização de em objeto na hora da sua criação
é operada de maneira diferente da atribuição. Considere o
seguinte trecho de programa:

class C{/*definição de C*/};

...

C v1;

C v2=v1;

No
trecho acima o objeto v1 é3 inicializado através do construtor
sem parâmetros conforme já discutido

(C v1; equivale a C v1=C(); ) . A
definição do objeto v2 é feita através de uma
semântica distinta da criação do objeto v1.

(C
v2=v1; equivale a C
v2=C(v1);). Se o programador não definir um
“construtor cópia” (Copy constructor) o compilador fornece um onde a semântica
é similar a atribuição membro a
membro. A forma de um construtor cópia de uma classe C é
tipicamente C(const
C &p){/* definição */}
(também pode ser C(C
&p){/*definição*/})

Resumindo,
a linguagem C++, mais tradicional, prevê quatro “membros
especiais” para as classes: (i) o construtor default, (ii) o destrutor (ou destruidor), (iii) o construtor cópia e o (iv)
operador de atribuição de cópia. A tabela abaixo exemplifica
cada um deles:

construtor “default”

C::C()

detrutor

C::~C()

construtor cópia

C::C(const C&)

operador de atribuição de cópia

C&
operator=(const C&)

Os
projetistas de C++ edição 2011 incluiram
dois novos membros especiais:

“move
constructor”

C::C(C&&)

“move
assigment”

C&
operator=(C&&)

Nesta
disciplina não detalharemos estes dois últimos membros. O
entendimento do construtor cópia e operador de atribuição
de cópia dependem do material sobre “referências”(denotado pelo uso do caractere &), além
disso o entendimento do operador de atribuição de cópia
depende do material relacionado a “sobrecarga de operadores”.

Exercícios:

Escreva
um programa em C++ que define uma classe “Lampada”.
O estado da lâmpada deve ser representado de forma privada mas deve ser acessível através dos seguintes
métodos de instância: estaAcesa(), estaApagada(), acender(), apagar(). A classe deve manter o
registro de quantas lâmpadas (instâncias/objectos)
existem.  O programa deve demostrar
que a classe monitora as instâncias e use o destruidor para decrementar o
número de instâncias!

Escreva
um programa em C++ que define uma classe Cubo e instancia alguns diferentes
cubos.  A classe deve ter pelo menos
os seguintes métodos públicos de instância:

double obterArea(),  double obterVolume() e double obterLado();

A
classe deve ter o seguinte método estático

static
int totalDeCubos()

que devolve quantos cubos já foram instanciados.

Por
agora considerar apenas uma unidades de medida
"default”.

A
classe deve poder atender a estes usos:

Cubo c1; //instancia um cubo com lado
“default

Cubo c2(2.0); //instancia um cubo com
lado de duas unidades

cout<<c2.obterVolume()

imprime 8.0

Escreva
um programa em C++ que define uma outra forma
geométrica similarmente à classe Cubo acima.

Faça
alguns programas em C++ com tema envolvendo duas classes: classe Ponto e classe
Retangulo. A classe Ponto deve ter um construtor que
funciona da seguinte maneira:  Ponto p(1.0, 2.0); Na
definição da Retangulo utilize a classe
ponto e defina pelo menos dois construtores, um construtor deve ter como
parâmetro o ponto superior esquerdo e o ponto inferior direito. O outro
construtor deve ter quatro parâmetros do tipo double os dois primeiros definindo o ponto superior
esquerdo e os dois seguintes o ponto inferior direito.

Escreva
um programa em C++ que define uma classe BombaDeCombustivel...quais
seriam os métodos?

Escreva
um programa em C++ que define uma classe PostoDeCombustivel.
A classe deve ter pelo menos os seguintes métodos públicos

bool temGasolina(),
tem Diesel()  etc

double precoTotal()...
etc

Estude
como que você deve fazer para cada instância de posto poder ter uma
composição de 1 a N instâncias de BombaDeCombustivel; mude o que for preciso na classe BombaDeCombustivel para poder operar com instâncias
de PostoDeCombustivel. Inicialmente faça um
programa bem simples.

Escreva
uma classe chamada Fracao. O construtor de uma
fração deve poder receber dois valores do tipo int: Fracao f1(3,10). A classe
deve ter métodos estáticos (soma, divide etc)
que recebem duas instâncias de Fracao e devolve
uma nova instância: Fracao f1=Fracao::soma(new Fracao(3,10), new Fracao(4,5)).
Esta classe deverá ser estendida quando for discutido o material de
sobrecarga de operadores! E deverá ser sobrecarregado o operador soma
(+) para poder ser escrito Fracao f1=(new Fracao(3,10))+(new Fracao(4,5));

Explique
porque a classe abaixo não compila.

class A{private: int
x; public: static void f(int x){this->x=x;}};

Faça
um desenho, seguindo as convenções de aula, da estrutura criada a
partir da variável pb
de acordo com o trecho de programa abaixo.

class Nodo(int info;

public:

Nodo *prox;

static int cnt;

Nodo(int i, Nodo
*p){info=i; prox=p; cnt++;}};

int Nodo::cnt=0;

...

Nodo a(10,NULL);

Nodo *pb=new
Nodo(20, &a);