leer - ¿Por qué se promueven los tipos enteros durante la suma en C?
scanf en c (4)
Así que tuvimos un problema de campo, y después de días de depuración, redujimos el problema a este bit particular de código, donde el procesamiento en un bucle while no estaba sucediendo:
// heavily redacted code
// numberA and numberB are both of uint16_t
// Important stuff happens in that while loop
while ( numberA + 1 == numberB )
{
// some processing
}
Esto funcionó bien, hasta que alcanzamos el límite de uint16 de 65535. Otro grupo de declaraciones impresas más tarde, descubrimos que el numberA + 1
tenía un valor de 65536
, mientras que el número numberB
regresaba a 0
. Esto falló la comprobación y no se realizó ningún procesamiento.
Esto me dio curiosidad, así que armé un programa rápido de C (compilado con GCC 4.9.2) para verificar esto:
#include <stdio.h>
#include <stdint.h>
int main()
{
uint16_t numberA, numberB;
numberA = 65535;
numberB = numberA + 1;
uint32_t numberC, numberD;
numberC = 4294967295;
numberD = numberC + 1;
printf("numberA = %d/n", numberA + 1);
printf("numberB = %d/n", numberB);
printf("numberC = %d/n", numberC + 1);
printf("numberD = %d/n", numberD);
return 0;
}
Y el resultado fue:
numberA = 65536
numberB = 0
numberC = 0
numberD = 0
Así que parece que el resultado de numberA + 1
se promovió a uint32_t. ¿Está previsto esto por el lenguaje C? ¿O es esto alguna rareza de compilador / hardware?
Así que parece que el resultado de
numberA + 1
se promovió auint32_t
Los operandos de la adición se promovieron a int
antes de que la adición tuviera lugar, y el resultado de la adición es del mismo tipo que los operandos efectivos ( int
).
De hecho, si int
es de 32 bits de ancho en su plataforma de compilación (lo que significa que el tipo que representa uint16_t
tiene un "rango de conversión" más bajo que int
), el numberA + 1
se calcula como una adición int
entre 1
y un número promovido numberA
como parte de Las reglas de promoción de enteros, 6.3.1.1:2 en el estándar C11:
Se puede usar lo siguiente en una expresión dondequiera que se pueda usar un int o un unsigned int: [...] Un objeto o expresión con un tipo entero (distinto de int o unsigned int) cuyo rango de conversión de entero es menor o igual que el rango de int y unsigned int.
[…]
Si un int puede representar todos los valores del tipo original [...], el valor se convierte en un int
En su caso, el unsigned short
que es, con toda probabilidad, lo que uint16_t
se define como en su plataforma, tiene todos sus valores representables como elementos de int
, por lo que el número A del valor unsigned short
se promueve a int
cuando ocurre en una operación aritmética.
Cuando se estaba desarrollando el lenguaje C, era deseable minimizar el número de tipos de compiladores aritméticos con los que tenía que lidiar. Por lo tanto, la mayoría de los operadores matemáticos (por ejemplo, la suma) solo admiten int + int, long + long y double + double. Si bien el lenguaje podría haberse simplificado omitiendo int + int (promoviendo todo a long
lugar), la aritmética en valores long
generalmente toma de 2 a 4 veces más código que la aritmética en valores int
; ya que la mayoría de los programas están dominados por la aritmética en los tipos int
, eso habría sido muy costoso. Por el contrario, la promoción de float
para double
ahorrará código, ya que significa que solo se necesitan dos funciones para admitir float
: convertir al double
y convertir del double
. Todas las demás operaciones aritméticas de punto flotante solo necesitan ser compatibles con un tipo de punto flotante, y dado que las matemáticas de punto flotante se realizan a menudo llamando a las rutinas de la biblioteca, el costo de llamar a una rutina para agregar dos valores double
es a menudo el mismo que el costo de llamar a una rutina Rutina para sumar dos valores float
.
Desafortunadamente, el lenguaje C se extendió en una variedad de plataformas antes de que alguien realmente descubriera lo que debería significar 0xFFFF + 1, y en ese momento ya había algunos compiladores donde la expresión daba 65536 y algunos donde daba cero. En consecuencia, los escritores de estándares se han esforzado por escribirlos de una manera que les permita a los compiladores seguir haciendo lo que estaban haciendo, pero que fue bastante inútil desde el punto de vista de cualquiera que desee escribir código portátil. Por lo tanto, en plataformas donde int
es de 32 bits, 0xFFFF + 1 producirá 65536, y en plataformas donde int
es de 16 bits, producirá cero. Si en alguna plataforma se int
de 17 bits, 0xFFFF + 1 autorizaría al compilador a negar las leyes del tiempo y la causalidad [por cierto, no sé si hay plataformas de 17 bits, pero hay algunas plataformas de 32 bits donde uint16_t x=0xFFFF; uint16_t y=x*x;
uint16_t x=0xFFFF; uint16_t y=x*x;
hará que el compilador confunda el comportamiento del código que lo precede ].
Literal 1
en int
, es decir, en su caso int32 tipo, por lo que las operaciones con int32 y int16 dan resultados de int32.
Para obtener el resultado de la numberA + 1
como uint16_t
intente el tipo explícito de uint16_t
para 1
, por ejemplo: numberA + (uint16_t)1
Para operadores aritméticos como +
, se aplican las conversiones aritméticas habituales .
Para los enteros, el primer paso de esas conversiones se llama las promociones de enteros , y esto promueve que cualquier valor de tipo más pequeño que int
sea un int
.
Los otros pasos no se aplican a su ejemplo, así que los omitiré por concisión.
En la expresión numberA + 1
, se aplican las promociones de enteros. 1
ya es un int
por lo que permanece sin cambios. numberA
tiene el tipo uint16_t
que es más estrecho que int
en su sistema, por lo que numberA
pasa a ser int
.
El resultado de agregar dos int
s es otro int
, y 65535 + 1
da 65536
ya que tiene int
s de 32 bits.
Así que su primera printf
produce este resultado.
En la linea:
numberB = numberA + 1;
La lógica anterior todavía se aplica al operador +
, esto es equivalente a:
numberB = 65536;
Dado que numberB
tiene un tipo sin signo, uint16_t
específicamente, 65536
se reduce (mod 65536), lo que da 0
.
Tenga en cuenta que sus dos últimas declaraciones printf
causan un comportamiento indefinido; debe utilizar %u
para imprimir unsigned int
. Para hacer frente a diferentes tamaños de int
, puede usar "%" PRIu32
para obtener el especificador de formato para uint32_t
.