Errores típicos en C: warnings, desbordamientos y depuración básica

Capítulo 10

Tiempo estimado de lectura: 10 minutos

+ Ejercicio

Warnings: señales de calidad (no “ruido”)

Un warning es una advertencia del compilador: el programa puede compilar, pero hay algo que podría ser un error o un comportamiento inesperado. En C, muchos fallos reales empiezan como warnings ignorados. La práctica recomendada es compilar con advertencias estrictas y tratarlas como si fueran errores durante el desarrollo.

Configuración práctica de compilación

  • Activa warnings comunes y útiles: -Wall -Wextra
  • Convierte warnings en errores para no “acumular deuda”: -Werror
  • Mejora diagnósticos: -g (depuración) y -O0 (sin optimizar al depurar)
gcc -std=c11 -Wall -Wextra -Werror -g -O0 main.c -o main

Si tu proyecto es grande, puedes empezar sin -Werror y activarlo cuando el código esté limpio, pero el objetivo es llegar a “cero warnings”.

Warnings típicos y cómo corregirlos

1) Conversiones peligrosas (pérdida de información)

Un warning frecuente aparece al asignar un tipo “grande” a uno “pequeño” o al mezclar signed/unsigned. El compilador te avisa porque puedes truncar valores o cambiar el significado.

#include <stdio.h>int main(void) {    int x = 300;    unsigned char c = x;   /* posible truncamiento */    printf("%u\n", (unsigned)c);    return 0;}

En muchas plataformas, unsigned char solo puede representar 0..255, así que 300 se convierte en 44 (300 mod 256). Mitigación:

  • Usa un tipo que pueda representar el rango real (por ejemplo, int, long, size_t según el caso).
  • Comprueba límites antes de convertir.
  • Convierte de forma explícita solo cuando sea intencional y seguro.
#include <limits.h>#include <stdio.h>int main(void) {    int x = 300;    if (x < 0 || x > UCHAR_MAX) {        printf("Fuera de rango\n");        return 1;    }    unsigned char c = (unsigned char)x;    printf("%u\n", (unsigned)c);    return 0;}

2) Variables sin usar

Una variable sin usar suele indicar código incompleto, lógica que cambió o un error (por ejemplo, calculas algo y luego no lo aplicas).

Continúa en nuestra aplicación.
  • Escuche el audio con la pantalla apagada.
  • Obtenga un certificado al finalizar.
  • ¡Más de 5000 cursos para que explores!
O continúa leyendo más abajo...
Download App

Descargar la aplicación

int main(void) {    int total = 0;    int descuento = 10; /* warning: unused variable */    total += 5;    return 0;}

Soluciones:

  • Elimina la variable si no es necesaria.
  • Úsala realmente (aplicar el descuento).
  • Si es intencional (por ejemplo, parámetro requerido por una interfaz), marca el parámetro como no usado: (void)param;
void f(int param) {    (void)param; /* intencional: evita warning */}

3) Comparaciones sospechosas

Algunas comparaciones son casi siempre un error: comparar con un valor imposible, usar asignación en vez de comparación, o mezclar signed/unsigned.

#include <stdio.h>int main(void) {    unsigned int u = 1;    if (u < 0) { /* siempre falso */        printf("Nunca entra\n");    }    return 0;}

Otro clásico: confundir = con == en una condición.

int x = 5;if (x = 0) { /* asigna 0, condición falsa */}

Mitigación:

  • Revisa tipos: si una variable no puede ser negativa, no compares con negativos.
  • Activa warnings: muchos compiladores avisan de asignaciones en condiciones.
  • Escribe condiciones claras y, si ayuda, separa asignación y comparación.

4) Formatos incorrectos en printf/scanf

Un formato incorrecto puede imprimir basura, leer mal o incluso provocar comportamiento indefinido. El compilador puede detectar muchos casos si incluyes cabeceras correctas y activas warnings.

Error típico: usar %d para un long o pasar un puntero incorrecto a scanf.

#include <stdio.h>int main(void) {    long n = 1234567890L;    printf("%d\n", n); /* formato incorrecto */    return 0;}

Solución: usa el especificador correcto.

printf("%ld\n", n);

Con scanf, el error común es olvidar el operador & (salvo en arrays) o usar un formato que no coincide con el tipo.

#include <stdio.h>int main(void) {    int x;    scanf("%d", x); /* ERROR: falta & */    return 0;}

Correcto:

scanf("%d", &x);

Guía rápida de verificación:

  • Comprueba que el formato coincide con el tipo exacto.
  • En scanf, asegúrate de pasar direcciones válidas.
  • Revisa el valor de retorno de scanf para saber si se leyó lo esperado.

Desbordamiento de enteros: resultados inesperados

El desbordamiento ocurre cuando el resultado de una operación no cabe en el tipo. En C, esto es especialmente delicado:

  • En enteros sin signo (unsigned), el desbordamiento es aritmética modular (se “da la vuelta”).
  • En enteros con signo (signed), el desbordamiento suele ser comportamiento indefinido (no puedes confiar en el resultado).

Ejemplo reproducible: overflow en unsigned

#include <stdio.h>#include <limits.h>int main(void) {    unsigned int u = UINT_MAX;    printf("UINT_MAX=%u\n", u);    u = u + 1;    printf("u+1=%u\n", u);    return 0;}

En la práctica, u+1 suele convertirse en 0.

Ejemplo peligroso: overflow en signed

#include <stdio.h>#include <limits.h>int main(void) {    int x = INT_MAX;    int y = x + 1; /* comportamiento indefinido */    printf("%d\n", y);    return 0;}

Puede parecer que “da la vuelta”, pero el estándar no lo garantiza. Con optimizaciones, el compilador puede asumir que ese overflow nunca ocurre y reordenar lógica de formas sorprendentes.

Mitigación paso a paso

  1. Elige tipos adecuados: si el rango puede crecer, usa un tipo más amplio (por ejemplo, long long), o tipos sin signo cuando el dominio sea no negativo (con cuidado en comparaciones).
  2. Comprueba antes de operar: valida que la operación cabe en el tipo.
  3. Usa límites de <limits.h> para comparar contra máximos/mínimos.

Ejemplo: suma segura en int.

#include <limits.h>#include <stdio.h>int safe_add_int(int a, int b, int *out) {    if ((b > 0 && a > INT_MAX - b) ||        (b < 0 && a < INT_MIN - b)) {        return 0; /* overflow */    }    *out = a + b;    return 1;}int main(void) {    int r;    if (!safe_add_int(INT_MAX, 1, &r)) {        printf("Overflow detectado\n");        return 1;    }    printf("%d\n", r);    return 0;}

Ejemplo: multiplicación segura (patrón común: comprobar antes de multiplicar).

#include <limits.h>int safe_mul_int(int a, int b, int *out) {    if (a == 0 || b == 0) {        *out = 0;        return 1;    }    if (a == -1 && b == INT_MIN) return 0;    if (b == -1 && a == INT_MIN) return 0;    if (a > 0) {        if (b > 0) { if (a > INT_MAX / b) return 0; }        else { if (b < INT_MIN / a) return 0; }    } else {        if (b > 0) { if (a < INT_MIN / b) return 0; }        else { if (a != 0 && b < INT_MAX / a) return 0; }    }    *out = a * b;    return 1;}

Errores comunes que parecen “funcionar” hasta que no

1) Uso de variables sin inicializar

Leer una variable automática (local) sin inicializar es un error: contiene “basura” (valor indeterminado). A veces parece funcionar por casualidad y luego falla al cambiar el compilador, la optimización o el entorno.

#include <stdio.h>int main(void) {    int x;    if (x > 0) { /* x indeterminado */        printf("positivo\n");    }    return 0;}

Mitigación:

  • Inicializa al declarar cuando tenga sentido.
  • Estructura el código para que toda variable tenga un valor asignado antes de leerse.
  • Compila con warnings estrictos: muchos compiladores detectan “may be used uninitialized”.
int x = 0; /* o asignar tras validar entrada */

2) Confusión entre && y & (y entre || y |)

&& y || son operadores lógicos con cortocircuito: si el resultado ya se conoce, no evalúan el resto. & y | son operadores bit a bit: evalúan ambos lados y operan a nivel de bits. Confundirlos puede causar bugs y, peor, ejecutar código que no debería ejecutarse.

#include <stdio.h>int main(void) {    int x = 0;    int y = 0;    if (x != 0 && (10 / x) > 1) {        printf("no llega\n");    }    if (x != 0 & (10 / x) > 1) { /* ERROR: & evalúa ambos lados */        printf("puede dividir por cero\n");    }    return 0;}

Mitigación:

  • Para condiciones, usa && y ||.
  • Reserva & y | para máscaras de bits y operaciones binarias intencionales.
  • Si realmente necesitas evaluar ambos lados, hazlo explícito con variables intermedias para que sea obvio.

3) Condiciones mal parentizadas (precedencia)

La precedencia de operadores puede hacer que una condición se evalúe distinto a lo que imaginas. Un caso típico es mezclar comparaciones con operadores bit a bit o lógicos sin paréntesis claros.

int flags = 2; /* 0b10 */if (flags & 1 == 1) {    /* se interpreta como flags & (1 == 1) => flags & 1 */}

La intención suele ser comprobar el bit 0, pero la expresión es confusa y propensa a errores. Mejor:

if ((flags & 1) == 1) {    /* claro y correcto */}

Otro patrón: combinar && y || sin paréntesis puede ser correcto por precedencia, pero difícil de leer. Si hay más de dos condiciones, añade paréntesis para expresar la intención.

Flujo de depuración básico (paso a paso)

Depurar es un proceso: no es “probar cosas al azar”, sino reducir incertidumbre. Este flujo funciona para la mayoría de errores en programas pequeños y medianos.

Paso 1: Reproducir el problema de forma consistente

  • Anota exactamente qué entrada, argumentos o acciones lo provocan.
  • Si es intermitente, intenta fijar condiciones: misma semilla aleatoria, mismos datos, mismo entorno.
  • Compila con símbolos de depuración: -g y sin optimización: -O0.

Paso 2: Aislar (¿dónde ocurre?)

  • Identifica la función o bloque donde aparece el síntoma.
  • Si el fallo es un resultado incorrecto, localiza el primer punto donde el valor se vuelve incorrecto.
  • Si es un crash, busca la última operación antes del fallo (acceso a memoria, división, índice).

Paso 3: Imprimir valores estratégicos (instrumentación)

Usa impresiones temporales para observar el estado. Hazlo con intención: imprime entradas, salidas y valores intermedios clave.

#include <stdio.h>int main(void) {    int a = 1000000000;    int b = 1000000000;    int c = a + b;    printf("a=%d b=%d c=%d\n", a, b, c);    return 0;}
  • Imprime también tipos y rangos cuando sospeches overflow (por ejemplo, comparando con INT_MAX).
  • Si el problema depende de una rama, imprime qué rama se toma y por qué.

Paso 4: Simplificar el caso (reduce hasta lo mínimo)

  • Elimina partes del programa que no afectan al fallo.
  • Reduce datos de entrada a un ejemplo mínimo que aún falle.
  • Si el bug desaparece al simplificar, reintroduce cambios de uno en uno hasta que reaparezca.

Este paso es especialmente útil con desbordamientos y condiciones mal parentizadas: un caso mínimo suele revelar el operador o el límite exacto que falla.

Paso 5: Formular y verificar hipótesis

En vez de “creo que es X”, conviértelo en una hipótesis comprobable.

  • Hipótesis: “Hay overflow en la suma”. Verificación: compara antes de sumar con INT_MAX - b.
  • Hipótesis: “Se evalúa una división por cero por usar &”. Verificación: cambia a && o imprime si el segundo término se evalúa.
  • Hipótesis: “La variable se usa sin inicializar”. Verificación: inicializa y observa si cambia el comportamiento; luego busca el camino donde se leía antes de asignarse.

Checklist rápido cuando algo no cuadra

SíntomaPreguntas rápidasAcción
Resultado numérico absurdo¿Puede haber overflow? ¿Mezcla signed/unsigned?Revisa límites, tipos y comprobaciones previas
Ramas que “no deberían” ejecutarse¿Hay precedencia confusa? ¿Uso de = en vez de ==?Añade paréntesis, separa expresiones, revisa warnings
Crash intermitente¿Variable sin inicializar? ¿Acceso inválido?Inicializa, imprime trazas, reduce el caso
Lecturas/impresiones raras¿Formato de printf/scanf coincide con el tipo?Corrige especificadores y verifica retornos

Ahora responde el ejercicio sobre el contenido:

Al depurar un posible bug por división por cero en una condición, ¿qué cambio es el más adecuado para evitar que se evalúe el segundo término cuando el primero ya determina el resultado?

¡Tienes razón! Felicitaciones, ahora pasa a la página siguiente.

¡Tú error! Inténtalo de nuevo.

Los operadores lógicos && y || tienen cortocircuito: si el primer término ya define el resultado, el segundo no se evalúa. En cambio, & evalúa ambos lados y puede ejecutar una división por cero.

Siguiente capítulo

Compilación en distintos entornos y configuración mínima de proyectos en C

Arrow Right Icon
Portada de libro electrónico gratuitaFundamentos de programación en C desde cero: variables, control de flujo y funciones
91%

Fundamentos de programación en C desde cero: variables, control de flujo y funciones

Nuevo curso

11 páginas

Descarga la aplicación para obtener una certificación gratuita y escuchar cursos en segundo plano, incluso con la pantalla apagada.