Programar em C++/Classes

Origem: Wikilivros, livros abertos por um mundo aberto.
Ir para: navegação, pesquisa


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

Classes[editar | editar código-fonte]

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 estruturas (structs). C++ traz uma nova representação de dados, muito semelhante na forma às estruturas, porém diferentes na forma conceitual: a palavra chave class, que é usada para criar uma classe de objetos mais rica que as structs. Ao declararmos um identificador, tal qual fazemos ao declarar uma variável e no lugar do tipo especifiquemos uma classe criaremos um objeto.

Antes de prosseguirmos, vejamos um pouco sobre o conceito por trás 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 de gerenciar seu conteúdo autonomamente, ou prover meios de outras entidades de código fazê-lo de forma segura.

Origem (atributos)[editar | editar código-fonte]

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 heterogêneo, que neste exemplo chamamos de MyData, o que acontece aqui é que os dados estão agrupados dentro desta 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.

Funções membro (Métodos)[editar | editar código-fonte]

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 estrutura estão acessíveis publicamente por padrão.

Conceituação[editar | editar código-fonte]

O problema da visibilidade 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 organizado, 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.

Declarando classes[editar | editar código-fonte]

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);
   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 tentarmos compilar o programa isto vai causar erros 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 acessados por membros da mesma classe)
  • public (pode ser acessadas fora do objeto, onde este estiver definido)
  • protected (deixemos esta para quando falarmos em classes derivadas, pois depende deste conceito).

Ora, como as funções getValues e setValues não são membros da classe Person, tal como o construtor Person, não conseguem acessar as variáveis "name" e "height".

Visualizamos melhor em forma de tabela:

Class Person private
string name
Int height

p1

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

 #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: Por que é que nos demos ao trabalho de recorrer a membros privados em vez de fazer todos públicos? Quando tínhamos uma estrutura no lugar de uma classe, não havia nada que impedisse a colocação de valores inválidos, por isso poderíamos ter valores vazios para a string e valores negativos para a variável "height".

Agora que "Person" é uma classe, as funções membro podem realizar a validação dos dados antes da atribuição de valores nas variáveis. Poderíamos fazer com que a função setName verificasse se a entrada na string seria vazia e caso fosse, colocaria um valor padrão como: “sem nome”. similarmente poderíamos ter "setHeight" para verificar se seriam colocados valores de entrada negativos e caso fossem, colocaria zero, ou não tomaria nenhuma ação.

Todas estas características demonstram o conceito de encapsulamento. A sua finalidade é de tornar o código mais modularizado, restringindo o escopo de análise a partes bem delimitadas dos programas. Devido a este conceito podemos contar com códigos mais fáceis de analisar e fazer manutenção.

Instanciando objetos[editar | editar código-fonte]

Instanciação de objetos é o processo de criar a estrutura lógica dos mesmos na memória. Isto ocorre quando declaramos os objetos, pois neste momento todo o processo de construção dos mesmos é efetivado. Assim, toda vez que declaramos um objeto estamos instanciando-o, ou seja, estamos criando uma instância da classe.

Podemos declarar os objetos logo após definir a classe conforme podemos ver no 1º caso logo abaixo. Neste caso teremos a variável rect criada como um objeto conforme estabelecido pelo modelo definido pela palavra chave class. Este tipo de declaração é mais usual para objetos criados globalmente, pois a inclusão desta declaração no cabeçalho pode fazer com que vários objetos sejam criados com o mesmo nome quando o cabeçalho é invocado de vários arquivos. Portanto, é mais prudente usar esta opção quando a declaração está no arquivo fonte e não no cabeçalho.


1º caso:

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

No 2º caso, apresentado logo abaixo, podemos declarar objetos apenas quando precisarmos. Esta opção de declarar o objeto depois é a mais usada, pois na maioria das vezes temos o modelo dos objetos, a classe, declarada em um arquivo de cabeçalho enquanto que os objetos serão criados no resto do código fonte. Desta forma é mais usual criar as classes em cabeçalhos e depois declarar os objetos na parte do programa que for mais conveniente.


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

Podemos, então, entender os objetos como blocos de dados que têm propriedades (variáveis) e que podem fazer algo (métodos). Então, criamos todas as funcionalidades que precisamos que a classe forneça aos programas, fazendo os testes necessários para assegurar sua consistência e estabilidade. Sempre que precisemos utilizar os objetos só temos que instanciá-los (declará-los), e não precisamos nos preocupar como eles funcionam internamente, uma vez que os desenhamos adequadamente.

Para entendermos melhor este conceito podemos fazer uma analogia. Consideremos um objeto resistência: sabemos que temos de usá-lo e que ela deve ter certas características, então teremos o seu valor em Ohms, sua potência máxima, tolerância, entre outras, e teremos uma função que nos dará a corrente que passa por ela quando lhe aplicamos uma tensão elétrica. Não precisamos saber de que é que ela é feita, ou como estas características internas a faz funcionar, basta-nos receber os resultados.

Vejamos o exemplo:

Agora vamos mostrar que podemos ter funções membro apenas como protótipos e defini-las fora da classe. Para isso usamos o operador de definição de escopo :: que permite definir o local do código onde um identificador existe, no formato: ESCOPO::função ou ESCOPO::dado. De maneira geral, quando declaramos identificadores dentro da classe podemos defini-los no escopo global referenciando estes pelo operador de escopo.

 // 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 objeto de classe
  rect.set_values (3,4);       //objeto-membro
  cout << "area: " << rect.area();
  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 objeto de classe
  rect.set_values (3,4);       //objeto-membro
  cout << "area: " << rect.area();
  return 0;
 }
area: 12

O exemplo anterior explora a característica de toda figura geométrica fechada, que possui uma área interna. Observe que este modo de definir a classe coloca o cálculo da área dentro da definição da mesma. Este modo faz com que o código seja apenas um modelo, a função de cálculo da área não será criada se não for usada durante o escrita do resto do programa.


Vejamos outro 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;
 }

Acima podemos ver um modo de declarar as funções apenas como protótipos, que ficam dentro da declaração da classe. Nesse contexto as funções são definidas fora da classe, usando-se o operador "::" para ligar a função à classe. Neste caso teremos as funções definidas e construídas no código das mesmas, enquanto que o modelo da classe poderá permanecer em um arquivo cabeçalho, o que possibilita incluí-lo em qualquer arquivo de códigos fontes do programa.

Definição de classes[editar | editar código-fonte]

Usa-se a palavra "class" para criar uma classe, seguindo-se depois o nome que se queira dar-lhe e finalmente a definição da mesma entre chaves.

A definição contém:

  • os dados ( propriedades );
  • os métodos (as funções membro)

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

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

A imagem possui 400 pixels de largura e 300 pixels altura. Cada pixel tem as propriedades de cor e imagem. A cor é composta por: vermelho, azul e verde, numa escala de 0 a 2^{64}. Portanto vamos necessitar de membros para guardar estas informações

Agora planejemos os métodos. Vamos, primeiramente, assumir que temos a restrição de <= 400 pixeis, e estes valores serão feitos pelo construtor na criação do objeto. Nós não precisamos dos métodos para estipular a altura e largura, mas vamos precisar para obter e ler os valores. Esta estratégia também nos ajudará a manter 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);
 };

Especificadores de acesso[editar | editar código-fonte]

  • Temos as palavras reservadas private e public – são os chamados especificadores de acesso.
    • private – Especifica uma faixa de variáveis ou funções que podem ser acessadas exclusivamente por membros da classe, de forma que nenhum outro código fora da mesma possa acessá-las;
    • public – Especifica uma faixa de variáveis ou funções que podem ser acessadas por qualquer código no programa, sendo que para as funções internas da classe não é necessário especificar o objeto enquanto que para as outras partes do programa é necessário especificar o objeto a qual estas pertencem.

Esta característica de limitar o acesso e manipulação dos membros de uma classe chama-se Encapsulamento. A boa prática no desenho de classes deve sempre forçar o encapsulamento. É raramente necessário ou desejável ter acesso livre e público aos dados internos de uma classe.

O encapsulamento visa, primariamente duas metas:

  1. Eliminar a necessidade de conhecimento da estrutura interna por quem deseja usá-la. Por exemplo, se os objetos precisam manter um conjunto de 4 bytes, isto pode ser conseguido usando-se duas variáveis short int, uma int, um vetor com 4 characteres, ou a variação de qualquer um dos anteriores sem sinal, mas estes detalhes não precisam estar expostos.
  2. Se a representação interna dos dados for modificada, desde que os tipos de retorno e de parâmetros das funções públicas mantenham-se inalteradas, não necessitemos de alterar código que utilizem objetos da classe.

Ou seja, o encapsulamento simplifica a programação escondendo as particulariadades da classe e elimina o retrabalho do código por alterações da mesma. Geralmente as funções (métodos) privadas, são auxiliares a outras funções da classe.

Se nenhum especificador de acesso for usado, todos os membros e metodos são declarados como privados por padrão.


Há dois métodos para definir as funções membro:

  • Eles podem ser definidos dentro da classe, o que é apropriado para funções pequenas;
  • E funções grandes podem ser definidas fora da classe.

Neste caso terão de ser identificadas como pertencentes à classe e para isso utilizamos o operador de resolução de escopo “::”.

Construtores[editar | editar código-fonte]

Conceito[editar | editar código-fonte]

Os construtores "constructors" são funções membro (métodos) especiais de uma classe. Permitem a inicialização das variáveis membro de um objeto. Ou melhor, permitem a construção e a inicialização de objetos das classes. Se não os declararmos o compilador faz isso por nós. Os construtores têm sempre o mesmo nome que a classe.

Os objetos são construídos através destas funções especiais chamadas de construtores. Até aqui não os declaramos, eram criados automaticamente. Estas funções tem certas características que as fazem distintas das normais, que permitem que as mesmas construam a estrutura lógica inicial do objeto. Desta forma estas funções são características da orientação a objetos e servem para criação dos mesmos.

Construtores não podem ser chamados explicitamente como fazemos no caso de funções membro regulares. Eles são apenas executados quando um novo objeto da classe é criado. Portanto, existe apenas um evento capaz de executar um construtor, a instanciação do objeto.

As principais características dos construtores são:

  • Não têm qualquer valor de retorno;
  • Não podem ser executados por chamada explícita no código;
  • São executados logo depois que os tipos básicos do objeto foram criados;
  • Inicializam os dados com os valores que o objeto precisa para começar a funcionar corretamente.

Declaração[editar | editar código-fonte]

Podemos criar construtores facilmente, através das características que os distinguem das funções membro convencionais. Ou seja, definimos uma função membro que possua o mesmo nome da classe, não tenha tipo de retorno e a declaramos como pública para que possa ser acessada por quem queira instanciar objetos. Vejamos como definir um construtor:

class Caneta
{ string cor;
  int volume;
  ///////////////
  public:
    Caneta( string c, int v );
};
 
Caneta::Caneta( string c, int v )
{ cor = c;
  volume = v;
}

Construtores podem iniciar os membros da classe de uma forma simplificada. Este formato é usado sempre que o construtor tem dados básicos que podem ser iniciados antes do resto da construção da estrutura do objeto. Podemos iniciar os dados do objeto declarando-o desta forma:

class Caneta
{ string cor;
  int volume;
  ///////////////
  public:
    Caneta( string c, int v ) : cor(c), volume(v)
         {
         }
};

Para fazê-lo, como vemos no código, basta listar as variáveis membro em uma sequência depois da declaração do nome do construtor e de um sinal de dois pontos ":". Iniciamos uma lista de membros, com o valor a ser atribuído entre parênteses depois de cada um, separando-os por vírgulas.

Destrutores[editar | editar código-fonte]

Conceito[editar | editar código-fonte]

Além do construtor a linguagem C++, assim como outras linguagens orientadas a objeto, possuem outro tipo de função especialmente criada e gerenciada pela linguagem, os destrutores. Estas são destinadas a desmontar a estrutura do objeto quando o mesmo está sendo encerrado. O destrutor terá o mesmo nome da classe, mas precedido pelo sinal til “~” e também não retorna valor.

O destrutor tem as seguintes características:

  • O destrutor é chamado quando o objeto está sendo finalizado;
  • É usado para liberar qualquer memória que tenha sido alocada;

Declaração[editar | editar código-fonte]

Façamos a classe Dog com o construtor e o destrutor.

 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 construtor tem o mesmo nome que a classe;
  • O destrutor tem o mesmo nome que a classe com o prefixo de tilde” ~”;
  • O construtor foi usado para inicializar as variáveis membro, mas noutros exemplos poderia alocar memória, tomar controle de recursos como dispositivos de sistema e executar inicializações de código;
  • O destrutor no exemplo não faz nenhuma ação real, para além de fazer o eco informando que foi chamado.

"copy constructors"[editar | editar código-fonte]

Um "copy constructor" é um construtor especial que toma como argumento a referência de um objeto da mesma classe e cria um novo objeto que é a copia do objeto em referência. Por padrão, o compilador providencia um "copy constructor" que faz a cópia membro por membro do objeto original, construindo um objeto idêntico. Isto é chamado de "shallow copy" ou "member wise".

Em algumas situações a cópia de um objeto não é satisfatória, para ver isso vamos ver a classe employee, abaixo:

 #include <iostream>
 #include <string.h>
 
 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 destrutor 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 contém um bug sério e morre com uma exceção quando é executado. O problema é que o construtor que está sendo usado para criar um objeto “manager" ,mas ele copia o endereço no ponteiro _name em "manager".

Nos temos 2 pointers ambos contendo o mesmo endereço. Imagine que agora um novo empregado é contratado. quando o nome for atualizado, não apenas iremos alterar o nome do empregado mas também do gerente. Finalmente, quando os objetos deixarem de ser usados e o destrutor da classe fizer a liberação de espaço na memória tentará liberar duas vezes para o mesmo endereço, provocando um erro no sistema de alocação dinâmica de memória, o que forçará o sistema operacional a eliminar o programa da memória.

Para resolver esse problema podemos definir um construtor de cópia ("copy constructor") na classe, substituindo a implementação padrão do compilador. Este recurso é automaticamente identificado e faz com que o compilador não crie a sua versão do construtor. Assim, definir um construtor de cópia próprio é a maneira mais eficiente quando nossos objetos detêm características que os fazem diferentes do padrão.

Para criar o construtor basta definí-lo na classe:

 class Employee 
 { 
   public:
     Employee( const Employee & e);
     Employee(char *name, int id);
     ~Employee();
     char *getName(){return _name;}
   private://Other Accessor methods
     int _id;
     char *_name;
 };
 
Employee::Employee( const Employee & e )
 {
    _id = e._id;
    _name = new char[strlen(e._name) + 1];        //Allocates an character array object
    strcpy(_name, e._name);
 }

Agora temos um construtor que pode fazer a cópia da forma correta. Neste novo construtor alocamos uma cadeia de caracteres para copiar o conteúdo da original no objeto a ser copiado e copiamos o conteúdo para o novo objeto. Assim, teremos objetos distintos, cada um com seu próprio conteúdo.

Ver também[editar | editar código-fonte]