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

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 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 <stdio.h>
#include <stdint.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
#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
6
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 Introducción.

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.2bit 00.1 \times 2 = 0.2 \rightarrow \text{bit } 0 (resto 0.2)

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

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

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

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

  6. 0.2×2=0.4bit 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.0001100110011001100112=0.0001120.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
9
#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;
}

Ejercicios de Autoevaluación

Solution to Exercise 1

El tipo uint16_t es un entero sin signo de 16 bits, lo que limita su valor máximo almacenable a 2161=655352^{16} - 1 = 65535.

El estándar C garantiza que las operaciones aritméticas sobre tipos enteros sin signo operan bajo aritmética modular. Al sumar 2, el valor realiza un desbordamiento cíclico (“da la vuelta”):

x=(65535+2)mod65536=1\text{x} = (65535 + 2) \bmod 65536 = 1

Por lo tanto, la variable x almacenará el valor 1 de manera completamente segura y predecible.

Solution to Exercise 2

El fragmento de código imprimirá Promedio: 3.00.

Esto ocurre porque total_puntos y partidas son variables de tipo int. Al aplicar el operador /, C realiza primero una división entera, truncando el resultado real de 3.75 a 3. Luego, este valor entero 3 se convierte implícitamente a flotante (3.0f) durante la asignación a la variable promedio.

Para solucionar esto y conservar la precisión decimal, se debe realizar una conversión explícita o cast (ver regla de casts) de al menos uno de los operandos a float antes de operar:

1
float promedio = (float)total_puntos / partidas; // Produce 3.75f
Solution to Exercise 3

La comparación evalúa como falsa debido a la imprecisión en la representación binaria bajo el estándar IEEE 754. Ni 0.1 ni 0.2 tienen representaciones binarias exactas finitas, lo que introduce un pequeño error de redondeo en la mantisa. Al sumarse, los errores se acumulan haciendo que el valor almacenado difiera ligeramente de 0.3f0.3f.

La forma correcta de realizar esta comparación es evaluando la diferencia absoluta contra un épsilon:

1
2
3
4
5
6
7
8
9
10
#include <math.h>
#include <stdio.h>

#define EPSILON 0.00001f

void comparar(float a, float b) {
    if (fabsf(a - b) < EPSILON) {
        printf("Son equivalentes dentro del margen de error.\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