O Fim do char* — Textos de Verdade com std::string

O Fim do char* — Textos de Verdade com std::string

Reservar cinquenta caracteres e torcer para caber, copiar sem estourar, percorrer tudo atrás do terminador: texto em C é vigilância constante. O std::string varre quase tudo isso — sabe o próprio tamanho, cresce sozinho e cuida da memória. É também a primeira aparição do RAII, que domina a fase seguinte.
Linguagem C++

10 min de leitura

Fechei o artigo Um Nome, Vários Sentidos — Sobrecarga de Funções prometendo aposentar o char*. Cumpro agora. De todas as dores que você carrega do C, poucas são tão constantes quanto lidar com texto: char nome[50], torcer para caber, strcpy sem estourar, strlen percorrendo tudo à procura do '\0', strcat que exige espaço pré-alocado, e a memória que você aloca e você libera. O std::string do C++ varre quase tudo isso para debaixo do tapete — e o mais importante: ele cuida da própria memória. Ao entender como, você vai provar pela primeira vez o mecanismo que domina toda a Fase 2, o RAII. Guarde essa sensação; ela é a chave do curso inteiro.

O texto que sabe seu próprio tamanho e cresce sozinho

Em C, uma string é um vetor de char com um '\0' no fim, e o tamanho você descobre percorrendo. Em C++, std::string é um objeto que guarda seu tamanho e gerencia seu próprio buffer:

#include <iostream>
#include <string>

int main() {
    std::string s = "Olá";     // aloca o necessário; nada de tamanho fixo
    s += ", mundo";             // cresce sozinha — sem strcat, sem buffer manual
    s += "!";

    std::cout << s << '\n';              // Olá, mundo!
    std::cout << "tamanho: " << s.size() << '\n';  // O(1): já sabe o tamanho
    std::cout << "vazia? " << (s.empty() ? "sim" : "não") << '\n';
    return 0;
}

Compare com o equivalente em C: você declararia um buffer, checaria capacidade antes de cada strcat, chamaria strlen toda vez que precisasse do tamanho. Aqui, += concatena e realoca se preciso, .size() responde na hora, e não há um único malloc à vista. O objeto faz o trabalho sujo.

Comparar, buscar, fatiar — sem funções soltas

O que em C exigia strcmp, strstr e aritmética de ponteiro vira método ou operador:

#include <iostream>
#include <string>

int main() {
    std::string a = "banana";
    std::string b = "banana";

    // Comparação direta com == (em C seria strcmp(a,b) == 0)
    if (a == b) std::cout << "iguais\n";

    // Busca: retorna o índice, ou std::string::npos se não achar
    std::string frase = "o rato roeu a roupa";
    auto pos = frase.find("roeu");
    if (pos != std::string::npos)
        std::cout << "achei em " << pos << '\n';   // achei em 8

    // Fatia: substr(inicio, quantidade)
    std::cout << frase.substr(2, 4) << '\n';        // "rato"

    // Índice individual, como em C, mas com .at() opcionalmente checado
    std::cout << frase[0] << '\n';                  // 'o'
    return 0;
}

Uma honestidade importante sobre índices: frase[i] não verifica limites — passar do fim é comportamento indefinido, exatamente como indexar fora de um vetor em C. Se quiser a checagem, frase.at(i) lança uma exceção ao ultrapassar o tamanho (exceções são assunto da Fase 6). Use [] no caminho quente onde você já garantiu o índice, e .at() quando a segurança vale o custo.

Lendo linhas inteiras: std::getline

No artigo A Filosofia por Trás do C++ e a Saída Elegante do printf vimos que std::cin >> s para no primeiro espaço. Para ler uma linha completa, existe std::getline:

#include <iostream>
#include <string>

int main() {
    std::cout << "Digite uma frase: ";
    std::string linha;
    std::getline(std::cin, linha);   // lê até o '\n', espaços incluídos
    std::cout << "Você escreveu: " << linha << '\n';
    return 0;
}

Isso resolve, sem drama, o que em C exigia fgets com um buffer de tamanho arbitrário e o risco de a linha não caber.

A primeira degustação de RAII

Chegamos ao ponto central. Pergunte-se: quando aquela std::string s do primeiro exemplo é destruída, quem libera a memória que ela alocou para crescer? A resposta é o próprio std::string. Quando o objeto sai de escopo — quando a chave } que o contém termina —, um método especial dele, o destrutor, roda automaticamente e devolve a memória ao sistema. Você não escreve free. Você não pode esquecer o free. Não existe vazamento possível, porque a liberação está amarrada ao fim de vida do objeto:

#include <string>

void demonstra() {
    std::string temp = "aloco bastante texto aqui...";
    // temp usa memória do heap para guardar o texto.
    // Ao chegar no '}', o destrutor de temp roda e libera tudo.
    // Nenhum free() escrito por você. Impossível vazar.
}   // ← aqui a memória de 'temp' é devolvida, automaticamente

Esse é o RAII — Resource Acquisition Is Initialization: o recurso (a memória) é adquirido quando o objeto nasce e liberado quando ele morre, e o "morrer" acontece sozinho no fim do escopo. É a resposta do C++ ao par malloc/free do C, e a Fase 2 inteira, a partir do próximo bloco de artigos, é dedicada a você aprender a construir seus próprios tipos que se comportam assim. std::string é só o primeiro de muitos.

Onde o char* ainda aparece: conversando com C

Seria desonesto dizer que o char* sumiu de vez. Bibliotecas de C — e há montanhas delas — esperam const char*. Para entregar sua std::string a elas, use .c_str(), que devolve um ponteiro para o conteúdo terminado em '\0':

#include <string>
#include <cstdio>   // printf, uma API de C

int main() {
    std::string nome = "Marcelo";
    // printf espera const char* — .c_str() faz a ponte
    std::printf("Olá, %s\n", nome.c_str());
    return 0;
}

Uma ressalva honesta e importante: o ponteiro de .c_str() é válido apenas enquanto a std::string existir e não for modificada. Guardá-lo e usá-lo depois de a string mudar ou morrer é o mesmo dangling pointer de sempre. Use-o na hora, para a chamada, e não o estoque.

O que o std::string entrega não é conveniência de sintaxe: é a memória deixando de ser problema seu. Ele adquire o buffer ao nascer, redimensiona quando precisa e libera ao sair de escopo — e é por isso que serve de primeira amostra do RAII. O char* não desaparece do mundo: continua sendo a fronteira com as bibliotecas escritas em C, e o c_str() existe para essa travessia, com a ressalva de que o ponteiro devolvido pertence à string e morre junto com ela.

Fontes e leituras recomendadas

  • cppreference.com/w/cpp/string/basic_string: a referência completa de std::string, com todos os métodos citados (find, substr, at, c_str) e suas garantias.
  • Bjarne Stroustrup, A Tour of C++ (3ª ed.), seção "Strings and Regular Expressions": a visão moderna do tipo, incluindo std::string_view (que veremos adiante).
  • cppreference.com/w/cpp/string/basic_string/c_str: os detalhes precisos sobre a validade do ponteiro retornado — leitura obrigatória antes de interoperar com C.
  • Scott Meyers, Effective C++ (3ª ed.), itens sobre gerenciamento de recursos: contextualizam por que tipos como std::string eliminam classes inteiras de bugs de memória.
  • ISO C++ Core Guidelines, regra SL.str.1 ("Use std::string to own character sequences"): a recomendação oficial de preferir std::string a arranjos de char.

Exercícios

Exercício 1

Leia uma frase inteira com std::getline e imprima quantos caracteres ela tem, usando .size().

Ver resposta

✓ Resposta: Direto ao ponto:

#include <iostream>
#include <string>
int main() {
    std::string linha;
    std::cout << "Frase: ";
    std::getline(std::cin, linha);
    std::cout << "Caracteres: " << linha.size() << '\n';
    return 0;
}

.size() responde em tempo constante — o objeto já conhece o próprio tamanho, ao contrário do strlen, que percorre.

Exercício 2

Dadas duas strings nome e sobrenome lidas do usuário, monte uma terceira completo = nome + " " + sobrenome e imprima-a com sua primeira letra maiúscula (reaproveite a ideia do artigo Referências e Ponteiros, Frente a Frente).

Ver resposta

✓ Resposta: Concatenação com + e ajuste do primeiro caractere:

#include <iostream>
#include <string>
#include <cctype>
int main() {
    std::string nome, sobrenome;
    std::cout << "Nome: ";      std::cin >> nome;
    std::cout << "Sobrenome: "; std::cin >> sobrenome;

    std::string completo = nome + " " + sobrenome;
    if (!completo.empty())
        completo[0] = std::toupper(completo[0]);

    std::cout << completo << '\n';
    return 0;
}

Exercício 3

Escreva uma função bool contem(const std::string& texto, const std::string& agulha) que devolve true se agulha aparece em texto. Use find e npos, e explique por que os parâmetros são const&.

Ver resposta

✓ Resposta: A função e a justificativa:

#include <string>
bool contem(const std::string& texto, const std::string& agulha) {
    return texto.find(agulha) != std::string::npos;
}

Os parâmetros são const& porque a função apenas as duas strings: a referência evita copiar textos possivelmente grandes a cada chamada, e o const garante — verificado pelo compilador — que nenhuma delas será alterada. É o padrão do artigo Referências e Ponteiros, Frente a Frente aplicado a um caso real.

Exercício 4

O programa abaixo tem um bug de tempo de vida. Aponte-o e corrija:

#include <cstdio>
#include <string>
const char* saudacao() {
    std::string s = "oi";
    return s.c_str();   // devolve ponteiro para o conteúdo de s
}
int main() {
    std::printf("%s\n", saudacao());
    return 0;
}
Ver resposta

✓ Resposta: O bug: saudacao devolve o ponteiro de .c_str() de uma std::string local, que é destruída ao fim da função. O printf no main usa memória já liberada — comportamento indefinido. Correção: devolver a própria std::string (que copia/move o conteúdo para o chamador) e chamar .c_str() só na hora do uso:

#include <cstdio>
#include <string>
std::string saudacao() {   // devolve por valor: o conteúdo sobrevive
    std::string s = "oi";
    return s;
}
int main() {
    std::string g = saudacao();
    std::printf("%s\n", g.c_str());  // c_str() usado enquanto 'g' está vivo
    return 0;
}

É a mesma lição da referência pendente do artigo Referências e Ponteiros, Frente a Frente, agora com ponteiro para o buffer interno.

Exercício 5

Sem usar nenhuma função da biblioteca além de .size() e indexação, escreva std::string invertida(const std::string& s) que devolve s de trás para frente. Depois reflita: em C, o que você teria de gerenciar manualmente que aqui o std::string cuida sozinho?

Ver resposta

✓ Resposta: A inversão manual:

#include <iostream>
#include <string>
std::string invertida(const std::string& s) {
    std::string r;                       // começa vazia
    for (std::size_t i = s.size(); i > 0; --i)
        r += s[i - 1];                   // acrescenta do fim para o começo
    return r;                            // devolve por valor
}
int main() {
    std::cout << invertida("mundo") << '\n';  // odnum
    return 0;
}

Em C, você teria de alocar um buffer do tamanho certo (mais o '\0'), copiar caractere a caractere e lembrar de free no chamador — três oportunidades de bug (tamanho errado, esquecer o terminador, vazar). Aqui, r cresce sozinha com +=, conhece o próprio tamanho e libera a memória automaticamente ao fim de vida. É RAII trabalhando por você.

Comentários

Mais em Linguagem C++

Percorrendo com Elegância — o Range-Based for a Fundo
Percorrendo com Elegância — o Range-Based for a Fundo

A sintaxe curta esconde uma decisão que custa caro se for tomada por descuido…

A Terceira Pedra — Um Motor de Consultas com Templates e Lambdas
A Terceira Pedra — Um Motor de Consultas com Templates e Lambdas

Consultar sem escrever laço exige que o filtro chegue como parâmetro, e é aí…

Posse Compartilhada e seus Perigos — shared_ptr e weak_ptr
Posse Compartilhada e seus Perigos — shared_ptr e weak_ptr

Quando várias partes do programa precisam legitimamente compartilhar um…