Classes e Objetos
A base da programação orientada a objetos são os objetos. No contexto de C++ objetos são instâncias de classes, classes são abstrações que contém a descrição de quais atributos e métodos um objeto possui e atributos são variáveis internas de uma classe. Métodos são funções membro (member functions) de uma classe. Exemplo:
#include <iostream>
class Cachorro {
public:
void latir()
{
std::cout << "Au! Au!!\n";
}
};
int main()
{
Cachorro bidu;
bidu.latir();
return 0;
}
No código acima, Cachorro
é uma classe. O objeto bidu
é uma instância da classe Cachorro
. Como Cachorro
possui o método latir
, é possível invoca-lo por meio de bidu.latir()
.
Métodos podem ser públicos (public
), protegidos (protected
) ou privados (private
). Métodos e atributos privados não podem ser invocados fora do escopo da classe. Métodos e atributos protegidos podem ser invocados apenas no escopo da classe ou de classes filhas (Hierarquia de classes é tema do próximo capítulo). Métodos e atributos públicos podem ser acessados de dentro ou de fora do escopo da classe.
Sugere-se manter os atributos de uma classe como membros privados ou protegidos, e acessa-los apenas através de métodos, a fim de esconder os detalhes do objeto, e expondo apenas alguns métodos públicos. À isso da-se o nome de encapsulamento.
O construtor de uma classe é um método especial que possui o nome igual ao nome da classe e não possui retorno. O construtor da classe serve para inicializar os atributos internos e deixa-la em um estado utilizavel. Exemplo:
#include <iostream>
#include <string>
class Cachorro {
public:
Cachorro(std::string const& nome)
: nome(nome)
{}
void latir()
{
std::cout << "Au! Au!! Eu sou o " << this->nome << '\n';
}
private:
std::string nome;
};
int main()
{
Cachorro bidu{"Bidu"};
bidu.latir();
return 0;
}
No exemplo acima Cachorro(std::string const& nome)
é o construtor da classe Cachorro
. Note que o construtor possui um parâmetro do tipo std::string
. Dessa forma, para construir um Cachorro
agora é necessário passar alguma string como parâmetro. Isso está sendo feito na linha Cachorro bidu{"Bidu"};
. Essa linha está construindo uma instância da classe Cachorro
de nome bidu
e cuja variável interna nome
terá o valor Bidu
.
Note a sintaxe de construção do Cachorro
:
Cachorro(std::string const& nome)
: nome(nome)
{}
Note que o : nome(nome)
está ANTES da abertura do escopo do corpo do construtor, ou seja, antes das chaves {}
. Essa sintaxe possibilita que a construção do objeto seja feita antes de entrar no corpo do construtor. Isso se chama lista de inicialização (initializer list). Alternativamente poderia-se escrever
Cachorro(std::string const& nome)
{
this->nome = nome;
}
Onde this
é uma variável especial que é um ponteiro para a própria instância da classe em questão. O uso de this
é
opcional dentro do escopo da classe, de forma que é possível se referir a atributos da mesma diretamente.
O código acima altera o valor do membro nome
para o valor contido na variável local nome
. Diferente do código
anterior, this->nome
primeiro é inicializado com valor vazio, para somente ser alterado dentro do corpo do construtor
de Cachorro
.
Assim como qualquer outra variável em C++, instâncias de classes podem ser alocadas na pilha ou na heap. Para alocar uma instância na pilha, basta seguir o procedimento dos exemplos anteriores, copiado abaixo apenas para facilitar a leitura:
int main()
{
Cachorro bidu{"Bidu"}; // <--- Alocado na pilha
bidu.latir();
return 0;
}
Por outro lado, para alocar na heap deve-se fazer uso da palavra reservada new
, conforme exemplo abaixo. Tal qual explicado em capítulos anteriores toda memória alocada na heap deve ser desalocada pelo programador. No caso de variáveis inicializadas com new
é necessário utilizar o delete
.
int main()
{
auto* bidu = new Cachorro{"Bidu"}; // <--- Alocado na heap
bidu->latir();
delete bidu;
return 0;
}
Note que a sintaxe de acesso ao método latir
mudou. Isso ocorre por que no exemplo acima bidu
é um ponteiro para uma instância de Cachorro
. Dessa forma o acesso ao método latir
é feito com ->
ao invés de .
. Alternativamente poderia-se de-referenciar o ponteiro. Porém, a sintaxe ficaria bastante esquisita e não é sugerida:
int main()
{
auto* bidu = new Cachorro{"Bidu"}; // <--- Alocado na heap
(*bidu).latir();
delete bidu;
return 0;
}
O objeto bidu
será destruído no momento que delete
é invocado. No caso da variável em pilha o objeto é destruido no momento que sair do escopo. Em ambos os casos é possível invocar um código especial de destrução do objeto. Esse código fica no método destrutor da classe que possui nome equivalente ao do construtor, porém, com o simbolo ~
como prefixo. O destrutor é útil quando é necessário liberar algum recurso obtido em algum momento da vida do objeto. Exemplo:
#include <iostream>
#include <string>
class Cachorro {
public:
Cachorro(std::string const& nome)
: nome(nome)
{}
~Cachorro()
{
// Código executado no momento que o cachorro está sendo destruido
std::cout << "Estou sendo destruido!!\n";
}
void latir()
{
std::cout << "Au! Au!! Eu sou o " << this->nome << '\n';
}
private:
std::string nome;
};
int main()
{
Cachorro bidu{"Bidu"};
bidu.latir();
return 0;
}
Esses são os conceitos básicos de orientação a objetos necessários para iniciar um contato com o tema em linguagem C++. Os capítulos posteriores vão entrar em outros conceitos fundamentais e de extrema importância para a programação orientada a objetos. Por fim, é importante mencionar que programação orientada a objetos é um paradigma muito interessante e pode ajudar na solução de vários problemas. Porém, tenha sempre em mente que não é o único paradigma de programação e que nem sempre é o melhor para qualquer situação.