Implementação de uma Classe String Dinâmica Personalizada em C++

Desenvolver uma classe de string customizada em C++ é um exercício fundamental para compreender o geranciamento de memória dinâmica e o ciclo de vida de objetos. O exemplo a seguir demonstra como encapsular um buffer de caracteres, tratando alocações, realocações e a implementação dos membros especiais da classe (Rule of Three/Five).

Estrutura do Cabeçalho: StringCustom.hpp


#ifndef STRING_CUSTOM_H
#define STRING_CUSTOM_H

#include <iostream>

class StringCustom {
private:
    char* m_buffer;           // Ponteiro para o array de caracteres
    int m_capacidade;         // Tamanho total alocado
    int m_tamanho;            // Comprimento atual da string

    void redimensionar(int nova_capacidade);

public:
    // Construtores
    StringCustom();
    StringCustom(const char* texto);
    StringCustom(const StringCustom& outro);

    // Operador de Atribuição
    StringCustom& operator=(const StringCustom& outro);

    // Destrutor
    ~StringCustom();

    // Métodos de Acesso e Modificação
    bool estaVazio() const;
    void adicionar(char caractere);
    void removerUltimo();
    char& obterEm(int indice);
    void limpar();

    // Utilitários
    const char* c_str() const;
    int comprimento() const { return m_tamanho; }
    int capacidade() const { return m_capacidade; }
    void exibir() const;
};

#endif

Implementação da Lógica: StringCustom.cpp


#include "StringCustom.hpp"
#include <cstring>
#include <stdexcept>

// Inicialização padrão com buffer inicial
StringCustom::StringCustom() : m_capacidade(16), m_tamanho(0) {
    m_buffer = new char[m_capacidade];
    m_buffer[0] = '\0';
}

// Construtor a partir de literal de string
StringCustom::StringCustom(const char* texto) {
    m_tamanho = std::strlen(texto);
    m_capacidade = m_tamanho + 1;
    m_buffer = new char[m_capacidade];
    std::strcpy(m_buffer, texto);
}

// Implementação de Deep Copy (Cópia Profunda)
StringCustom::StringCustom(const StringCustom& outro) 
    : m_capacidade(outro.m_capacidade), m_tamanho(outro.m_tamanho) {
    m_buffer = new char[m_capacidade];
    std::strcpy(m_buffer, outro.m_buffer);
}

// Operador de atribuição com verificação de auto-atribuição
StringCustom& StringCustom::operator=(const StringCustom& outro) {
    if (this != &outro) {
        delete[] m_buffer;
        m_capacidade = outro.m_capacidade;
        m_tamanho = outro.m_tamanho;
        m_buffer = new char[m_capacidade];
        std::strcpy(m_buffer, outro.m_buffer);
    }
    return *this;
}

StringCustom::~StringCustom() {
    delete[] m_buffer;
}

void StringCustom::redimensionar(int nova_capacidade) {
    char* novo_buffer = new char[nova_capacidade];
    std::strcpy(novo_buffer, m_buffer);
    delete[] m_buffer;
    m_buffer = novo_buffer;
    m_capacidade = nova_capacidade;
}

void StringCustom::adicionar(char caractere) {
    if (m_tamanho + 1 >= m_capacidade) {
        redimensionar(m_capacidade * 2);
    }
    m_buffer[m_tamanho++] = caractere;
    m_buffer[m_tamanho] = '\0';
}

bool StringCustom::estaVazio() const {
    return m_tamanho == 0;
}

void StringCustom::removerUltimo() {
    if (m_tamanho > 0) {
        m_buffer[--m_tamanho] = '\0';
    }
}

char& StringCustom::obterEm(int indice) {
    if (indice < 0 || indice >= m_tamanho) {
        throw std::out_of_range("Índice fora dos limites do buffer");
    }
    return m_buffer[indice];
}

void StringCustom::limpar() {
    m_tamanho = 0;
    m_buffer[0] = '\0';
}

const char* StringCustom::c_str() const {
    return m_buffer;
}

void StringCustom::exibir() const {
    std::cout << m_buffer << std::endl;
}

Exemplo de Utilização: main.cpp


#include "StringCustom.hpp"

int main() {
    StringCustom str1("Engenharia");
    StringCustom str2 = str1; // Cópia via construtor

    str1.adicionar(' ');
    str1.adicionar('C');
    str1.adicionar('+');
    str1.adicionar('+');

    str1.exibir(); // Saída: Engenharia C++
    str2.exibir(); // Saída: Engenharia

    return 0;
}

Conceitos Fundamentais Gerenciados

Ao trabalhar com classes que gerenciam recursos externos (como memória no heap), é crucial observar os seguintes pilares da Programação Orientada a Objetos em C++:

  • Construtores: Responsáveis por garantir que o objeto comece em um estado válido. O uso de listas de inicialização aumenta a eficiência.
  • Destrutores: Essenciais para liberar memória alocada dinamicamente via new, evitando vazamentos de memória (memory leaks).
  • Cópia vs. Atribuição: O construtor de cópia cria um novo objeto a partir de um existente, enquanto o operador de atribuição lida com um objeto já inicializado, precisando liberar seus recursos antigos antes de copiar os novos.
  • Semântica de Movimentação: Em implementações avançadas, pode-se adicionar construtores de movimento para transferir a propriedade do buffer sem realizar cópias caras.
  • Segurança de Acesso: O método obterEm (ou at) fornece uma camada de segurança ao validar os índices antes de acessar a memória diretamente.

Tags: C++ OOP Memória Dinâmica Estruturas de Dados

Publicado em 6-5 03:10 por Thomas