Strings: Vetores de Caracteres e a Biblioteca string.h

[351] Strings: Vetores de Caracteres e a Biblioteca string.h

A palavra Ana tem três letras e ocupa quatro bytes: o quarto é o terminador nulo, e quem esquece de reservá-lo passa horas atrás de um bug invisível. Daqui saem a diferença entre strlen e sizeof, por que literal apontada por ponteiro não se modifica, e o estouro de buffer que strcpy não impede.
Linguagem C

11 min de leitura

Toda linguagem moderna trata texto como algo natural: você junta, corta e compara palavras sem pensar. O C não. Aqui, uma string é apenas um vetor de caracteres com uma regra secreta — e quem não conhece essa regra passa horas caçando bugs invisíveis. Hoje você vai conhecê-la, e a partir de agora vai enxergar texto em C como ele realmente é: memória bruta, um caractere por casa, encerrada por um marcador silencioso.

A regra secreta: o terminador nulo

Em C, uma string é um vetor de char que termina com um caractere especial: o nulo, escrito '\0' (o byte de valor zero). Esse marcador não é um detalhe — é o que define onde a string acaba. As funções de texto do C não sabem o tamanho da string; elas simplesmente leem caractere por caractere até esbarrar no '\0'.

Quando você escreve uma string entre aspas duplas, o C adiciona o terminador automaticamente:

#include <stdio.h>

int main(void) {
    char nome[] = "Ana";
    // Na memória: 'A' 'n' 'a' '\0'  -> quatro bytes, não três!

    printf("Texto: %s\n", nome);
    printf("Ocupa %zu bytes na memória.\n", sizeof(nome)); // 4
    return 0;
}

Repare: "Ana" tem três letras, mas ocupa quatro bytes. O quarto é o '\0' invisível. Esquecer de reservar espaço para ele é a causa número um de bugs com strings — se você declarar char nome[3] = "Ana";, não haverá lugar para o terminador, e as funções de texto vão continuar lendo memória alheia até encontrar um zero por acaso.

String literal via ponteiro vs. vetor de char

Há duas formas comuns de criar uma string, e a diferença entre elas importa:

char  vetor[]   = "mutável";   // uma CÓPIA, que você pode alterar
char *ponteiro  = "constante"; // aponta para memória somente-leitura

O vetor[] cria seu próprio espaço, copiando os caracteres — você pode modificá-lo à vontade. Já o char *ponteiro aponta para uma string literal que costuma viver em memória somente-leitura; tentar alterá-la (ponteiro[0] = 'X';) é comportamento indefinido e frequentemente trava o programa. Regra prática: se pretende modificar a string, declare-a como vetor (char v[] = "...").

Lendo o tamanho com strlen

Como o C não guarda o comprimento, para descobri-lo é preciso contar até o '\0'. A função strlen, do cabeçalho <string.h>, faz isso:

#include <stdio.h>
#include <string.h>

int main(void) {
    char frase[] = "Linguagem C";
    printf("Comprimento: %zu\n", strlen(frase)); // 11
    printf("Bytes na memória: %zu\n", sizeof(frase)); // 12 (inclui o '\0')
    return 0;
}

Guarde a distinção: strlen conta os caracteres visíveis (11), sem o terminador; sizeof mede os bytes reservados (12), incluindo o '\0'. Confundir os dois gera erros clássicos de "off-by-one".

Vale entender como strlen funciona por dentro — é um belo exemplo de tudo que aprendemos sobre ponteiros:

size_t meu_strlen(const char *s) {
    size_t n = 0;
    while (s[n] != '\0') {  // avança até o terminador
        n++;
    }
    return n;
}

Ela simplesmente caminha pelo vetor contando, e para no '\0'. Toda a biblioteca de strings do C é feita dessa ideia.

Copiar e concatenar: as ferramentas do string.h

O C não deixa você fazer s1 = s2 para copiar strings — isso só copiaria ponteiros, não o conteúdo. Para trabalhar com texto, usamos funções do <string.h>. As mais comuns:

#include <stdio.h>
#include <string.h>

int main(void) {
    char destino[50];

    strcpy(destino, "Olá");       // copia "Olá" para destino
    strcat(destino, ", mundo!");  // concatena ", mundo!" ao final

    printf("%s\n", destino);       // Olá, mundo!
    printf("Tamanho: %zu\n", strlen(destino)); // 11 (bytes de acentos à parte)

    // Comparação: retorna 0 se forem iguais
    if (strcmp(destino, "Olá, mundo!") == 0) {
        printf("As strings são iguais.\n");
    }
    return 0;
}

Três funções essenciais: strcpy copia, strcat concatena, strcmp compara. Uma observação sobre strcmp: ela devolve 0 quando as strings são iguais — o que confunde quem espera um "verdadeiro". Pense nela como "diferença": zero significa "nenhuma diferença".

O perigo mora aqui: estouro de buffer

Agora a parte séria. As funções strcpy e strcat não verificam se cabe. Se o destino não tiver espaço suficiente, elas escrevem além do fim do vetor, corrompendo a memória vizinha:

char pequeno[5];
strcpy(pequeno, "Esta frase é gigante"); // DESASTRE: escreve muito além dos 5 bytes

Esse é o infame buffer overflow, uma das falhas de segurança mais exploradas da história da computação. A defesa é usar as versões "com limite", que recebem o tamanho máximo do destino e param antes de estourar:

Para ver o estouro acontecer: cole o exemplo no Compiler Explorer com -fsanitize=address. Sem a flag, um strcpy grande demais costuma "funcionar" — e é esse silêncio que fez do estouro de buffer a falha mais explorada da história. Com ela, o programa para na hora e informa quantos bytes passaram do fim do vetor.

#include <stdio.h>
#include <string.h>

int main(void) {
    char destino[10];

    strncpy(destino, "Texto muito longo", sizeof(destino) - 1);
    destino[sizeof(destino) - 1] = '\0'; // garante o terminador

    printf("%s\n", destino); // Texto mui
    return 0;
}

O strncpy copia no máximo n caracteres, evitando o estouro. Mas ele tem uma pegadinha: se o texto for maior que o limite, ele não adiciona o '\0' sozinho — por isso a linha seguinte força o terminador manualmente. Sempre garanta o terminador ao usar strncpy. Essa disciplina defensiva é o preço de trabalhar com texto em C.

Percorrendo uma string caractere a caractere

Como uma string é um vetor, você a percorre exatamente como aprendeu — o '\0' é a condição de parada natural:

#include <stdio.h>

int main(void) {
    char texto[] = "C é elegante";
    int vogais = 0;

    for (int i = 0; texto[i] != '\0'; i++) {
        char c = texto[i];
        if (c=='a'||c=='e'||c=='i'||c=='o'||c=='u'||
            c=='A'||c=='E'||c=='I'||c=='O'||c=='U') {
            vogais++;
        }
    }

    printf("O texto tem %d vogais.\n", vogais);
    return 0;
}

O laço não precisa saber o comprimento antecipadamente: ele anda enquanto o caractere atual não for o terminador. Esse padrão — "vá até o '\0'" — é a assinatura de todo processamento de texto em C.

Um utilitário completo

Vamos juntar tudo numa função que conta as palavras de uma frase:

#include <stdio.h>

int contar_palavras(const char *s) {
    int palavras = 0;
    int dentro = 0; // estamos no meio de uma palavra?

    for (int i = 0; s[i] != '\0'; i++) {
        if (s[i] != ' ') {
            if (!dentro) {   // acabamos de entrar numa palavra
                palavras++;
                dentro = 1;
            }
        } else {
            dentro = 0;      // espaço: saímos da palavra
        }
    }
    return palavras;
}

int main(void) {
    printf("%d\n", contar_palavras("dominando a linguagem C")); // 4
    return 0;
}

Repare como o algoritmo só precisa de um caractere por vez e do terminador para saber quando parar — todo o resto é lógica sobre o que já dominamos.

O que vem a seguir

Hoje desmistificamos as strings: são vetores de char fechados por um '\0', e a biblioteca <string.h> nos dá ferramentas para copiá-las, concatená-las e compará-las — sempre com o cuidado de não estourar buffers. Na próxima aula, subimos mais um nível na hierarquia de ponteiros: vamos ao ponteiro para ponteiro, e usá-lo num contexto que você encontra em todo programa C do mundo real — os argumentos de linha de comando, aquele int main(int argc, char *argv[]) que até agora evitamos.

Fontes e leituras recomendadas

Exercícios

Exercício 1

Declare char saudacao[] = "Bom dia";. Imprima o texto com %s, seu comprimento com strlen e o total de bytes que ocupa com sizeof. Explique por que os dois números diferem em 1.

Ver resposta

✓ Resposta:

#include <stdio.h>
#include <string.h>
int main(void) {
    char saudacao[] = "Bom dia";
    printf("%s\n", saudacao);
    printf("strlen: %zu\n", strlen(saudacao)); // 7
    printf("sizeof: %zu\n", sizeof(saudacao));  // 8
    return 0;
}

strlen conta os 7 caracteres visíveis; sizeof mede os 8 bytes reservados, incluindo o terminador '\0' que o C acrescentou. A diferença de 1 é exatamente esse terminador.

Exercício 2

Escreva sua própria versão de strlen, chamada int comprimento(const char *s), sem usar a função da biblioteca. Teste com três strings diferentes.

Ver resposta

✓ Resposta:

#include <stdio.h>
int comprimento(const char *s) {
    int n = 0;
    while (s[n] != '\0') {
        n++;
    }
    return n;
}
int main(void) {
    printf("%d\n", comprimento("C"));       // 1
    printf("%d\n", comprimento("Rust"));    // 4
    printf("%d\n", comprimento(""));        // 0 (string vazia)
    return 0;
}

A string vazia "" tem comprimento 0 porque seu primeiro caractere já é o '\0'.

Exercício 3

Escreva uma função void para_maiuscula(char *s) que converta uma string para letras maiúsculas, modificando-a no lugar. Dica: para letras minúsculas de 'a' a 'z', subtraia 32 (a diferença ASCII) ou use toupper de <ctype.h>.

Ver resposta

✓ Resposta:

#include <stdio.h>
#include <ctype.h>
void para_maiuscula(char *s) {
    for (int i = 0; s[i] != '\0'; i++) {
        s[i] = (char) toupper((unsigned char) s[i]);
    }
}
int main(void) {
    char texto[] = "linguagem c";
    para_maiuscula(texto);
    printf("%s\n", texto); // LINGUAGEM C
    return 0;
}

Usar toupper de <ctype.h> é mais seguro e portável do que subtrair 32 na mão, pois lida corretamente com caracteres que não são letras (deixa-os intactos). O (unsigned char) no argumento evita comportamento indefinido com caracteres de valor negativo.

Exercício 4

Explique o que há de perigoso no trecho char buf[8]; strcpy(buf, "sequência longa demais");. Reescreva-o de forma segura usando strncpy.

Ver resposta

✓ Resposta: O buffer buf tem só 8 bytes, mas o texto a ser copiado tem muito mais que isso. O strcpy não verifica o tamanho do destino: ele vai escrever a string inteira além dos 8 bytes, corrompendo a memória vizinha — um clássico buffer overflow, que pode travar o programa ou virar uma brecha de segurança. Versão segura:

char buf[8];
strncpy(buf, "sequência longa demais", sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0'; // garante o terminador

Copiamos no máximo 7 caracteres e forçamos o '\0' na última posição, respeitando os limites do buffer.

Exercício 5

Escreva uma função int eh_palindromo(const char *s) que retorne 1 se a string for um palíndromo (lê-se igual de trás para frente, considerando só letras e ignorando maiúsculas/minúsculas) e 0 caso contrário. Teste com "arara" e "casa".

Ver resposta

✓ Resposta:

#include <stdio.h>
#include <ctype.h>
#include <string.h>

int eh_palindromo(const char *s) {
    int i = 0;
    int j = (int) strlen(s) - 1;
    while (i < j) {
        if (tolower((unsigned char) s[i]) != tolower((unsigned char) s[j])) {
            return 0; // caracteres diferentes: não é palíndromo
        }
        i++;
        j--;
    }
    return 1;
}

int main(void) {
    printf("%d\n", eh_palindromo("arara")); // 1
    printf("%d\n", eh_palindromo("casa"));  // 0
    return 0;
}

Usamos dois índices que caminham das pontas para o centro (i sobe, j desce), comparando os caracteres correspondentes. Se todos baterem até se cruzarem, é um palíndromo. O tolower torna a comparação insensível a maiúsculas/minúsculas.

Comentários

Mais em Linguagem C

Ponteiros para Struct e o Operador Seta
Ponteiros para Struct e o Operador Seta

Os parênteses em (*ptr).x não são preciosismo: sem eles o ponto vence o…

Comunicação em Rede com Sockets
Comunicação em Rede com Sockets

Depois de conectado, um socket se lê e se escreve como arquivo — e é essa…

Listas Duplamente Encadeadas e Circulares
Listas Duplamente Encadeadas e Circulares

Numa lista dupla, toda ligação tem dois lados: fez A->proximo = B, tem de…