Programar em C++/Classes

De Wikibooks

Esta página precisa ser reciclada (discuta).
Ao melhorá-la, você estará ajudando o Wikilivros.

Tabela de conteúdo

[editar] Classes

Existem duas categorias de tipos de dados usuais em C++, são classificados como tipos básicos e tipos definidos pelo programador, assim como na linguagem C, podemos definir dados compostos por associações dos tipos básicos, estes tipos são chamados de structs. C++ traz uma nova representação de dados, muito semelhante na forma às structs, porém diferentes na forma conceitual, a palavra chave class, pode ser usada para criar tipos de objetos.

Antes de prosseguirmos, vejamos um pouco sobre o conceito por tras do uso de objetos... Um objeto é entendido como uma entidade de dados dentro da memória que, basicamente, deve ser responsável por seu conteúdo, ou seja, um objeto deve ser capaz gerenciar seu conteúdo autonomamente, ou prover meios de outras entidades de código fazê-lo de forma segura.

[editar] Origem (atributos)

Observemos, por exemplo, o código abaixo:

struct MyData
{ int    n;
  char   data[10];
  float  nReal;
};

Esta declaração, bem conhecida de quem já está familiarizado com a linguagem C, cria um tipo de dado composto, que neste exemplo chamamos de MyData, o que acontece aqui é que os dados estão agrupados detro destra estrutura, isto promove a possibilidade de manipulá-los em conjunto. Um dos problemas com esta estrutura é a presença de uma matriz de caracteres chamada "data", observe que a mesma tem um tamanho definido de 10 caracteres, imagine que em algum momento da execução do programa tentamos colocar um caractere na posição 11, ou qualquer posição fora da matriz, neste caso estamos colocando o referido dado em endereços inválidos para a operação que pretendemos realizar, ou seja, não há controle nenhum que assegure que o código não fará um acesso fora da área que pertença a matriz. Um acesso de memória a qualquer elemento da matriz acima da posição 9, fará com que invadamos dados na área onde a variável nReal está definida.

[editar] Funções membro (Métodos)

Agora suponha que tenhamos como definir um modo para entrar e outro para ler dados da matriz:

struct MyData
{ int    n;
  char   data[10];
  float  nReal;
 
  bool write_data(int pos, char c)
    { if (pos >= 0 && pos < 10) 
          {   data[pos]=c;
              return true;
          }
      return false;
    }
 
 char read_data(int pos)
    { if (pos >= 0 && pos < 10) 
          {  return data[pos];
          }
      return '\0';
    }
};

Agora temos assegurados métodos de inclusão e acesso a dados da matriz de caracteres, porém ainda existe um pequeno problema: Quem quiser o antigo método de acesso direto conseguirá facilmente, pois os elementos da estutura estão acessíveis publicamente por padrão.

[editar] Conceituação

O problema da visbilidade pública dos dados em uma estrutura pode ser resolvido com um dos conceitos de objetos, o encapsulamento. Encapsular os dados, significa reservar o acesso a funções que estejam dentro de um grupo restrito, especializado para tais operações de manipulação destes dados. Uma das vantagens deste procedimento é que o código adquire um formato mais modularizado, onde os processos tornam-se claramente distintos, caso tenhamos que analisar o código, cada procedimento estará restrito a partes definidas para cada operação.

[editar] Declarar classes

As estruturas são bem parecidas com as classes, com uma pequena diferença, peguemos o caso da passagem de estruturas como argumentos de funções

 #include <iostream>
 #include <string>
 
 using namespace std;
 
 class Person 
 {
   string name; 
   int height;
 };
 
 void setValues(Person&);
 void getValues(const Person&);
 
 int main ()
 {
   Person p1;
   setValues(p1);  
   cout << "Informando dados sobre a pessoa:\n";
   cout << "================================\n";
   getValues(p1);
#ifdef _MSC_VER
   system ("pause");
#endif
   return 0;
 }
 
 void setValues(Person& pers)
 {
   cout << "Informe o nome da pessoa: ";
   getline(cin, pers.name);
   cout << "Informe a altura em milímetros: ";
   cin >> pers.height; 
   cin.ignore();
 }
 
 void getValues(const Person& pers)
 {
   cout << "Nome da pessoa: " << pers.name << endl; 
   cout << "A altura da pessoa em milímetros é: " << pers.height << endl;
 }


  • Mudamos o identificador de struct para class
  • Mas se fizermos rodar o programa isto vai dar um erro de compilação, porque agora temos variáveis membro que são privadas por padrão, estas não são vistas por funções fora da classe.


Dentro de uma classe podemos definir diversos modos de visibilidade de variáveis e funções.

As modalidades podem ser:

  • private (só podem ser acedidas funções membros da mesma classe)
  • public (pode ser acedida por qualquer parte do programa)
  • protected (esta vamos avançar quando falarmos em derived class – vamos deixar esta de lado por agora).

Ora como a função getValues e setValues não são membros da classe Person, tal como o constructor Person, não conseguem aceder ás variáveis name e height

Representação

Class Person private
string name Int height
p1

A solução é criar funções publicas, para ler de e escrever para as variáveis private

#include <iostream>
#include <string>
using namespace std;
 
class Person 
  {
  private:
     string name; 
     int height;      
  public:
     string getName() const;
     void setName(string);
     int getHeight() const;
     void setHeight(int);
  };
 
  string Person::getName() const
  { return name; }
 
  void Person::setName(string s)
  { 
     if (s.length() == 0)
        name = "No name assigned";
     else
        name = s; 
  }
 
  int Person::getHeight() const
  { return height; }
 
  void Person::setHeight(int h)
  { 
     if (h < 0)
        height = 0;
     else
        height = h; 
  }
 
void setValues(Person&);
void getValues(const Person&);
 
int main ()
{
  Person p1;
  setValues(p1);  
  cout << "Outputting person data\n";
  cout << "======================\n";
  getValues(p1);
  return 0;
}
 
void setValues(Person& pers)
{
  string str;
  int h;
  cout << "Enter person's name: ";
  getline(cin,str);
  pers.setName(str);
  cout << "Enter height in milimeters: ";
  cin >> h;
  cin.ignore();
  pers.setHeight(h);
}
 
void getValues(const Person& pers)
{
  cout << "Person's name: " << pers.getName() << endl; 
  cout << "Person's height in milimeters is: " << pers.getHeight() << endl;  
}


Mas perguntam? Porque é que nos demos ao trabalho de recorrer aos private em vez de fazer tudo como public? Quando tínhamos a estrutura em vez da classe, não havia nada a impedir a colocação de valores inválidos, por isso poderíamos ter valores vazios para a string e valores negativos para op height:

Agora que a Person é uma class, as funções membro podem realizar a validação dos dados antes antes da colocação de valores nas variáveis. Poderíamos fazer com que a função setName verificasse a a entrada na string era vazia e se sim colocar “no name assigned”. similarmoste poderíamos ter a setHeight a verificar se eram colocados de input valores negativos e se sim colocar zero.


Isto tudo ainda demonstra outra coisa: --o conceito de encapsulação e/ou data-hiding

[editar] Declarar

Podemos declarar os objectos logo a seguir á definição da classe (ver 1º caso) como declará-lo depois (ver caso 2)


1º caso:

class CRectangle 
{
  int x, y;
 public:
   void set_values (int,int);
   int area (void);
 } rect;	

2º caso:

class CRectangle 
{
   int x, y;
 public:
   void set_values (int,int);
   int area (void);
 }
 
int main()
{
 CRectangle rect;
}


Em ambos os casos temos

CRectangle Private: public:
int x int y void set_values (int,int); int area (void);
rect

Ora nós podemos entender os objectos como algo que têm certas características (variáveis) e que podem fazer algo. Nós agora sempre que utilizarmos os objectos só os temos de chamar, e não necessitamos de saber exactamente como é que eles funcionam. A ideia é a analogia a uma resistência: sabemos que temos de usar uma resistência, e que ela deve ter umas certas características, então basta-nos olhar para as listinhas de cores das resistências. Não necessitamos de saber de que é que ela é feita, ou como é que é feito o seu processo interior. é o chamado data hiding.

Vejamos o exemplo: Nós agora vamos mostrar que podemos ter variáveis membro apenas como prototypes e definir essas variáveis membro fora da classe

// classes example
#include <iostream>
using namespace std;
 
class CRectangle 
{
   int x, y;
 public:
   void set_values (int,int);
   int area () {return (x*y);}
};
 
void CRectangle::set_values (int a, int b) 
{
 x = a;
 y = b;
}                 
 
//repare no “::” que pemite-nos definir a função membro da classe CRectangle fora da classe
 
int main () 
{
 CRectangle rect;     //definimos objecto de classe
 rect.set_values (3,4);       //objecto-membro
 cout << "area: " << rect.area();
 system (“pause”); 
 return 0;
}	



// classes example
#include <iostream>
using namespace std;
 
class CRectangle 
{
   int x, y;
 public:
   void set_values (int a,int b)
   {
    x = a;
    y = b;
   }
   int area () {return (x*y);}
};
  
int main () 
{
 CRectangle rect;         //definimos objecto de classe
 rect.set_values (3,4);       //objecto-membro
 cout << "area: " << rect.area();
 system ("pause"); 
 return 0;
}


area: 12


Porque é que nós temos a modalidade private e public, porque assim permite-nos separar os detalhes de como os dados são gravados do modo como eles são usados. Repare que se criarmos uma função para dar os dados retidos numa variável; uma alteração nos dados fará que tenhamos de alterar uma função que esteja a chamar a classe. e assim se criarmos a função que retorna os dados, cria redundância sim mas permite que depois não tenhamos de alterar a função que esteja a chamar a classe.


Vejamos o exemplo:

class Dog 
{
public:
   void setAge(int age);
   int getAge();
   void setWeight(int weight);
   int getWeight();
   void speak();
private:
   int age;
   int weight;
};
void Dog::setAge(int age)
{
   this->age = age;
}
int Dog::getAge()
{
   return age;
}
void Dog::setWeight(int weight)
{
   this->weight = weight;
}
int Dog::getWeight()
{
   return weight;
}
void Dog::speak()
{
   cout << "BARK!!" << endl;
}

Este exemplo é bom porque: Temos methods implementados fora da definição de classe. ie definimos os methods fora, embora tenhamos obrigatoriamente de colocar o prototype da função dentro da classe. Conseguimos implementa-los fora da classe porque recorremos ao símbolo :: cada objecto tem o ponteiro this (que vamos aprofundar daqui a pouco) que é um ponteiro especial

[editar] Definição de class

Usa-se a palavra class para criar uma classe Seguindo-se depois o nome (specifier) que se queira dar á classe E depois a definição da classe em braces

A definição contém:

  • os class members (os dados, as variáveis)
  • os class methods (as funções)

Vamos acompanhar com um exemplo: Vamos fazer o design de uma classe chamada de “Image” que será usada para guardar e manipular uma imagem.

Primeiro perguntamos o que é necessário para guardar uma imagem e depois que tipo de manipulações é que necessitamos.

Então a imagem tem de largura e altura medida em pixeis 400 por 300. Cada pixel tem as propriedades de cor e imagem. a cor é composta por 3 cores separadas vermelho, azul e verde, numa escala de 0 a 1. Portanto vamos necessitar de data members para guardar estas informações

agora os methods. vamos primeiramente assumir que temos a restrição de <=400 pixeis, e estes valores serão feitos pelo constructor na criação do objecto. nós não precisamos dos methods para estipular a altura e largura mas vamos querer para obter e ler os valores. depois também é uma maneira para ter os valores de um determinado pixel e a sua localização

a primeira versão então seria

class Image {
public:
   int getWidth();
   int getHeight();
   void setX(int x);
   int getX();
   void setY(int y);
   int getY();
   void setRed(double red);
   double getRed();
   void setBlue(double blue);
   double getBlue();
   void setGreen(double green);
   double getGreen();
private:
   int _width;
   int _height;
   int _x;
   int _y;
   double _red[400][400];
   double _blue[400][400];
   double _green[400][400];
   boolean isWithinSize(int s);
   double clipIntensity(double brightness);
};


[editar] access specifiers

  • temos as palavras private e public – são os chamados Access specifiers

private – indica que a member or a method pode ser acedido somente pelos class methods e não por outras partes do programa ou outras classes. public – indica que um member ou a method pode ser acedido por outras partes do programa usando os objectos da classe. a este método de limitar o acesso e manipulação dos membros chama-se de Data Hiding. o bom desenho de classes deve sempre forçar a datahiding – é raramente necessário ou desejável ter acesso directo aos dados internos de uma classe. o data hiding consegue dois objectivos:

  1. os utilizadores da classe não necessitam de se preocupar com a representação interna dos dados, ie, como ela é armazenada, ie, não necessitam de saber se os dados são gardados via 2 arryas ou um array ou outra forma qq.
  2. se a representação interna dos dados for modificada, desde que as tipologias de retorno e o numero de parâmetros dos public methods manterem-se, não necessitamos de alterar código que esteja a utilizar a classe.

ou seja datahiding simplifica a programaçõ escondendo o inner workings da classe e protege o utilizador de alterações á classe

usualmente private methods são helper functions a outros methods da classe.

se nenhum acess specificer for usado, todos os members e methos têm private Access por defaut


Há dois métodos para definir os methods

  • Eles podem ser definidos dentro da classe, o que é apropriado para methods pequenos
  • E methods grandes podem ser definidos fora da classe.

neste caso terão de ser identificados como pertencentes á classe e para isso vamos utilizar o scope resolution operator “::”



[editar] Constructors and destructors

Quanto aos objectos. Para os criarmos fora das classes temos de utilizar os constructors (até aqui não os utilizamos porque eles estavam invisíveis, eram criados automaticamente)

constructor não podem ser chamados explicitamente como fossem funções membro regulares. Elas são apenas executadas quando um novo objecto da classe é criado. Os constructors não têm qualquer tipologia de retorno

O desconstructor terá o mesmo nome da classe mas precedida pelo sinal tilde “~” e retorna nenhum valor

Os constructors são funções membro (method) especiais de uma classe. Permitem a inicialização de uma variável membro de uma classe. Ou melhor permitem a construção e a inicialização de objectos nas classes. Se não os declararmos o compilador faz isso por nós. e estes construtores têm sempre o mesmo nome que a classe

permitem a iniciação de variáveis, permitem a alocação dinâmica de memória ou fazem a reserva de recursos

O destructor

  • O destructor é chamado quando se quer destruir o objecto.
  • É usado para libertar qualquer memória que tenha sido alocada

Façamos de novo a classe Dog com o constructr e o destructor.

class Dog 
{
public:
   Dog();      //Constructor
   ~Dog();    //Destructor
   void setAge(int age);
   int getAge();
   void setWeight(int weight);
   int getWeight();
   void speak();
private:
   int age;
   int weight;
};
 Dog::Dog()		
{
   age = 0;
   weight = 0;
   cout << "Dog Constructor Called" << endl;
}
Dog::~Dog()
{
   cout << "Dog Destructor Called" << endl;
}

Repare que o

  • Constructor tem o mesmo nome que a classe
  • O destructor tem o mesmo nome que a classe com o prefixo de tilde” ~”
  • O constructor foi usado para inicializar as variáveis membro, mas noutros exemplo poderia alocar memória, tomar controlo de recursos como system devices e executar complicadas iniciações de código
  • o destructor no exemplo não faz nenhuma acção real, para além de fazer o eco á que foi chamada


[editar] copy constructors

Um copy constructor é um constructor especial que toma como argumentos a referência de um objecto da mesma classe e cria um novo objecto que é a copia. Por defaut, o compilador providencia a copy constructor que performa a cópia membro por membro do objecto original. Isto é chamado de shallow copy ou member wise. Porém em algumas situações isto não é satisfatório, para ver isso vamos ver a class employer

#include <iostream>
using namespace std;
 
class Employee 
{ 
  public:
    Employee(char *name, int id);
    ~Employee();
    char *getName(){return _name;}
  private://Other Accessor methods
    int _id;
    char *_name;
};
 
Employee::Employee(char *name, int id)
{
   _id = id;
   _name = new char[strlen(name) + 1];        //Allocates an character array object
   strcpy(_name, name);
}
 
Employee::~Employee()
{
   delete[] _name;
}
 
int main()
{
   Employee programmer("John",22);
   cout << programmer.getName() << endl;
   return 0;
}

a função strlen retorna o tamanho da string passada pelo constructor. Repare que o nome do employee é agora guardado num carácter array dinâmico. é o string lenght +1 para permitir o null terminator usado no estilo c.

a função strcpy automaticamente adiciona o null terminator a´string destino.

Note também que o destructor liberta a memoria usada para guardar o employee name, para evitar memory leak.

agora imagine que o john é promovido

int main()
{
   Employee programmer("John",22);
   cout << programmer.getName() << endl;
 
   //Lots of code ....
 
   Employee manager(&programmer);
       //Creates a new Employee "manager",
       //which is an exact copy of the 
       //Employee "programmer".
 
   return 0;
}

este programa contem um bug sério. e morre com uma excepção quando corrido. o problema é que o wise members defaut copy constructor está a ser usado para criar um object “mamager”. ,mas ele copia o endereço no ponteiro _name in manager.

nos temos 2 pointers ambos contendo o mesmo endereço. imagine que agora um novo empregado é contractado. quando o nome for actualizad, não apenas iremos alterar o nome do empregado mas também do manager





Ferramentas pessoais
Criar um livro
  • Adicionar página wiki
  • Ajuda de colecções