HOWTO · C
Diferencia entre Unsigned Int y Signed Int en C
El siguiente artículo explorará las distinciones entre int firmado y sin firmar en el lenguaje de programación C.
En esta página
Un signed int puede representar valores negativos y positivos, mientras que un unsigned int representa valores no negativos. En C, int tiene signo de forma predeterminada: se escribe int para un entero con signo y unsigned int cuando se necesita uno sin signo. El tipo adecuado depende de los valores y las operaciones aritméticas que requiera el programa. La diferencia parece pequeña —un tipo admite el signo menos y el otro no—, pero también cambia el rango de valores, lo que ocurre en los límites de ese rango y cómo se comportan las expresiones cuando se combinan ambos tipos.
Diferencias entre signed y unsigned int en C
Ambos son tipos enteros, pero sus rangos son distintos. El estándar de C garantiza que int puede representar como mínimo los valores de -32767 a 32767, y que unsigned int puede representar como mínimo los valores de 0 a 65535. Estas son garantías mínimas, no los límites exactos de todos los sistemas. Incluye limits.h y consulta INT_MIN, INT_MAX y UINT_MAX para conocer los límites de la implementación con la que compilas.
| Tipo | Representa | Rango mínimo portable |
|---|---|---|
int |
Valores negativos y no negativos | De -32767 a 32767 |
unsigned int |
Valores no negativos | De 0 a 65535 |
Normalmente, ambos tipos ocupan el mismo tamaño de almacenamiento, por lo que elegir entre ellos no consigue más espacio en total; decide a qué se destina ese espacio. Un tipo con signo reparte sus valores entre números negativos y positivos, mientras que un tipo sin signo dedica todo su rango a valores no negativos, lo que hace que su máximo sea aproximadamente el doble en la misma implementación. En el sistema Linux de 64 bits usado para los ejemplos de este artículo, ambos tipos ocupan cuatro bytes, e imprimir los límites da los familiares rangos de 32 bits:
#include <limits.h>
#include <stdio.h>
int main(void) {
printf("sizeof(int) = %zu\n", sizeof(int));
printf("sizeof(unsigned int) = %zu\n", sizeof(unsigned int));
printf("int range: %d to %d\n", INT_MIN, INT_MAX);
printf("unsigned int range: 0 to %u\n", UINT_MAX);
return 0;
}
sizeof(int) = 4
sizeof(unsigned int) = 4
int range: -2147483648 to 2147483647
unsigned int range: 0 to 4294967295
Trata esos números exactos como propiedades de esta implementación, no del lenguaje. El código que debe funcionar en cualquier plataforma debería comparar con INT_MIN, INT_MAX y UINT_MAX en lugar de fijar en el código los límites de 32 bits.
Rangos de valores y conversiones
El rango real y el tamaño de almacenamiento dependen de la implementación. sizeof(int) y sizeof(unsigned int) no tienen por qué ser de cuatro bytes, y C no garantiza que int tenga 32 bits. No es portable describir un entero con signo como si reservara su bit más a la izquierda para el signo; los detalles de representación dependen de la implementación.
Cuando un valor negativo se convierte a un tipo sin signo, el resultado es congruente con ese valor módulo UINT_MAX + 1. Por tanto, el objeto sin signo contiene un valor no negativo; no conserva un valor negativo que pueda recuperarse al leerlo más tarde. La aritmética sin signo también da la vuelta módulo UINT_MAX + 1. El siguiente ejemplo aplica esa regla de tres maneras: convierte -1, resta por debajo de cero y suma más allá del máximo:
#include <limits.h>
#include <stdio.h>
int main(void) {
unsigned int converted = -1;
unsigned int wrapped_down = 0u - 1u;
unsigned int wrapped_up = UINT_MAX + 1u;
printf("converted -1: %u\n", converted);
printf("0u - 1u: %u\n", wrapped_down);
printf("UINT_MAX + 1u: %u\n", wrapped_up);
return 0;
}
converted -1: 4294967295
0u - 1u: 4294967295
UINT_MAX + 1u: 0
En esta implementación, -1 se convierte en 4294967295, que es exactamente UINT_MAX; en cualquier implementación el resultado es el propio UINT_MAX, porque -1 siempre está un paso por debajo de cero en la aritmética modular. Restar uno a cero da la vuelta hasta ese mismo máximo en lugar de producir -1, y sumar uno al máximo vuelve a cero. Ninguna de estas operaciones es un error: dar la vuelta es el comportamiento definido de la aritmética sin signo.
Este ejemplo completo usa %d para int y %u para unsigned int. Se ejecutó correctamente; la salida que aparece a continuación corresponde a esa ejecución.
#include <stdio.h>
int main(void) {
int temperature_change = -7;
unsigned int item_count = 42;
printf("Temperature change: %d\n", temperature_change);
printf("Item count: %u\n", item_count);
return 0;
}
Temperature change: -7
Item count: 42
Aplica la regla de conversión anterior para razonar sobre una asignación de un valor negativo a un tipo sin signo; el valor resultante exacto depende de UINT_MAX. Pasar un unsigned int a printf con %d en lugar de %u es una discrepancia de formato y provoca comportamiento indefinido, por lo que no se puede confiar en el resultado impreso.
Desbordamiento: definido para unsigned, indefinido para signed
La vuelta anterior no tiene equivalente con signo. El desbordamiento de enteros con signo —calcular un resultado por encima de INT_MAX o por debajo de INT_MIN— es comportamiento indefinido en C, no una vuelta garantizada. Los compiladores optimizan suponiendo que nunca ocurre, así que una comprobación como verificar si x + 1 es menor que x no sirve para detectar de forma fiable un desbordamiento con signo a posteriori. Si un cálculo puede acercarse a los límites, comprueba antes de calcular, por ejemplo probando x > INT_MAX - y antes de sumar y a x, o usa un tipo más amplio. Esta asimetría es una razón práctica para preferir los tipos con signo en la aritmética ordinaria: los errores con valores sin signo dan la vuelta en silencio hasta un número incorrecto pero de aspecto válido, mientras que el código con signo mantiene los valores esperados hasta que realmente se supera un límite.
Combinar signed y unsigned en una misma expresión
Las diferencias más sorprendentes aparecen cuando ambos tipos se encuentran en una misma expresión. Según las conversiones aritméticas habituales (usual arithmetic conversions), si se combinan un int y un unsigned int del mismo rango, el operando int se convierte a unsigned int antes de la operación. Por tanto, la comparación o la operación se realiza sin signo, aunque un lado empezara siendo negativo:
#include <stdio.h>
int main(void) {
printf("-1 < 1: %d\n", -1 < 1);
printf("-1 < 1u: %d\n", -1 < 1u);
printf("-1 == 4294967295u: %d\n", -1 == 4294967295u);
return 0;
}
-1 < 1: 1
-1 < 1u: 0
-1 == 4294967295u: 1
Leído como matemática, -1 < 1 es verdadero e imprime 1. Añadir el sufijo u cambia la respuesta a 0: -1 se convierte primero en 4294967295, y ese valor no es menor que 1. Por la misma razón, -1 resulta igual al mayor valor sin signo. GCC advierte de exactamente este patrón con -Wsign-compare, que merece la pena activar de forma permanente. La aritmética sigue la misma regla, de modo que un ajuste negativo puede convertirse en un enorme resultado positivo en lugar de uno negativo pequeño:
#include <stdio.h>
int main(void) {
unsigned int count = 10u;
int delta = -20;
int signed_total = 10 + delta;
printf("unsigned total: %u\n", count + delta);
printf("signed total: %d\n", signed_total);
return 0;
}
unsigned total: 4294967286
signed total: -10
Aquí no se desbordó nada en el tipo sin signo; la conversión por sí sola produjo 4294967286. Siempre que un valor pueda volverse negativo —una diferencia entre dos conteos, un saldo, un centinela de error como -1— mantenlo en un tipo con signo hasta el punto en que se garantice un valor no negativo.
La trampa de sizeof y los contadores de bucle sin signo
La mezcla también ocurre donde no se ve ninguna conversión ni sufijo. El operador sizeof produce un size_t, un tipo sin signo, así que comparar una variable con signo con un resultado de sizeof provoca la misma conversión:
#include <stdio.h>
int main(void) {
int index = -1;
printf("sizeof(int): %zu\n", sizeof(int));
printf("index < (int)sizeof(int): %d\n", index < (int)sizeof(int));
printf("index < sizeof(int): %d\n", index < sizeof(int));
return 0;
}
sizeof(int): 4
index < (int)sizeof(int): 1
index < sizeof(int): 0
La última comparación debería ser verdadera —-1 es realmente menor que 4—, pero la comparación sin signo dice lo contrario. Convertir el resultado de sizeof a int, cuando se sabe que su valor cabe, restablece la respuesta esperada. Una trampa relacionada es el bucle descendente escrito como for (unsigned int i = count - 1; i >= 0; --i): un valor sin signo siempre es mayor o igual que cero, así que la condición nunca es falsa. Cuando el contador llega a cero y se decrementa, da la vuelta hasta UINT_MAX —la misma vuelta que el ejemplo de conversión imprimió para 0u - 1u— y el bucle continúa con un índice enorme. Cuenta hacia abajo en un tipo con signo, o usa la forma idiomática for (unsigned int i = count; i-- > 0; ), que comprueba el valor antiguo antes de decrementar.
Cómo elegir entre signed y unsigned int
Elige int cuando los valores negativos tengan sentido, por ejemplo, para un cambio que pueda ser mayor o menor que cero. Elige unsigned int cuando el dominio no admita valores negativos y sea apropiada la aritmética modular, como en un contador que, al alcanzar el valor máximo del tipo, vuelva a cero bajo aritmética módulo UINT_MAX + 1. unsigned no es una forma general de almacenar de manera segura valores positivos más grandes: su rango depende de la implementación, y convertir o combinar valores con y sin signo puede cambiar los resultados de comparaciones y operaciones aritméticas. Usa el tipo cuyo rango y comportamiento aritmético se ajusten a los valores que realmente maneja tu programa.
En la práctica, esto lleva a unos valores predeterminados fiables. Usa un tipo con signo para la aritmética general, las diferencias y los contadores de bucle, incluso para cantidades que empiezan siendo no negativas, porque los resultados intermedios a menudo necesitan bajar de cero. Reserva los tipos sin signo para patrones de bits y máscaras, para valores cuya vuelta es deliberada, como valores hash y registros de hardware, y para ajustarte a una interfaz que ya usa un tipo sin signo o size_t. No elijas unsigned solo para documentar que un valor nunca debería ser negativo: eso no impide que el valor se produzca mediante una resta, solo lo renombra como un número positivo muy grande. Por último, si el problema real es el rango, pasa a un tipo más amplio como long long en lugar de confiar en el espacio extra que un tipo sin signo parece ofrecer.
Tres ideas equivocadas causan la mayor parte de la confusión en torno a estos tipos. La primera es que unsigned int es simplemente int con un rango positivo mayor; en realidad, los dos tipos difieren en el comportamiento ante el desbordamiento, en las conversiones y en las comparaciones, no solo en el rango. La segunda es que almacenar un valor negativo en una variable sin signo recuerda de algún modo el valor negativo; la conversión lo reemplaza de inmediato, como muestran los ejemplos anteriores. La tercera es que la aritmética sin signo es más segura porque no puede volverse negativa; sustituye los resultados negativos por positivos grandes, en silencio, lo que a menudo es más difícil de depurar que el problema que pretendía evitar.