tipos tipo programacion long informatica float enteros ejemplos datos dato c++ unsigned-integer

c++ - tipo - ¿Por qué los enteros sin signo son propensos a errores?



tipos de datos enteros en programacion (8)

Además del problema de rango / deformación con tipos sin signo. El uso de una combinación de tipos enteros sin signo y con signo impacta un problema de rendimiento significativo para el procesador. Menos del reparto de coma flotante, pero mucho para ignorar eso. Además, el compilador puede realizar una verificación de rango para el valor y cambiar el comportamiento de otras verificaciones.

Estaba mirando este video . Bjarne Stroustrup dice que las entradas sin firmar son propensas a errores y provocan errores. Por lo tanto, solo debe usarlos cuando realmente los necesite. También leí en una de las preguntas sobre Desbordamiento de pila (pero no recuerdo cuál) que usar entradas sin firmar puede generar errores de seguridad.

¿Cómo conducen a errores de seguridad? ¿Alguien puede explicarlo claramente dando un ejemplo adecuado?


Aunque solo puede considerarse como una variante de las respuestas existentes: Refiriéndose a "Tipos firmados y sin firmar en las interfaces", Informe C ++, septiembre de 1995 por Scott Meyers, es particularmente importante evitar los tipos sin firmar en las interfaces .

El problema es que se hace imposible detectar ciertos errores que los clientes de la interfaz podrían cometer (y si pudieran cometerlos, los cometerán).

El ejemplo dado allí es:

template <class T> class Array { public: Array(unsigned int size); ...

y una posible instanciación de esta clase

int f(); // f and g are functions that return int g(); // ints; what they do is unimportant Array<double> a(f()-g()); // array size is f()-g()

La diferencia de los valores devueltos por f() g() podría ser negativa, por una cantidad horrible de razones. El constructor de la clase Array recibirá esta diferencia como un valor que se convierte implícitamente en unsigned . Por lo tanto, como implementador de la clase Array , no se puede distinguir entre un valor pasado erróneamente de -1 y una asignación de matriz muy grande.


El gran problema con unsigned int es que si resta 1 de un unsigned int 0, el resultado no es un número negativo, el resultado no es menor que el número con el que comenzó, pero el resultado es el valor int sin signo más grande posible .

unsigned int x = 0; unsigned int y = x - 1; if (y > x) printf ("What a surprise! /n");

Y esto es lo que hace que el error int sin signo sea propenso. Por supuesto, unsigned int funciona exactamente como está diseñado para funcionar. Es absolutamente seguro si sabe lo que está haciendo y no comete errores. Pero la mayoría de la gente comete errores.

Si está utilizando un buen compilador, activa todas las advertencias que produce el compilador y le indicará cuándo hace cosas peligrosas que probablemente sean errores.


El problema con los tipos enteros sin signo es que, dependiendo de su tamaño, pueden representar una de dos cosas diferentes:

  1. Los tipos sin uint8 más pequeños que int (por ejemplo, uint8 ) contienen números en el rango 0..2ⁿ-1, y los cálculos con ellos se comportarán de acuerdo con las reglas de la aritmética de enteros siempre que no excedan el rango del tipo int . Según las reglas actuales, si dicho cálculo excede el rango de un int , un compilador puede hacer lo que quiera con el código, incluso llegando a negar las leyes del tiempo y la causalidad (¡algunos compiladores harán precisamente eso!) , e incluso si el resultado del cálculo se asignaría de nuevo a un tipo sin signo menor que int .
  2. Tipos unsigned int sin unsigned int y miembros de retención más grandes del anillo algebraico de envoltura abstracta de números enteros congruentes mod 2ⁿ; esto significa efectivamente que si un cálculo sale del rango 0..2ⁿ-1, el sistema sumará o restará cualquier múltiplo de 2ⁿ que se necesitaría para volver a poner el valor en el rango.

En consecuencia, dado uint32_t x=1, y=2; la expresión xy puede tener uno de dos significados dependiendo de si int es mayor que 32 bits.

  1. Si int es mayor que 32 bits, la expresión restará el número 2 del número 1, produciendo el número -1. Tenga en cuenta que, si bien una variable de tipo uint32_t no puede contener el valor -1 independientemente del tamaño de int , y el almacenamiento de -1 provocaría que dicha variable contenga 0xFFFFFFFF, pero a menos o hasta que el valor se coaccione a un tipo sin signo, se comportará como la cantidad firmada -1.
  2. Si int tiene 32 bits o menos, la expresión producirá un valor uint32_t que, cuando se agrega al valor uint32_t 2, generará el valor uint32_t 1 (es decir, el valor uint32_t 0xFFFFFFFF).

En mi humilde opinión, este problema podría resolverse limpiamente si C y C ++ definieran nuevos tipos sin signo [por ejemplo, unum32_t y uwrap32_t] de modo que unum32_t siempre se comportara como un número, independientemente del tamaño de int (posiblemente requiriendo la operación con la mano derecha de una resta o unario menos se promocionará al siguiente tipo con signo más grande si int es 32 bits o menor), mientras que un wrap32_t siempre se comportaría como un miembro de un anillo algebraico (bloqueando promociones incluso si int fuera mayor que 32 bits). Sin embargo, en ausencia de tales tipos, a menudo es imposible escribir código que sea portátil y limpio, ya que el código portátil a menudo requerirá coacciones de tipo en todo el lugar.


Las reglas de conversión numérica en C y C ++ son un desastre bizantino. El uso de tipos sin signo se expone a ese desorden en mayor medida que el uso de tipos con signo puro.

Tomemos, por ejemplo, el caso simple de una comparación entre dos variables, una con signo y la otra sin signo.

  • Si ambos operandos son más pequeños que int, ambos se convertirán a int y la comparación dará resultados numéricamente correctos.
  • Si el operando sin signo es más pequeño que el operando con signo, ambos se convertirán al tipo del operando con signo y la comparación dará resultados numéricamente correctos.
  • Si el operando sin signo tiene un tamaño mayor o igual que el operando con signo y también tiene un tamaño mayor o igual que int, entonces ambos se convertirán al tipo del operando sin signo. Si el valor del operando firmado es menor que cero, esto conducirá a resultados numéricamente incorrectos.

Para tomar otro ejemplo, considere multiplicar dos enteros sin signo del mismo tamaño.

  • Si el tamaño del operando es mayor o igual que el tamaño de int, la multiplicación tendrá una semántica envolvente definida.
  • Si el tamaño del operando es menor que int pero mayor o igual que la mitad del tamaño de int, existe la posibilidad de un comportamiento indefinido.
  • Si el tamaño del operando es inferior a la mitad del tamaño de int, la multiplicación producirá resultados numéricamente correctos. Asignar este resultado nuevamente a una variable del tipo original sin signo producirá una semántica envolvente definida.

No voy a ver un video solo para responder una pregunta, pero un problema son las conversiones confusas que pueden ocurrir si combina valores con y sin signo. Por ejemplo:

#include <iostream> int main() { unsigned n = 42; int i = -42; if (i < n) { std::cout << "All is well/n"; } else { std::cout << "ARITHMETIC IS BROKEN!/n"; } }

Las reglas de promoción significan que i se convierte en unsigned para la comparación, dando un gran número positivo y un resultado sorprendente.


Un factor importante es que hace que la lógica del bucle sea más difícil: imagina que quieres iterar sobre todo menos el último elemento de una matriz (lo que sucede en el mundo real). Entonces escribes tu función:

void fun (const std::vector<int> &vec) { for (std::size_t i = 0; i < vec.size() - 1; ++i) do_something(vec[i]); }

Se ve bien, ¿no? ¡Incluso se compila limpiamente con niveles de advertencia muy altos! ( Live ) Así que pones esto en tu código, todas las pruebas se ejecutan sin problemas y lo olvidas.

Ahora, más tarde, alguien aparece y pasa un vector vacío a su función. Ahora, con un número entero firmado, es de esperar que haya notado la advertencia del compilador de comparación de signos , introducido el elenco apropiado y no haya publicado el código con errores en primer lugar.

Pero en su implementación con el entero sin signo, se ajusta y la condición del bucle se convierte en i < SIZE_T_MAX . ¡Desastre, UB y muy probablemente choque!

¿Quiero saber cómo conducen a errores de seguridad?

Esto también es un problema de seguridad, en particular es un desbordamiento de búfer . Una forma de explotar esto sería si do_something hiciera algo que el atacante pueda observar. Es posible que puedan encontrar qué entrada entró en do_something , y de esa manera los datos que el atacante no debería poder acceder se filtrarán de su memoria. Este sería un escenario similar al error Heartbleed . (Gracias a Ratchet Freak por señalar eso en un comment ).


Un posible aspecto es que los enteros sin signo pueden provocar problemas algo difíciles de detectar en los bucles, porque el flujo inferior conduce a grandes números. No puedo contar (¡incluso con un entero sin signo!) Cuántas veces hice una variante de este error

for(size_t i = foo.size(); i >= 0; --i) ...

Tenga en cuenta que, por definición, i >= 0 siempre es cierto. (Lo que causa esto en primer lugar es que si i firmado, el compilador advertirá sobre un posible desbordamiento con el size_t de size() ).

Hay otras razones mencionadas Peligro: ¡tipos sin signo utilizados aquí! , el más fuerte de los cuales, en mi opinión, es la conversión de tipo implícita entre firmado y no firmado.