Em C, muitos erros são sutis e podem passar despercebidos, causando falhas em tempo de execução, comportamento indefinido ou resultados incorretos. Nesta aula, vamos explorar quatro dos erros mais comuns cometidos por programadores de C: off-by-one, esquecimento de free, confusão entre = e ==, e uso de variáveis não inicializadas. Ao entender esses equívocos, você estará mais preparado para escrever código robusto e confiável.

Esses erros não são apenas para iniciantes; até programadores experientes podem cometê-los em situações de pressa ou complexidade. Portanto, dominar a identificação e correção desses problemas é essencial para qualquer desenvolvedor C.

Off-by-one

O erro off-by-one (ou erro de deslocamento de um) ocorre quando um loop ou iteração é executado uma vez a mais ou uma vez a menos do que o necessário. Isso geralmente acontece ao usar índices de arrays, que em C começam em 0, e ao definir condições de parada com <= em vez de <, ou ao esquecer de incluir o último elemento.

Por exemplo, se você tem um array de tamanho N, os índices válidos vão de 0 a N-1. Um loop que itera de 0 até N (inclusive) acessará um elemento fora dos limites, causando comportamento indefinido. Isso é um clássico off-by-one. Vamos ver um exemplo que imprime os elementos de um array:

#include <stdio.h>

int main() {
    int arr[5] = {10, 20, 30, 40, 50};
    int i;

    // Errado: i <= 5 acessa arr[5] que não existe
    for (i = 0; i <= 5; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");

    // Correto: i < 5
    for (i = 0; i < 5; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");

    return 0;
}

No primeiro loop, quando i chega a 5, arr[5] está fora dos limites do array, o que pode corromper memória ou causar falhas. O segundo loop está correto. É importante sempre lembrar que o índice de um array de tamanho N varia de 0 a N-1.

Outra situação comum é ao percorrer uma string. O tamanho de uma string inclui o caractere nulo '\0', mas a condição de parada deve ser o caractere nulo, não o tamanho. Veja:

#include <stdio.h>

int main() {
    char str[] = "Hello";
    int len = sizeof(str) / sizeof(str[0]) - 1; // 5, excluindo o nulo
    int i;

    // Errado: i <= len acessa o caractere nulo
    for (i = 0; i <= len; i++) {
        printf("%c", str[i]);
    }
    printf("\n");

    // Correto: i < len
    for (i = 0; i < len; i++) {
        printf("%c", str[i]);
    }
    printf("\n");

    return 0;
}

No primeiro loop, o caractere nulo é impresso, o que pode não ser visível, mas é um erro lógico. Sempre verifique os limites ao usar loops com arrays e strings.

Esquecer free

Quando você aloca memória dinamicamente com malloc, calloc ou realloc, é sua responsabilidade liberá-la com free quando não for mais necessária. Esquecer de chamar free causa vazamento de memória, o que pode esgotar a memória disponível e degradar o desempenho do programa, especialmente em aplicações de longa duração.

Vamos analisar um exemplo onde a memória é alocada e não liberada:

#include <stdio.h>
#include <stdlib.h>

int main() {
    int *p = (int*)malloc(sizeof(int) * 10);
    if (p == NULL) {
        fprintf(stderr, "Falha na alocação\n");
        return 1;
    }

    // Uso da memória
    for (int i = 0; i < 10; i++) {
        p[i] = i * 2;
    }

    // Esquecer de free(p); aqui é um vazamento de memória

    return 0;
}

Neste código, a memória alocada para p não é liberada antes do retorno. Embora o sistema operacional libere a memória quando o programa termina, em programas que executam por muito tempo, vazamentos repetidos podem levar à exaustão de memória. Para evitar isso, sempre libere a memória quando ela não for mais necessária:

free(p);

Além disso, é uma boa prática definir o ponteiro como NULL após liberar, para evitar ponteiros pendentes:

free(p);
 p = NULL;

Isso ajuda a evitar acesso acidental a memória liberada. Outro ponto importante é que você deve liberar memória na mesma ordem inversa da alocação, especialmente se houver dependências entre estruturas de dados.

Para detectar vazamentos de memória, você pode usar ferramentas como Valgrind, que analisa o uso de memória em tempo de execução e reporta vazamentos. Considere integrar essas ferramentas ao seu fluxo de desenvolvimento.

Comparar com = vs ==

Em C, o operador = é de atribuição, enquanto == é de comparação. É muito comum, especialmente para iniciantes, usar = dentro de uma condição if, o que não compara valores, mas atribui um valor à variável, resultando em comportamento inesperado. Isso pode ser um erro silencioso, pois a expressão de atribuição sempre avalia para o valor atribuído, que pode ser diferente de zero (verdadeiro) ou zero (falso).

Vejamos um exemplo típico:

#include <stdio.h>

int main() {
    int x = 5;
    int y = 10;

    // Errado: usa = em vez de ==
    if (x = y) {
        printf("x é igual a y\n");
    } else {
        printf("x não é igual a y\n");
    }

    // Correto:
    if (x == y) {
        printf("x é igual a y\n");
    } else {
        printf("x não é igual a y\n");
    }

    return 0;
}

No primeiro if, x = y atribui o valor de y (10) a x, e a expressão avalia para 10, que é diferente de zero, então o bloco é executado. Além disso, x é alterado para 10, mudando o estado do programa. No segundo if, a comparação é feita corretamente e, como x agora é 10, a condição é verdadeira, mas isso é uma coincidência. Para evitar esse erro, muitos programadores invertem a ordem: colocam a constante à esquerda, como if (y == x) ou if (10 == x). Assim, se você digitar if (10 = x), o compilador emitirá um erro, pois não é possível atribuir a uma constante.

Outra técnica é habilitar avisos do compilador, como -Wparentheses no GCC, que emite um aviso quando uma atribuição é usada em uma condição. Sempre preste atenção aos avisos do compilador.

Além disso, cuidado ao usar atribuições em expressões booleanas, como em while ((ch = getchar()) != EOF). Nesse caso, a atribuição é intencional e correta, porque precisamos atribuir e comparar ao mesmo tempo. Mas é importante entender que a atribuição retorna o valor atribuído.

Variáveis não inicializadas

Em C, variáveis locais não são inicializadas automaticamente com zero; elas contêm valores indeterminados (lixo) que podem ser qualquer coisa. Usar uma variável não inicializada é um erro grave, pois o comportamento é indefinido. Isso pode levar a resultados imprevisíveis, falhas de segurança e bugs difíceis de rastrear.

Vamos ver um exemplo:

#include <stdio.h>

int main() {
    int contador;
    int i;

    // Errado: contador não inicializado
    for (i = 0; i < 5; i++) {
        contador += i;
    }
    printf("Soma: %d\n", contador);

    // Correto: inicializar contador com 0
    contador = 0;
    for (i = 0; i < 5; i++) {
        contador += i;
    }
    printf("Soma: %d\n", contador);

    return 0;
}

No primeiro loop, contador começa com um valor lixo, e somar a ele produz um resultado imprevisível. No segundo loop, inicializamos contador com 0, garantindo um resultado correto.

Variáveis globais e estáticas são inicializadas com zero automaticamente, mas é sempre uma boa prática inicializar explicitamente para tornar o código mais claro e evitar confusão. Além disso, use compiladores com avisos habilitados, como -Wuninitialized no GCC, que emite avisos para variáveis potencialmente não inicializadas.

Outro caso comum é usar ponteiros não inicializados. Um ponteiro não inicializado pode apontar para qualquer lugar, e desreferenciá-lo causa comportamento indefinido. Sempre inicialize ponteiros com NULL ou com um endereço válido.

int *p;
// ...
*p = 10; // Errado: p não inicializado

int *q = NULL;
if (q != NULL) {
    *q = 10; // Seguro
}

Inicializar variáveis é uma das melhores práticas para evitar bugs. Não confie em valores padrão implícitos.

Boas Práticas e Observações Finais

Para evitar esses erros comuns, adote as seguintes práticas:

  • Sempre inicialize variáveis no momento da declaração, mesmo que seja com 0 ou NULL.
  • Ao usar loops com arrays, use < em vez de <= e lembre-se de que o índice máximo é tamanho - 1.
  • Para cada malloc ou calloc, tenha um free correspondente. Use ferramentas como Valgrind para detectar vazamentos.
  • Em condições, use == para comparação. Para evitar erros de digitação, coloque a constante à esquerda, como if (5 == x).
  • Compile com avisos habilitados: gcc -Wall -Wextra -Werror no GCC ou clang -Weverything no Clang.
  • Revise seu código com atenção e faça testes unitários para verificar o comportamento esperado.

Esses erros são comuns, mas com prática e disciplina, você pode evitá-los. A chave é entender profundamente como a linguagem funciona e sempre escrever código defensivo.

Exercícios

  1. O código abaixo tem um erro off-by-one. Identifique e corrija.

    #include <stdio.h>
    
    int main() {
        int vetor[5] = {1, 2, 3, 4, 5};
        int i;
        for (i = 0; i <= 5; i++) {
            printf("%d\n", vetor[i]);
        }
        return 0;
    }
    

    ✓ Resposta: O erro é que o loop vai até i <= 5, acessando o índice 5 que não existe. A correção é usar i < 5.
  2. O programa a seguir aloca memória para um array de inteiros, mas não libera. Adicione a chamada free correta.

    #include <stdlib.h>
    
    int main() {
        int *p = malloc(10 * sizeof(int));
        if (p == NULL) return 1;
        // uso
        return 0;
    }
    

    ✓ Resposta: Adicione free(p); antes do return 0;.
  3. Explique a diferença entre = e == no contexto de uma condição if e dê um exemplo de erro comum.

    ✓ Resposta: = é atribuição, == é comparação. Erro comum: if (x = 5) atribui 5 a x e a condição é verdadeira (5 != 0), em vez de verificar se x é igual a 5. O correto é if (x == 5).
  4. O código abaixo usa uma variável não inicializada. Corrija.

    #include <stdio.h>
    
    int main() {
        int total;
        int i;
        for (i = 0; i < 10; i++) {
            total += i;
        }
        printf("Total: %d\n", total);
        return 0;
    }
    

    ✓ Resposta: Inicialize total com 0 antes do loop: int total = 0;.
  5. Escreva um programa que leia 5 números do usuário, armazene-os em um array alocado dinamicamente e depois calcule a soma. Certifique-se de liberar a memória corretamente.

    ✓ Resposta:
    #include <stdio.h>
    #include <stdlib.h>
    
    int main() {
        int *arr = malloc(5 * sizeof(int));
        if (arr == NULL) {
            printf("Erro de alocação\n");
            return 1;
        }
        int soma = 0;
        for (int i = 0; i < 5; i++) {
            printf("Digite o número %d: ", i+1);
            scanf("%d", &arr[i]);
            soma += arr[i];
        }
        printf("Soma: %d\n", soma);
        free(arr);
        return 0;
    }
    

Referências