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). O desenho acima tem uma simplificação que é a mistura de código e dados da classe em uma mesma região. Normalmente o código das funções das classes ficam junto ao código das funções (por exemplo o código da função main()) e os dados de classe ficam junto às variáveis globais. As variáveis declaradas dentro das funções (não estáticas) ficam nos respectivos registros de ativação.
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(/*parâmetros*/){/*corpo*/}};
Os construtores são elementos de uma classe “parecidos” com funções, mas sempre têm o nome da classe e não retornam valores. Uma classe pode definir vários construtores e diferem entre sí pelo número e tipo dos parâmetros:
class NomeClasse{ ...
NomeClasse(){/*corpo*/} /*estilo do construtor default */
NomeClasse(int p){/*corpo*/}
NomeClasse(double d){/*corpo*/}
NomeClasse(int p1, int p2){/*corpo*/}
...};
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/Destrutores
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” ou destrutor, 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. Uma classe possui 1 ou mais construtores mas apenas 1 destrutor.
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”, ou “heap”. 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 destrói a variável p mas não destrói o objeto apontado por p. Se removermos a devolução da memória 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() |
|
destrutor |
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”.
Na linguagem C um mecanismo típico de inicialização é o uso do sinal igual (=) na definição da variável, p.ex., int ii=1234; A linguagem C++ desde sua concepção introduziu um estilo de inicialização com o uso de parênteses e uma inicialização do tipo C v(20); (invocar construtor para variável v do tipo C) que foi estendida mesmo para tipos simples, p.ex. int ii(1234); Mas alguns problemas ainda permaneceram, a partir de 2011 a especificação da linguagem C++ foi estendida para dar abrigo à chamada “inicialização uniforme” (“uniform initialization”) A nova sintaxe permite uma inicialização do tipo C v{20}; e ela foi estendida para tipos simples, p.ex., int ii{1234};
int
ii=1234; //estilo C
int jj(1234);//estilo construtor
int kk{1234};//estilo 2011 inicialização uniforme
cout<<ii<<" "<<jj<<"
"<<kk<<endl;
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/objetos) existem. O programa deve demostrar que a classe monitora as instâncias; 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);