Esta apresentação ensinará os conceitos de Orientação a Objetos com C++, do mais básico para o mais avançado. É suposto que o aluno já tenha conhecimento avançado de C.
Exemplo em C: Faça um programa que jogue dois dados e some os números que sairam. Ficaria assim: #include <stdio.h> #include <stdlib.h> #include <time.h> int main(int argc, char **argv) { short int dado1, dado2, soma; srand (time(null)); dado1 = (short int) (rand() % 6) + 1; dado2 = (short int) (rand() % 6) + 1; soma = dado1 + dado2; printf("%d\n", dado1); printf("%d\n", dado2); printf("%d\n", soma); return 0; Este programa funciona e faz o que foi pedido, mas... ele é organizado? Em um programa pequeno é aceitável fazer o código assim, mas e num projeto grande como um jogo, feito por mais de 15 pessoas? Os dados estão sendo representados por ints, e para sortear um número, repete-se a linha do rand() várias vezes. Usando OO, este problema da desorganização é resolvido. A partir do próximo slide, este código será transformado em C++ orientado a objetos.
Em C existe uma ferramenta chamada struct. Uma struct para representar um dado seria assim: typedef struct { int ultimo_valor_sorteado; Dado; Em C++, embora também seja possível fazer structs, há um irmão evoluido desta ferramenta: classes. Uma classe é como uma struct, mas que permite adição de funções além das váriaveis. Na verdade classes são muito mais complexas do que isso, como veremos mais a frente, mas esta comparação com struct não deixa de estar certa... Veja uma classe dado: class Dado { public: int valor; int sorteia(void) { return valor = (rand() % 6) + 1; Dado() { srand (time(null)); valor = 0; ;
#include <iostream> #include <time.h> using namespace std; class Dado { public: int valor; int sorteia(void) { return valor = (rand() % 6) + 1; Dado() { srand (time(null)); valor = 0; ; int main(int argc, char **argv) { Dado dado1,dado2; cout << dado1.sorteia() << endl; cout << dado2.sorteia() << endl; cout << dado1.valor + dado2.valor << endl; return 0; Assim, o código fica muito mais legível do que o outro em C. Os dados não são mais representados por ints: agora um dado é chamado exatamente de Dado. As váriaveis e contas, para não serem repetidas o tempo todo no código, estão dentro da classe. Querendo usar o valor sorteado, escreve-se x.valor, e para sortear, x.sorteia(). Cada dado terá armazenado o valor sorteado, e poderá ser jogado de novo. Esta prática organiza o código, diminui o número de linhas em projetos grandes e facilita o entendimento, pois os nomes de classes, variáveis e métodos são diretos. Detalhes ainda desconhecidos como o namespace e a função Dado() serão explicadas no futuro.
Construtores Antes de continuar, alguns detalhes de nomenclatura devem ser ditos. Variáveis dentro de classes são chamadas de atributos, e funções dentro de classes são chamadas de métodos. No exemplo anterior, um Dado possui o atributo valor e o método sortear. Atributos são os valores característicos de uma classe, e métodos são ações praticadas por ela. Não é mais lógico chamar assim? Havia no exemplo anterior um método Dado() dentro da classe Dado. Métodos com o mesmo nome da classe são chamados de métodos construtores. Eles são chamados quando a variável é criada. Ou seja, ao escrever a linha Dado dado1;, o método Dado() é chamado automaticamente. Significa que assim que d1 é criado, o seu atributo valor já é setado para zero. É possível criar também construtores que recebem parâmetros.
#include <iostream> using namespace std; class Pessoa { public: string nome; int idade; Pessoa(string nome, int idade):nome(nome), idade(idade) { ; int main (int argc, char **argv) { Pessoa p("joao",15); cout << p.nome << endl; cout << p.idade << endl; return 0; Exemplo de construtor recebendo parâmetros. Veja que a variável p, quando é criada, recebe valores por parênteses, uma string e um int. Estes valores são passados para o construtor e os atributos são inicializadas. Note que ao lado do construtor há dois pontos seguidos por: nome(nome), idade(idade). Este é um modo de inicializar as variáveis. idade(idade) significa: O atributo 'idade' receberá o valor do parâmetro 'int idade'. Escrito deste modo, ambos podem ter o mesmo nome que o compilador não se confundirá. Deste modo, o código não fica poluído com várias variáveis de nomes diferentes desnecessariamente.
Encapsulamento Em ambos os exemplos até agora foi cometido um erro proposital. Embora não pareça, os códigos de Dado e Pessoa estão com uma séria falha de segurança. Dado: Um dado foi arremessado com o método sortear(), e este valor sorteado foi armazenado no atributo 'valor'. Mas note que quem usar a classe Dado ainda será capaz de fazer, na main, x.valor = 7. Isto estragaria a lógica do programa, pois um dado (comum) não possui o valor 7. Pessoa: É possível entrar com o valor da idade da pessoa. O que acontece se for entrado o valor -1? Nada. Ou seja, outra falha. Para contornar estas falhas, existe o encapsulamento. Basta proibir o acesso a todos os atributos e tratar suas modificações através de métodos, chamados getters e setters.
#include <iostream> #include <time.h> using namespace std; class Dado { private: // Nada abaixo de private pode ser acessado fora da classe. int valor; // Como o que define o 'valor' é o sorteio, ela não pode nunca ser modificada. public: // Os métodos marcados como public são acessíveis fora da classe int sorteia(void) { return valor = (rand() % 6) + 1; int getvalor(void) { return valor; ; Dado() { srand (time(null)); valor = 0; int main(int argc, char **argv) { Dado dado1,dado2; // Tente modificar o valor da variável valor. Agora é impossível! cout << dado1.sorteia() << endl; cout << dado2.sorteia() << endl; cout << dado1.getvalor() + dado2.getvalor() << endl; return 0;
#include <iostream> using namespace std; class Pessoa { private: string nome; int idade; public: Pessoa(string nome, int idade):nome(nome) { setidade(idade); ; string getnome(void) { return nome; void setnome(string nome) { this->nome = nome; int getidade(void) { return idade; void setidade(int idade) { this->idade = idade < 0? -idade : idade; int main (int argc, char **argv) { Pessoa p("maria",-20); cout << p.getnome() << endl; cout << p.getidade() << endl; return 0; Deve-se sempre proibir o acesso a todos os atributos usando a palavra private, e liberar todo o resto com public. Os que não estiverem em um destes modificadores de acesso serão considerados private. Mesmo que a proibição em alguns casos pareça inútil, pode ser que venha a ser útil no futuro. Os métodos getx() e setx() servem para acessar e tratar os dados privados. Não esqueça de colocar estes métodos sempre como públicos. this->nome = nome; significa: O atributo nome desta classe recebe o valor da variável nome passada por parâmetro. É um outro artifício usado para economizar palavras. O outro é o nome(nome), usado no construtor. Deixe os atributos sempre private, mas faça getters e setters apenas se necessário. Note que não foi feito o método setvalor() no slide anterior.
Sobrecarga Em C++ (e em qualquer outra linguagem orientada a objetos, como Java), é possível criar mais de um método com o mesmo nome. Os métodos de mesmo nome são diferenciados pelos parâmetros: tipos, ordem e quantidade deles. É possível modificar a classe Dado adicionando os dois métodos: // Método que já existe na classe int sorteia(void) { return valor = (rand() % 6) + 1; // Supondo que o dado seja de vários lados int sorteia(int ordem) { return valor = (rand() % ordem) + 1; Os métodos possuem o mesmo nome, mas os parâmetros são diferentes. Tente você desta vez modificar a classe Dado :)
É possível também fazer sobrecarga de construtores. Ou seja, uma classe pode ter vários construtores, desde que seus atributos sejam capazes de diferenciá-los uns dos outros. A classe Pessoa possui um construtor que seta os valores do nome e da idade. É possível fazer, ao mesmo tempo, construtores assim: Pessoa(string nome, int idade) {... // Já existe na classe! Pessoa(string nome) {... // Inicia o valor do nome apenas. Pessoa(int idade) {... // Inicia o valor da idade apenas. Pessoa() {... // Não inicia nada. Apenas cria a pessoa. Tente fazer esta modificação na classe pessoa e veja que de fato é possível criar vários construtores por classe. Esta possibilidade de escrever um mesmo método de várias formas é chamado de sobrecarga.
Uso de Ponteiros Até agora não foi dada muita atenção, mas é preciso (e muito importante) diferenciar uma variável de um objeto. Fazendo Dado d1, é criado um objeto da classe Dado. Foi criada uma instância de Dado. Entenda classe como algo abstrato e generalizado, e objeto (ou instância) como algo concreto e único. Outro exemplo: Imagine que existe, no além, uma classe Pessoa, e que VOCÊ E EU somos instâncias desta classe :) É possível criar em C++ ponteiros para objetos. A linha Dado *d1 cria um objeto? A resposta é NÃO! Neste caso é criado apenas um ponteiro, que inicialmente aponta para NULL. Veja nos slides seguintes como usar ponteiros para objetos. Será apresentada a palavra new.
#include <iostream> using namespace std; // Esta classe representa uma coordenada. (x,y) class Ponto { private: int x, y; public: int getx(void) { return this->x; int gety(void) { return this->y; void setx(int x) { this->x = x; void sety(int y) { this->y = y; Ponto(int x, int y) : x(x), y(y) { Ponto(void) { this->x = this->y = 0; ; Continua no slide seguinte...
int main(int argc, char **argv) { // Ponteiro para Ponto. Ele está apontando, inicialmente, para NULL. Ponto *p1; // Está sendo criado um ponteiro chamado p2. p2 APONTA para um objeto, // criado com a palavra 'new'. // O objeto não possui um nome. Sabe-se apenas que p2 aponta para ele. Ponto *p2 = new Ponto; // O mesmo caso acima, mas desta vez, foi usado outro construtor. Ponto *p3 = new Ponto(9,15); // Aloca espaço para um ponto, mas nenhum construtor é chamado. Ponto *p4 = (Ponto*) (malloc(sizeof(ponto))); // Objetos criados diretamente. Ponto p5; Ponto p6(2,5); // Para buscar um atributo ou método através de um ponteiro, usa-se uma seta. cout << p3->getx() << endl; // E para buscar de um objeto criado diretamente, usa-se um ponto. cout << p6.gety() << endl; return 0;
Destrutores Objetos podem ser construídos e também destruídos. Um objeto deve ser destruído quando não for mais utilizado. Esta destruição é feita através do método destrutor. Objetos criados diretamente, sem ponteiros, são destruídos automaticamente ao fim do código. Os objetos instanciados a partir de ponteiros (com a palavra new) não são destruidos automaticamente, apenas o programador pode fazê-lo. Métodos construtores são similares ao free(), de C. Os dois desalocam espaço de um ponteiro. É uma boa prática (essencial em projetos grandes) destruir todos os objetos que não forem mais utilizados. Em algumas linguagens isso é feito automaticamente. Toda classe já possui um destrutor padrão, que desaloca um ponteiro. Mas é possível escrever um método destrutor, para executar alguma ação específica. Ao escrever um novo destrutor, estamos substituindo o padrão por este novo método. Métodos destrutores são escritos com o mesmo nome da classe, e com um ~(til) antes do nome. Um ponteiro é desalocado através da palavra delete.
#include <iostream> using namespace std; class Ponto { private: int x, y; public: int getx(void) { return this->x; int gety(void) { return this->y; void setx(int x) { this->x = x; void sety(int y) { this->y = y; Ponto(int x, int y) : x(x), y(y) { Ponto(void) { this->x = this->y = 0; ~Ponto(void) { // "cout" apenas para que seja visível // a destruição na compilação :) cout << "BUM!" << endl; ; int main(int argc, char **argv) { Ponto *p1; Ponto *p2 = new Ponto; Ponto *p3 = new Ponto(9,15); Ponto *p4 = (Ponto*) (malloc(sizeof(ponto))); Ponto p5; Ponto p6(2,5); delete p4; // Compile e veja que o destrutor será chamado 3 vezes. // Foram destruidos p4, p5 e p6. return 0;
O operador :: As classes feitas até agora (Dado, Pessoa e Ponto) foram feitos de modo correto, mas não do modo padrão de C++. Em C, as funções presentes no programa têm seus cabeçalhos declarados no início do código. Estes cabeçalhos são chamados protótipos das funções. As classes devem ser apenas o conjunto de protótipos dos métodos. Estes devem ser declarados do lado de fora da classe. A relação entre classe e método é feita com o operador :: Este operador também pode acessar os elementos de um namespace. Namespace é como um pacote que carrega variáveis, funções, classes... qualquer coisa. O namespace std é um pacote especial que carrega em seu interior várias ferramentas padrões de C++. É possível também criar seus próprios namespaces e adicioná-los usando a palavra using.
#include <iostream> // Não será adicionado o namespace std. // Veja como são chamados cout e endl. class Ponto { private: int x, y; public: int getx(void); int gety(void); void setx(int x); void sety(int y); ; Ponto(int, int); Ponto(void); ~Ponto(void); int Ponto :: getx(void) { return this->x; int Ponto :: gety(void) { return this->y; void Ponto :: setx(int x) { this->x = x; Ponto :: Ponto(int x, int y) : x(x), y(y) { Ponto :: Ponto(void) { this->x = this->y = 0; Ponto::~Ponto(void) { std::cout << "BUM!" << std::endl; int main(int argc, char **argv) { Ponto *p = new Ponto(7,2); std::cout << p->getx() << std::endl; return 0; Esta sintaxe é feia numa primeira impressão, mas não é difícil e ajuda a visualizar o que uma classe possui. Faça suas classes sempre assim. void Ponto :: sety(int y) { this->y = y;
Métodos Inline C++ possui um modificador que facilita e otimiza o acesso a métodos pequenos: inline. Sempre que um método tiver apenas uma linha (getters e setters, geralmente) ele deve ter a palavra inline antes do tipo de retorno, e este método deve ser criado diretamente na classe, sem o operador ::. Métodos inline são acessados muito mais rapidamente do que os comuns, por isso, sempre que possível, faça-os assim. class Inteiro { private: int valor; public: inline int getvalor(void) { return valor; inline void setvalor(int valor) { this->valor = valor; bool iszero(void); Inteiro(int); ; bool Inteiro :: iszero(void) { if(valor == 0) { return true; else { return false; Inteiro :: Inteiro(int valor) : valor(valor) {