Strings em C: arrays de char e o terminador nulo

1. Conceitos Fundamentais: O que é uma String em C?

Em C, não existe um tipo de dado nativo chamado "string". O que chamamos de string é, na verdade, um array de caracteres (char) que termina obrigatoriamente com um caractere especial: o terminador nulo ('\0'). Esse caractere tem valor numérico zero e serve como marcador de fim da string.

A diferença crucial entre um array de char genérico e uma string é justamente a presença do '\0'. Um array de caracteres pode conter qualquer sequência de bytes, mas só é considerado uma string se for terminado por nulo.

// Array de char genérico (NÃO é uma string)
char nao_string[] = {'a', 'b', 'c'};

// String propriamente dita
char string_valida[] = {'a', 'b', 'c', '\0'};

2. Declaração e Inicialização de Strings

Existem várias formas de declarar e inicializar strings em C. A mais comum é usando literais entre aspas duplas:

#include <stdio.h>

int main() {
    // Inicialização literal - o '\0' é adicionado automaticamente
    char str1[] = "Hello";

    // Equivalente manual - você adiciona o '\0' explicitamente
    char str2[] = {'H', 'e', 'l', 'l', 'o', '\0'};

    // Declaração com tamanho fixo - precisa reservar espaço para '\0'
    char str3[10] = "Hello";  // espaço para 6 caracteres + 4 extras

    // String constante via ponteiro (não modificável)
    char *str4 = "Hello";

    printf("str1: %s\n", str1);
    printf("str2: %s\n", str2);
    printf("str3: %s\n", str3);
    printf("str4: %s\n", str4);

    return 0;
}

No caso de str3[10] = "Hello", o array tem tamanho 10, mas a string ocupa apenas 6 posições (5 letras + '\0'). O restante fica preenchido com zeros.

3. Acessando e Percorrendo Strings

O acesso aos caracteres individuais é feito por índice, como em qualquer array. O percorrimento tradicional usa um laço que continua até encontrar o '\0':

#include <stdio.h>

int main() {
    char mensagem[] = "C linguagem";

    // Acesso por índice
    printf("Primeiro caractere: %c\n", mensagem[0]);
    printf("Quinto caractere: %c\n", mensagem[4]);

    // Percorrendo com while até '\0'
    int i = 0;
    while (mensagem[i] != '\0') {
        printf("mensagem[%d] = '%c'\n", i, mensagem[i]);
        i++;
    }

    // CUIDADO: acesso fora dos limites - comportamento indefinido!
    // printf("%c", mensagem[20]);  // Pode crashar ou ler lixo

    return 0;
}

Importante: C não faz verificação automática de limites. Acessar posições além do array pode causar comportamento indefinido, incluindo falhas de segmentação.

4. O Terminador Nulo: Guardião da Integridade

O '\0' é o que define onde a string termina, não o tamanho do array. Isso tem implicações profundas:

#include <stdio.h>

int main() {
    // Array maior que a string
    char buffer[50] = "Teste";
    // O '\0' está na posição 5, mas o array tem 50 posições

    printf("Tamanho do array: %zu\n", sizeof(buffer));  // 50
    printf("String: %s\n", buffer);                     // "Teste"

    // O PERIGO: string sem '\0'
    char sem_terminador[] = {'O', 'l', 'a'};  // sem '\0'!
    printf("%s\n", sem_terminador);  // Pode imprimir "Ola" + lixo até encontrar um 0

    return 0;
}

Funções como printf("%s") e todas da biblioteca string.h dependem do '\0' para saber onde parar. Sem ele, continuam lendo memória adjacente até encontrar um byte zero aleatório.

5. Principais Operações com Strings (Sem Funções Prontas)

Para entender como as funções padrão funcionam, vejamos implementações manuais:

#include <stdio.h>

// Cálculo manual do comprimento
int meu_strlen(char str[]) {
    int len = 0;
    while (str[len] != '\0') {
        len++;
    }
    return len;
}

// Cópia manual
void meu_strcpy(char destino[], char origem[]) {
    int i = 0;
    while (origem[i] != '\0') {
        destino[i] = origem[i];
        i++;
    }
    destino[i] = '\0';  // Não esquecer o terminador!
}

// Concatenação manual
void meu_strcat(char destino[], char origem[]) {
    int i = 0, j = 0;

    // Encontra o fim da primeira string
    while (destino[i] != '\0') {
        i++;
    }

    // Copia a segunda string a partir daí
    while (origem[j] != '\0') {
        destino[i] = origem[j];
        i++;
        j++;
    }
    destino[i] = '\0';
}

int main() {
    char str1[20] = "Bom ";
    char str2[] = "dia!";

    printf("Comprimento de '%s': %d\n", str2, meu_strlen(str2));

    meu_strcat(str1, str2);
    printf("Concatenado: %s\n", str1);

    char copia[20];
    meu_strcpy(copia, str1);
    printf("Copia: %s\n", copia);

    return 0;
}

6. Strings e Funções da Biblioteca Padrão (string.h)

A biblioteca <string.h> oferece funções que operam sobre strings terminadas por nulo:

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

int main() {
    char str1[30] = "Programando";
    char str2[] = " em C";

    // strlen: retorna o comprimento (sem contar '\0')
    printf("Tamanho de '%s': %zu\n", str1, strlen(str1));

    // strcpy: copia str2 para str1 (CUIDADO com estouro!)
    strcpy(str1, "Nova string");
    printf("Após strcpy: %s\n", str1);

    // strcat: concatena (também pode estourar buffer!)
    strcat(str1, str2);
    printf("Após strcat: %s\n", str1);

    // strcmp: compara caractere a caractere
    char a[] = "abc";
    char b[] = "abd";
    int resultado = strcmp(a, b);
    printf("Comparação 'abc' vs 'abd': %d\n", resultado);  // negativo

    return 0;
}

Riscos: strcpy e strcat não verificam se o destino tem espaço suficiente. Se a string de origem for maior que o buffer de destino, ocorre estouro de buffer, uma das vulnerabilidades mais comuns em C.

7. Armadilhas Comuns e Boas Práticas

Armadilha 1: Esquecer o espaço para '\0'

char nome[5] = "Maria";  // ERRO: precisa de 6 posições (5 letras + '\0')
// Isso compila, mas pode corromper memória adjacente!

Armadilha 2: Confundir tamanho do array com comprimento da string

char texto[100] = "Curto";
printf("%zu\n", sizeof(texto));   // 100 (tamanho do array)
printf("%zu\n", strlen(texto));   // 5 (comprimento da string)

Boas práticas:
- Sempre reserve um caractere extra para o '\0'
- Use fgets em vez de gets para leitura segura
- Prefira funções com limite de tamanho como strncpy e strncat

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

int main() {
    char nome[20];

    // fgets é seguro: lê no máximo 19 caracteres + '\0'
    printf("Digite seu nome: ");
    fgets(nome, sizeof(nome), stdin);

    // Remove o '\n' que fgets pode incluir
    size_t len = strlen(nome);
    if (len > 0 && nome[len-1] == '\n') {
        nome[len-1] = '\0';
    }

    printf("Olá, %s!\n", nome);

    // Versão segura com limite de tamanho
    char destino[10];
    strncpy(destino, nome, sizeof(destino) - 1);
    destino[sizeof(destino) - 1] = '\0';  // Garante terminação

    return 0;
}

Referências