Laboratório de Programação Orientada a Objetos: Classes, Construtores e Friends em C++

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.

Tags: C++ OOP classes constructors destructors

Publicado em 8-24 02:06