Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Representación de Números y Aritmética

Tipos de datos numéricos en C, límites físicos, desbordamientos e imprecisión de reales

Universidad Nacional de Río Negro

Prerrequisitos: operadores aritméticos, tipos básicos y compilación con warnings. La representación binaria se introduce en este capítulo.

Objetivos: 1. Consultar los límites de un tipo con la biblioteca estándar. 2. Distinguir desbordamiento entero de imprecisión de punto flotante.

Comprobación de salida: predecí si una conversión pierde información y justificá el resultado con el rango de los tipos.

Representación de Números y Aritmética

Introducción

En el lenguaje C, a diferencia de la matemática pura donde los conjuntos numéricos son infinitos, los números se representan físicamente utilizando una cantidad finita de bits en la memoria RAM del sistema. Esto significa que cada tipo de dato numérico posee un rango de representación estrictamente limitado por el hardware del procesador.

El estándar C clasifica los tipos numéricos en dos grandes familias: enteros (con y sin signo) y números reales (o de punto flotante).


Desarrollo

Números Enteros y Portabilidad

El estándar C define varios tipos enteros básicos: char (usualmente 8 bits), short (mínimo 16 bits), int (mínimo 16 bits, usualmente 32 bits en plataformas modernas) y long (mínimo 32 bits).

Tipos de Ancho Fijo (<stdint.h>)

Para solucionar este problema de portabilidad y evitar comportamientos erráticos al compilar en diferentes procesadores, el estándar C99 introdujo la biblioteca <stdint.h>. Esta define tipos de datos con tamaños fijos y explícitos:

Table 1:Tipos de datos de ancho fijo de stdint.h

Tipo con SignoTipo sin SignoTamaño en BitsRango Aproximado
int8_tuint8_t8 bits±127\pm 127 / 0 a 255
int16_tuint16_t16 bits±32.767\pm 32.767 / 0 a 65.535
int32_tuint32_t32 bits±2.147.483.647\pm 2.147.483.647 / 0 a 4.294.967.295
int64_tuint64_t64 bits±9×1018\pm 9 \times 10^{18}

Como regla general de la cátedra (ver regla 0x3010h: Las variables que representan tamaños o índices de arreglos deben ser de tipo size_t), debés utilizar siempre size_t para almacenar tamaños de memoria, índices de arreglos o conteos de elementos, ya que es el tipo sin signo garantizado por el sistema para representar el tamaño de cualquier objeto en memoria.

Desbordamientos (Overflow y Underflow)

El desbordamiento ocurre cuando el resultado de una operación aritmética produce un valor numérico que supera los límites físicos que el tipo de dato asignado puede representar.

Enteros Sin Signo (unsigned): Comportamiento Cíclico

El estándar C especifica de forma estricta que los enteros sin signo operan bajo aritmética modular (módulo 2w2^w, donde ww es el ancho en bits). Si se supera el límite superior, el valor “da la vuelta” y comienza cíclicamente desde cero de manera predecible.

1
2
3
4
5
6
7
8
#include <stdint.h>
#include <stdio.h>
void demo_overflow_unsigned(void)
{
    uint8_t numero = 255; // Valor máximo almacenable en 8 bits
    numero = numero + 1;  // Produce 0 de forma garantizada por el estándar C
    printf("Valor: %u\n", numero);
}
Enteros Con Signo: Comportamiento Indefinido (UB)

A diferencia de los tipos sin signo, el desbordamiento de un entero con signo (ej: int o int32_t) constituye un Comportamiento Indefinido (Undefined Behavior o UB) según el estándar ISO de C.

1
2
3
4
5
6
7
#include <stdint.h>
void demo_overflow_signed(void)
{
    int8_t numero = 127; // Valor máximo en 8 bits con signo
    numero = numero + 1; // ¡COMPORTAMIENTO INDEFINIDO! No asumas que dará
    -128.
}

División de Enteros vs. División Real

En C, los operadores aritméticos cambian su semántica según el tipo de datos de sus operandos. Esto es especialmente crítico en la división (/):

1
2
3
4
5
int a = 5;
int b = 2;
int resultado_entero = a / b; // Produce 2 (se descarta la parte fraccional)
float resultado_real = (float)a / b; // Produce 2.5 (conversión explícita o
cast)

Para mayor detalle de cómo el compilador convierte implícitamente estos tipos durante operaciones aritméticas, consultá el capítulo de Blockquote.

Imprecisión de Reales (IEEE 754)

Los procesadores modernos representan los números reales en binario siguiendo el estándar IEEE 754. Dado que no es posible representar un número infinito de valores reales utilizando una cantidad finita de bits (como los 32 bits de un float), la gran mayoría de los números fraccionarios experimentan un redondeo o truncamiento físico en memoria.

Demostración de la Periodicidad Binaria de 0.1

El número decimal 0.110 parece simple, pero en base binaria es un número periódico infinito. Para convertir la parte fraccionaria, multiplicamos sucesivamente por 2 y tomamos la parte entera resultante como el bit correspondiente:

  1. 0.1×2=0.2→bit 00.1 \times 2 = 0.2 \rightarrow \text{bit } 0 (resto 0.2)

  2. 0.2×2=0.4→bit 00.2 \times 2 = 0.4 \rightarrow \text{bit } 0 (resto 0.4)

  3. 0.4×2=0.8→bit 00.4 \times 2 = 0.8 \rightarrow \text{bit } 0 (resto 0.8)

  4. 0.8×2=1.6→bit 10.8 \times 2 = 1.6 \rightarrow \text{bit } 1 (resto 0.6)

  5. 0.6×2=1.2→bit 10.6 \times 2 = 1.2 \rightarrow \text{bit } 1 (resto 0.2)

  6. 0.2×2=0.4→bit 00.2 \times 2 = 0.4 \rightarrow \text{bit } 0 (se repite la secuencia de restos...)

Como el resto vuelve a ser 0.2 en el paso 6, el patrón se repite infinitamente:

0.110=0.000110011001100110011…2=0.00011‾20.1_{10} = 0.000110011001100110011\dots_2 = 0.0\overline{0011}_2

Dado que un float reserva únicamente 23 bits para la mantisa, esta secuencia se corta físicamente en memoria, almacenando la aproximación:

0.1000000014901161193847656250.100000001490116119384765625
Comparación Segura mediante Épsilon

Debido a esta imprecisión acumulada, nunca debés comparar dos números reales utilizando el operador de igualdad directa ==. En su lugar, debés verificar que la diferencia absoluta entre ambos sea menor que un margen de tolerancia sumamente pequeño denominado épsilon (ϵ\epsilon):

1
2
3
4
5
6
7
8
#include <math.h>
#include <stdbool.h>
#define EPSILON 0.00001f
bool reales_casi_iguales(float a, float b)
{
    // Retorna verdadero si la diferencia absoluta es menor al margen de error
    return fabsf(a - b) < EPSILON;
}

Un epsilon fijo como 0.00001f no sirve para cualquier escala: hay que elegirlo según la magnitud de los valores comparados y el error acumulado del problema, no como constante universal. Una comparación más robusta combina tolerancia absoluta y relativa:

#include <math.h>
#include <stdbool.h>

bool cerca(double a, double b, double abs_tol, double rel_tol)
{
    double d = fabs(a - b);
    return d <= abs_tol || d <= rel_tol * fmax(fabs(a), fabs(b));
}
NaN e infinitos

IEEE 754 reserva patrones de bits especiales para representar resultados que no son un número real: NaN (Not a Number, resultado de operaciones como 0.0 / 0.0) e infinitos (1.0 / 0.0). NaN tiene una propiedad inusual: no es igual a sí mismo. Comparar x == NAN es siempre falso, incluso si x es NaN; hay que usar isnan(x) para detectarlo.

#include <math.h>

double a = 0.0 / 0.0;   /* NaN */
double b = 1.0 / 0.0;   /* +Inf */

isnan(a);  /* verdadero */
isinf(b);  /* verdadero */
a == a;    /* falso: NaN nunca es igual a sí mismo */

No serialices la representación binaria de un double (por ejemplo, escribiendo sus bytes crudos a un archivo) como si fuera un formato portable: el layout exacto de signo, exponente y mantisa es una propiedad de la plataforma, no una garantía del lenguaje.

Mini-ejercicio

Usá isnan para detectar el resultado de 0.0 / 0.0 y isinf para el de 1.0 / 0.0. ¿Qué devuelve comparar NAN == NAN con ==? Escribí la forma correcta de verificar si un double es NaN.


Ejercicios de Autoevaluación


Glosario

IEEE 754
Estándar técnico para la representación por hardware de números de punto flotante.
Épsilon (epsilon)
El menor margen de tolerancia o valor de error aceptado en la comparación de números reales debido a imprecisiones de almacenamiento.
Desbordamiento (Overflow)
Fenómeno en el cual un cálculo matemático excede los límites superiores representables por el tipo de dato físico.
Comportamiento Indefinido (Undefined Behavior)
Situación descrita en el estándar de un lenguaje de programación donde no se especifica ninguna restricción sobre lo que el compilador y el entorno pueden hacer.

Síntesis y Resumen

En este capítulo estudiaste cómo se representan numéricamente los datos en hardware y sus consecuencias:


Referencias y Lecturas Complementarias