c=a+b y conversión implícita
implicit-conversion (3)
Con mi compilador, c
es 54464 (16 bits truncados) y d
es 10176. Pero con gcc
, c
es 120000 d
es 600000.
¿Cuál es el verdadero comportamiento? ¿Está indefinido el comportamiento? ¿O es mi compilador falso?
unsigned short a = 60000;
unsigned short b = 60000;
unsigned long c = a + b;
unsigned long d = a * 10;
¿Hay alguna opción para alertar en estos casos?
Wconversion advierte sobre:
void foo(unsigned long a);
foo(a+b);
pero no advierte sobre
unsigned long c = a + b
En C, los tipos char
, short
(y sus couterparts sin signo) y float
deben considerarse como tipos de "almacenamiento" porque están diseñados para optimizar el almacenamiento pero no son el tamaño "nativo" que prefiere la CPU y nunca son utilizado para los cálculos .
Por ejemplo, cuando tiene dos valores char
y los coloca en una expresión, primero se convierten a int
, luego se realiza la operación. La razón es que la CPU funciona mejor con int
. Lo mismo sucede con el float
que siempre se convierte implícitamente a un double
para los cálculos.
En su código, el cálculo a+b
es una suma de dos enteros sin signo; en C no hay forma de calcular la suma de dos cortos sin firmar ... lo que puede hacer es almacenar el resultado final en un corto sin firmar que, gracias a las propiedades del módulo matemático, será el mismo.
Primero, debe saber que en C los tipos estándar no tienen una precisión específica (número de valores representables) para los tipos de enteros estándar. Solo requiere una precisión mínima para cada tipo. Estos resultados en los siguientes tamaños de bits típicos , el standard permite representaciones más complejas:
-
char
: 8 bits -
short
: 16 bits -
int
: 16 (!) bits -
long
: 32 bits -
long long
(desde C99): 64 bits
Nota: Los límites reales (que implican una cierta precisión) de una implementación se dan en limits.h
.
En segundo lugar, el tipo en que se realiza una operación está determinado por los tipos de los operandos, no por el tipo del lado izquierdo de una asignación (debido a que las asignaciones también son solo expresiones). Para esto, los tipos dados arriba están ordenados por rango de conversión . Los operandos con un rango menor que int
se convierten a int
primero. Para otros operandos, el que tiene un rango más pequeño se convierte al tipo del otro operando. Estas son las conversiones aritméticas habituales .
Su implementación parece usar unsigned int
16 bits unsigned int
con el mismo tamaño que unsigned short
, por lo que a
y b
se convierten a unsigned int
, la operación se realiza con 16 bits. Para los unsigned
, la operación se realiza en el módulo 65536 (2 a la potencia de 16); esto se denomina envolvente (¡ no se requiere para los tipos firmados!). El resultado se convierte a un unsigned long
y se asigna a las variables.
Para gcc, asumo que esto se compila para una PC o una CPU de 32 bits. para este (unsigned) int
tiene típicamente 32 bits, mientras que (unsigned) long
tiene al menos 32 bits (requerido). Por lo tanto, no hay envoltura para las operaciones.
Nota: para la PC, los operandos se convierten a int
, no a unsigned int
. Esto se debe a que int
ya puede representar todos los valores de unsigned short
; unsigned int
no es obligatorio. ¡Esto puede dar como resultado un comportamiento inesperado (en realidad: definido por la implementación ) si el resultado de la operación desborda un signed int
!
Si necesita tipos de tamaño definido, consulte stdint.h
(desde C99) para uint16_t
, uint32_t
. Estos son tipos de typedef
a tipos con el tamaño apropiado para su implementación.
También puede convertir uno de los operandos (¡no la expresión completa!) Para el tipo del resultado:
unsigned long c = (unsigned long)a + b;
o, utilizando tipos de tamaño conocido:
#include <stdint.h>
...
uint16_t a = 60000, b = 60000;
uint32_t c = (uint32_t)a + b;
Tenga en cuenta que debido a las reglas de conversión, la conversión de un operando es suficiente.
Actualización (gracias a @chux):
El elenco mostrado arriba funciona sin problemas. Sin embargo, si a
tiene un rango de conversión mayor que el de la conversión, esto podría truncar su valor al tipo más pequeño. Si bien esto se puede evitar fácilmente ya que todos los tipos se conocen en tiempo de compilación (escritura estática), una alternativa es multiplicar con 1 del tipo deseado:
unsigned long c = ((unsigned long)1U * a) + b
De esta manera, se utiliza el rango mayor del tipo dado en el modelo o a
(o b
). La multiplicación será eliminada por cualquier compilador razonable.
Otro enfoque, evitar incluso saber el nombre del tipo de destino se puede hacer con la extensión typeof()
gcc:
unsigned long c;
... many lines of code
c = ((typeof(c))1U * a) + b
a + b
se computará como un unsigned int
(el hecho de que se asigne a un unsigned long
no es relevante). El estándar de C obliga a que esta suma envuelva alrededor de módulo "uno más el más grande sin firmar posible". En su sistema, parece que un unsigned int
es de 16 bits, por lo que el resultado se calcula en módulo 65536.
En el otro sistema, parece que int
y unsigned int
son más grandes y, por lo tanto, capaces de contener los números más grandes. Lo que sucede ahora es bastante sutil (reconozca @PascalCuoq): debido a que todos los valores de unsigned short
son representables en int
, a + b
se computará como un int
. (Solo si short
y int
son del mismo ancho o, de alguna otra manera, algunos valores de unsigned short
no se pueden representar como int
, la suma se computará como unsigned int
).
Aunque el estándar C no especifica un tamaño fijo para un unsigned short
o un unsigned int
, el comportamiento de su programa está bien definido. Tenga en cuenta que esto no es cierto para un tipo firmado sin embargo.
Como observación final, puede utilizar los tipos de tamaño uint16_t
, uint16_t
, etc. que, si son compatibles con su compilador, tienen garantizado el tamaño especificado.