Introdução à Programação Orientada a Objetos em C++
Este artigo explora conceitos fundamentais da programação orientada a objetos através de cinco experimentos práticos envolvendo classes, construtores, destrutores, membros estáticos e funções friend em C++. Cada experimento demonstra aspectos específicos do paradigma orientado a objetos, desde a implementação básica de classes até operações matemáticas e financeiras complexas.
Experimento 1: Implementação de Classe com Membros Estáticos e Funções Friend
Declaração da Classe
O primeiro experimento apresenta a implementação de uma classe chamada Dado que demonstra o uso de construtores, destrutores, membros estáticos e funções friend. A estrutura da classe permite controlar o número de instâncias criadas e fornece métodos para manipulação de dados.
#pragma once
#include <string>
// Classe Dado: Declaração
class Dado {
// Atributos e métodos de instância
public:
Dado(int x = 0, int y = 0); // Construtor padrão
Dado(const Dado &d); // Construtor de cópia
Dado(Dado &&d); // Construtor de movimentação
~Dado(); // Destrutor
void multiplicar(int fator); // Multiplica valores por um fator
void mostrar() const; // Exibe no formato (v1, v2)
private:
int valor1, valor2;
// Atributos e métodos de classe
public:
static int obter_total(); // Retorna quantidade de objetos
public:
static const std::string descricao; // Descrição da classe
static const int limite_objetos; // Limite máximo de objetos
private:
static int contador; // Contador de instâncias
// Declaração de função friend
friend void processar_dados();
};
// Declaração da função externa
void processar_dados();
Implementação dos Métodos
A implementação dos métodos da classe demonstra como os construtores e destrutor interagem com o contador estático de instâncias. Cada vez que um objeto é criado, o contador é incrementado, e quando um objeto é destruído, o contador é decrementado.
// Implementação da classe Dado
#include "dado.h"
#include <iostream>
#include <string>
using std::cout;
using std::endl;
// Inicialização de membros estáticos
const std::string Dado::descricao{"Exemplo simples de classe Orientada a Objetos"};
const int Dado::limite_objetos = 999;
int Dado::contador = 0;
// Construtor padrão
Dado::Dado(int x, int y): valor1{x}, valor2{y} {
++contador;
cout << "Construtor Dado chamado.\n";
}
// Construtor de cópia
Dado::Dado(const Dado &d): valor1{d.valor1}, valor2{d.valor2} {
++contador;
cout << "Construtor de cópia chamado.\n";
}
// Construtor de movimentação
Dado::Dado(Dado &&d): valor1{d.valor1}, valor2{d.valor2} {
++contador;
cout << "Construtor de movimentação chamado.\n";
}
// Destrutor
Dado::~Dado() {
--contador;
cout << "Destrutor Dado chamado.\n";
}
// Método de instância para multiplicação
void Dado::multiplicar(int fator) {
valor1 *= fator;
valor2 *= fator;
}
// Método de instância para exibição
void Dado::mostrar() const {
cout << "(" << valor1 << ", " << valor2 << ")" ;
}
// Método estático
int Dado::obter_total() {
return contador;
}
// Função friend
void processar_dados() {
Dado obj(42);
obj.valor2 = 2049;
cout << "obj = "; obj.mostrar(); cout << endl;
}
Programa de Teste
O programa principal cria várias instâncias da classe utilizando diferentes construtores e demonstra o comportamento do contador de objetos e da função friend.
#include "dado.h"
#include <iostream>
using std::cout;
using std::endl;
void executar_testes();
int main() {
executar_testes();
cout << "\nmain: \n";
cout << "Quantidade atual de objetos Dado: " << Dado::obter_total() << endl;
}
void executar_testes() {
cout << "Testando a classe Dado: \n";
cout << "Informação da classe: " << Dado::descricao << endl;
cout << "Limite de objetos: " << Dado::limite_objetos << endl;
cout << "Quantidade atual: " << Dado::obter_total() << endl << endl;
Dado obj1;
cout << "obj1 = "; obj1.mostrar(); cout << endl;
Dado obj2(3, 4);
cout << "obj2 = "; obj2.mostrar(); cout << endl;
Dado obj3(obj2);
obj3.multiplicar(2);
cout << "obj3 = "; obj3.mostrar(); cout << endl;
Dado obj4(std::move(obj2));
cout << "obj4 = "; obj4.mostrar(); cout << endl;
cout << "Quantidade atual de objetos: " << Dado::obter_total() << endl;
processar_dados();
}
Análise de Resultados
Questão 1: Necessidade da Declaração externa de Função Friend
Ao remover a declaração externa da função processar_dados() no arquivo de cabeçalho e recompilar, o programa não compila corretamente. O erro ocorre porque a declaração friend dentro da classe permite que a função accesse membros privados, mas não fornece informações suficientes para que outras partes do código chamem essa função. A declaração externa é necessária para que o compilador reconheça a função como um símbolo válido fora do contexto da classe.
Questão 2: Tipos de Construtores e Destrutor
Construtor padrão: Inicializa um objeto com valores fornecidos ou valores padrão. É chamado quando um novo objeto é criado sem especificar um objeto existente como fonte de dados.
Construtor de cópia: Cria um novo objeto como cópia de outro objeto existente. É invoked quando um objeto é passado por valor, retornado por valor, ou explicitamente construído a partir de outro objeto do mesmo tipo.
Construtor de movimentação: Transfere recursos de um objeto temporário para um novo objeto, evitando cópias desnecessárias. Melhora significativamente a performance ao trabalhar com objetos temporários ou objetos que gerenciam recursos externos.
Destrutor: Libera recursos adquiridos pelo objeto durante seu tempo de vida. É chamado automaticamente quando o objeto sai de escopo ou é explicitamente deletado.
Questão 3: Impacto da Realocação de Código entre Arquivos
Mover a implementação do construtor de cópia para o arquivo de cabeçalho resulta em erro de linker. O compilador pode processar a instrução, mas quando outras unidades de tradução incluem o cabeçalho, múltiplas definições idênticas são criadas, violando a regra de definição única (One Definition Rule). A implementação deve permanecer em um arquivo .cpp para garantir que exista exatamente uma definição linkável.
Epxerimento 2: Implementação de Classe para Números Complexos
Definição da Classe NumeroComplexo
Este experimento implementa uma classe para representar e manipular números complexos, demonstrando o uso de funções friend para sobrecarga de operadores e métodos utilitários.
#pragma once
#include<iostream>
#include<cmath>
using std::cout;
using std::endl;
using std::boolalpha;
void executar_teste();
class NumeroComplexo {
private:
double real, imaginario;
public:
static std::string descricao;
// Construtores
NumeroComplexo(double r = 0, double i = 0) :real{ r }, imaginario{ i } {}
NumeroComplexo(const NumeroComplexo &c) :real{ c.obter_real() }, imaginario{ c.obter_imag() } {}
// Métodos de acesso
double obter_real() const {
return real;
}
double obter_imag() const {
return imaginario;
}
// Método de instância para adição
void adicionar(const NumeroComplexo c) {
real += c.obter_real();
imaginario += c.obter_imag();
}
// Funções friend
friend NumeroComplexo somar(const NumeroComplexo c1, const NumeroComplexo c2);
friend bool sao_iguais(const NumeroComplexo &c1, const NumeroComplexo &c2);
friend bool sao_diferentes(const NumeroComplexo &c1, const NumeroComplexo &c2);
friend void exibir(const NumeroComplexo &c);
friend double modulo(const NumeroComplexo &c);
};
// Declarações das funções externas
NumeroComplexo somar(const NumeroComplexo c1, const NumeroComplexo c2);
bool sao_iguais(const NumeroComplexo &c1, const NumeroComplexo &c2);
bool sao_diferentes(const NumeroComplexo &c1, const NumeroComplexo &c2);
void exibir(const NumeroComplexo &c);
double modulo(const NumeroComplexo &c);
Implementação das Operações
#include"NumeroComplexo.h"
std::string NumeroComplexo::descricao = "Classe simples para números complexos";
void executar_teste() {
cout << "Teste de membros da classe: " << endl;
cout << NumeroComplexo::descricao << endl;
cout << endl;
cout << "Teste de objetos NumeroComplexo: " << endl;
NumeroComplexo c1;
NumeroComplexo c2(3, -4);
const NumeroComplexo c3(3.5);
NumeroComplexo c4(c3);
cout << "c1 = "; exibir(c1); cout << endl;
cout << "c2 = "; exibir(c2); cout << endl;
cout << "c3 = "; exibir(c3); cout << endl;
cout << "c4 = "; exibir(c4); cout << endl;
cout << "c4.real = " << c4.obter_real() << ", c4.imag = " << c4.obter_imag() << endl;
cout << endl;
cout << "Teste de operações com números complexos: " << endl;
cout << "abs(c2) = " << modulo(c2) << endl;
c1.adicionar(c2);
cout << "c1 += c2, c1 = "; exibir(c1); cout << endl;
cout << boolalpha;
cout << "c1 == c2 : " << sao_iguais(c1, c2) << endl;
cout << "c1 != c3 : " << sao_diferentes(c1, c3) << endl;
c4 = somar(c2, c3);
cout << "c4 = c2 + c3, c4 = "; exibir(c4); cout << endl;
}
// Implementação da função de soma
NumeroComplexo somar(const NumeroComplexo c1, const NumeroComplexo c2) {
NumeroComplexo resultado;
resultado.adicionar(c2);
resultado.adicionar(c1);
return resultado;
}
// Implementação da função de comparação de igualdade
bool sao_iguais(const NumeroComplexo &c1, const NumeroComplexo &c2) {
if (c1.obter_imag() == c2.obter_imag() && c1.obter_real() == c2.obter_real()) {
return true;
}
return false;
}
// Implementação da função de comparação de desigualdade
bool sao_diferentes(const NumeroComplexo &c1, const NumeroComplexo &c2) {
return !sao_iguais(c1, c2);
}
// Implementação da função de exibição
void exibir(const NumeroComplexo &c) {
if (c.obter_imag() >= 0) {
cout << c.obter_real() << '+' << c.obter_imag() << 'i' << endl;
}
else {
cout << c.obter_real() << '-' << modulo(c) << 'i' << endl;
}
}
// Implementação da função de módulo
double modulo(const NumeroComplexo &c) {
return sqrt(c.obter_imag() * c.obter_imag() + c.obter_real() * c.obter_real());
}
Função Principal
#include"NumeroComplexo.h"
int main() {
executar_teste();
}
Experimento 3: Utilização da Biblioteca Padrão de Números Complexos
Este experimento demonstra o uso da classe de template std::complex fornecida pela biblioteca padrão do C++, comparando sua utilização com a implementação personalizada desenvolvida no experimento anterior.
#include <iostream>
#include <complex>
using std::cout;
using std::endl;
using std::boolalpha;
using std::complex;
void teste_completo() {
cout << "Teste do template std::complex da biblioteca padrão: " << endl;
complex<double> c1;
complex<double> c2(3, -4);
const complex<double> c3(3.5);
complex<double> c4(c3);
cout << "c1 = " << c1 << endl;
cout << "c2 = " << c2 << endl;
cout << "c3 = " << c3 << endl;
cout << "c4 = " << c4 << endl;
cout << "c4.real = " << c4.real() << ", c4.imag = " << c4.imag() << endl;
cout << endl;
cout << "Teste de operações com números complexos: " << endl;
cout << "abs(c2) = " << abs(c2) << endl;
c1 += c2;
cout << "c1 += c2, c1 = " << c1 << endl;
cout << boolalpha;
cout << "c1 == c2 : " << (c1 == c2) << endl;
cout << "c1 != c3 : " << (c1 != c3) << endl;
c4 = c2 + c3;
cout << "c4 = c2 + c3, c4 = " << c4 << endl;
}
int main() {
teste_completo();
}
A classe de template std::complex oferece funcionalidades prontas para operações matemáticas, sobrecarga de operadores e integração direta com streams de saída, simplificando significativamente o desenvolvimento de aplicações que trabalham com números complexos.
Experimento 4: Implementação de Classe para Frações
Definição da Classe Fracao
Este experimento implementa uma classe para representar e operar frações, incluindo simplificação automática, verificação de denominador zero e sobrecarga de operadores aritméticos através de funções friend.
#pragma once
#include <iostream>
using std::cout;
using std::endl;
int calcular_mdc(int x, int y);
class Fracao {
private:
int numerador;
int denominador;
public:
static std::string informacao;
// Construtor com simplificação automática
Fracao(int n, int d = 1) {
int divisor = calcular_mdc(n, d);
numerador = n / divisor;
denominador = d / divisor;
if (denominador < 0) {
denominador = -denominador;
numerador = -numerador;
}
}
// Métodos de acesso
int obter_numerador() const {
return numerador;
}
int obter_denominador() const {
return denominador;
}
// Método para obter fração negativa
Fracao obter_negativa() {
Fracao f(-numerador, denominador);
return f;
}
// Funções friend para operações aritméticas
friend void mostrar(const Fracao &f);
friend Fracao operator+(const Fracao f1, const Fracao f2);
friend Fracao operator-(const Fracao f1, const Fracao f2);
friend Fracao operator*(const Fracao f1, const Fracao f2);
friend Fracao operator/(const Fracao f1, const Fracao f2);
};
// Sobrecarga de operadores como funções externas
Fracao operator+(const Fracao f1, const Fracao f2);
Fracao operator-(const Fracao f1, const Fracao f2);
Fracao operator*(const Fracao f1, const Fracao f2);
Fracao operator/(const Fracao f1, const Fracao f2);
void mostrar(const Fracao &f);
Implementação das Operações Aritméticas
#include "Fracao.h"
std::string Fracao::informacao = "Classe Fracao v1.0\nSuporta construção, exibição e operações aritméticas\n";
// Implementação do operador de soma
Fracao operator+(const Fracao f1, const Fracao f2) {
int n1, n2, d1, d2, n, d;
n1 = f1.numerador;
n2 = f2.numerador;
d1 = f1.denominador;
d2 = f2.denominador;
n = n1 * d2 + n2 * d1;
d = d1 * d2;
int divisor = calcular_mdc(n, d);
return Fracao(n / divisor, d / divisor);
}
// Implementação do operador de subtração
Fracao operator-(const Fracao f1, const Fracao f2) {
int n1, n2, d1, d2, n, d;
n1 = f1.numerador;
n2 = f2.numerador;
d1 = f1.denominador;
d2 = f2.denominador;
n = n1 * d2 - n2 * d1;
d = d1 * d2;
int divisor = calcular_mdc(n, d);
return Fracao(n / divisor, d / divisor);
}
// Implementação do operador de multiplicação
Fracao operator*(const Fracao f1, const Fracao f2) {
int n1, n2, d1, d2, n, d;
n1 = f1.numerador;
n2 = f2.numerador;
d1 = f1.denominador;
d2 = f2.denominador;
n = n1 * n2;
d = d1 * d2;
int divisor = calcular_mdc(n, d);
return Fracao(n / divisor, d / divisor);
}
// Implementação do operador de divisão
Fracao operator/(const Fracao f1, const Fracao f2) {
if (f2.numerador == 0) {
return Fracao(0, 0);
}
int n1, n2, d1, d2, n, d;
n1 = f1.numerador;
n2 = f2.numerador;
d1 = f1.denominador;
d2 = f2.denominador;
n = n1 * d2;
d = n2 * d1;
int divisor = calcular_mdc(n, d);
return Fracao(n / divisor, d / divisor);
}
// Implementação da função de exibição
void mostrar(const Fracao &f) {
if (f.denominador != 0) {
if (f.numerador == 0 || f.denominador == 1) {
cout << f.numerador;
}
else {
cout << f.numerador << "/" << f.denominador;
}
}
else {
cout << "Denominador nao pode ser zero";
}
}
// Implementação do algoritmo de MDC
int calcular_mdc(int x, int y) {
if (y == 0) {
return 1;
}
int resto = y;
while (x % y != 0) {
resto = x % y;
x = y;
y = resto;
}
return resto;
}
Programa de Teste
#include"Fracao.h"
void teste_operacoes_basicas() {
cout << "Teste da classe Fracao: " << endl;
cout << Fracao::informacao << endl << endl;
Fracao f1(5);
Fracao f2(3, -4), f3(-18, 12);
Fracao f4(f3);
cout << "f1 = "; mostrar(f1); cout << endl;
cout << "f2 = "; mostrar(f2); cout << endl;
cout << "f3 = "; mostrar(f3); cout << endl;
cout << "f4 = "; mostrar(f4); cout << endl;
Fracao f5(f4.obter_negativa());
cout << "f5 = "; mostrar(f5); cout << endl;
cout << "f5.obter_numerador() = " << f5.obter_numerador() << ", f5.obter_denominador() = " << f5.obter_denominador() << endl;
cout << "f1 + f2 = "; mostrar(f1 + f2); cout << endl;
cout << "f1 - f2 = "; mostrar(f1 - f2); cout << endl;
cout << "f1 * f2 = "; mostrar(f1 * f2); cout << endl;
cout << "f1 / f2 = "; mostrar(f1 / f2); cout << endl;
cout << "f4 + f5 = "; mostrar(f4 + f5); cout << endl;
}
void teste_denominador_zero() {
Fracao f6(42, 55), f7(0, 3);
cout << "f6 = "; mostrar(f6); cout << endl;
cout << "f7 = "; mostrar(f7); cout << endl;
cout << "f6 / f7 = "; mostrar(f6 / f7); cout << endl;
}
int main() {
cout << "Teste 1: Funcionalidades basicas da classe Fracao\n";
teste_operacoes_basicas();
cout << "\nTeste 2: Verificacao de denominador zero: \n";
teste_denominador_zero();
}
A implementação demonstra como classes podem encapsular operações matemáticas complexas, fornecendo uma interface limpa e intuitiva para manipulação de frações, incluindo simplificação automática e tratamento de casos especiais como divisão por zero.
Experimento 5: Sistema de Conta Poupança com Acúmulo de Juros
Definição da Classe ContaPoupanca
Este experimento apresenta uma implementação completa de um sistema de conta poupança que gerencia saldo, taxa de juros e registra todas as transações, demonstrando conceitos avançados de encapsulamento e gerenciamento de estado.
#pragma once
class ContaPoupanca {
private:
int identificador;
double saldo;
double taxa_juros;
int ultima_data;
double acumulador_juros;
static double total_sistema;
void registrar(int data, double valor);
double calcular_acumulado(int data) const {
return acumulador_juros + saldo * (data - ultima_data);
}
public:
// Construtor
ContaPoupanca(int data, int id, double taxa);
// Métodos de acesso
int get_id() const { return identificador; }
double get_saldo() const { return saldo; }
double get_taxa() const { return taxa_juros; }
static double get_total() { return total_sistema; }
// Operações bancárias
void deposito(int data, double valor);
void saque(int data, double valor);
void creditar_juros(int data);
void exibir() const;
};
Implementação dos Métodos
#include"ContaPoupanca.h"
#include<cmath>
#include<iostream>
using namespace std;
double ContaPoupanca::total_sistema = 0;
ContaPoupanca::ContaPoupanca(int data, int id, double taxa)
:identificador(id), saldo(0), taxa_juros(taxa), ultima_data(data), acumulador_juros(0) {
cout << data << "\t#" << id << " criado" << endl;
}
void ContaPoupanca::registrar(int data, double valor) {
acumulador_juros = calcular_acumulado(data);
ultima_data = data;
// Arredondamento para duas casas decimais
valor = floor(valor * 100 + 0.5) / 100;
saldo += valor;
total_sistema += valor;
cout << data << "\t#" << identificador << "\t" << valor << "\t" << saldo << endl;
}
void ContaPoupanca::deposito(int data, double valor) {
registrar(data, valor);
}
void ContaPoupanca::saque(int data, double valor) {
if (valor > get_saldo())
cout << "Erro: saldo insuficiente" << endl;
else
registrar(data, -valor);
}
void ContaPoupanca::creditar_juros(int data) {
double juros = calcular_acumulado(data) * taxa_juros / 365;
if (juros != 0)
registrar(data, juros);
acumulador_juros = 0;
}
void ContaPoupanca::exibir() const {
cout << "#" << identificador << "\tSaldo:" << saldo;
}
Simulação de Operações Bancárias
#include"ContaPoupanca.h"
#include<iostream>
using namespace std;
int main() {
ContaPoupanca conta0(1, 2132502, 0.015);
ContaPoupanca conta1(1, 58320212, 0.015);
conta0.deposito(5, 5000);
conta1.deposito(25, 10000);
conta0.deposito(45, 5500);
conta1.saque(60, 4000);
conta0.creditar_juros(90);
conta1.creditar_juros(90);
conta0.exibir(); cout << endl;
conta1.exibir(); cout << endl;
cout << "Total do sistema:" << ContaPoupanca::get_total() << endl;
return 0;
}
A implementação demonstra um sistema completo de gerenciamento de contas, com tratamento de transações, cálculo automático de juros baseados em tempo, controle de saldo e manutenção de um total global do sistema através de membros estáticos.
Conclusão
Os cinco experimentos apresentados cobrem aspectos essenciais da progarmação orientada a objetos em C++, desde conceitos básicos como construtores e destrutores até implementações complexas de classes para aplicações matemáticas e financeiras. O domínio desses conceitos é fundamental para o desenvolvimento de software robusto e eficiente utilizando o paradigma orientado a objetos.