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.
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).
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;
}
--------------
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;
}
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 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”.
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);