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(ouat) fornece uma camada de segurança ao validar os índices antes de acessar a memória diretamente.