texto - Reglas de promoción de tipo implícito
que es explícito (2)
C fue diseñado para cambiar implícita y silenciosamente los tipos enteros de los operandos utilizados en las expresiones. Existen varios casos en los que el lenguaje obliga al compilador a cambiar los operandos a un tipo más grande o a cambiar su firma.
La razón detrás de esto es evitar desbordamientos accidentales durante la aritmética, pero también permitir que coexistan operandos con diferente firma en la misma expresión.
Desafortunadamente, las reglas para la promoción de tipo implícito causan mucho más daño que bien, hasta el punto de que podrían ser uno de los mayores defectos en el lenguaje C. Estas reglas a menudo ni siquiera son conocidas por el programador promedio de C y, por lo tanto, causan todo tipo de errores muy sutiles.
Por lo general, se ven escenarios en los que el programador dice "simplemente envía al tipo x y funciona", pero no sabe por qué. O tales errores se manifiestan como fenómenos raros e intermitentes que atacan desde dentro de un código aparentemente simple y directo. La promoción implícita es particularmente problemática en el código que realiza manipulaciones de bits, ya que la mayoría de los operadores de C en bits tienen un comportamiento mal definido cuando se les da un operando firmado.
Tipos enteros y rango de conversión
Los tipos enteros en C son
char
,
short
,
int
,
long
,
long long
y
enum
.
_Bool
/
bool
también se trata como un tipo entero cuando se trata de promociones de tipo.
Todos los enteros tienen un rango de conversión especificado. C11 6.3.1.1, énfasis mío en las partes más importantes:
Cada tipo de entero tiene un rango de conversión de enteros definido de la siguiente manera:
- No hay dos tipos enteros con signo que tengan el mismo rango, incluso si tienen la misma representación.
- El rango de un tipo entero con signo será mayor que el rango de cualquier tipo entero con signo con menos precisión.
- El rango delong long int
será mayor que el rango delong int
, que será mayor que el rango deint
, que será mayor que el rango deshort int
, que será mayor que el rango de caracteressigned char
.
- El rango de cualquier tipo de entero sin signo será igual al rango del tipo de entero con signo correspondiente, si lo hay.
- El rango de cualquier tipo entero estándar será mayor que el rango de cualquier tipo entero extendido con el mismo ancho.
- El rango de char será igual al rango de char firmado y no firmado.
- El rango de _Bool será menor que el rango de todos los demás tipos enteros estándar.
- El rango de cualquier tipo enumerado será igual al rango del tipo entero compatible (véase 6.7.2.2).
Los tipos de
stdint.h
ordenan aquí, con el mismo rango que el tipo al que corresponden en el sistema dado.
Por ejemplo,
int32_t
tiene el mismo rango que
int
en un sistema de 32 bits.
Además, C11 6.3.1.1 especifica qué tipos se consideran tipos enteros pequeños (no es un término formal):
Lo siguiente se puede usar en una expresión siempre que se pueda usar un
int
ounsigned int
:- Un objeto o expresión con un tipo entero (distinto de
int
ounsigned int
) cuyo rango de conversión de enteros es menor o igual que el rango deint
yunsigned int
.
Lo que este texto algo críptico significa en la práctica es que
_Bool
,
char
y
short
(y también
int8_t
,
uint8_t
, etc.) son los "tipos enteros pequeños".
Estos se tratan de manera especial y están sujetos a una promoción implícita, como se explica a continuación.
Las promociones enteras
Cada vez que se usa un tipo entero pequeño en una expresión, se convierte implícitamente en
int
que siempre está firmado.
Esto se conoce como las
promociones de enteros
o
la regla de promoción de enteros
.
Formalmente, la regla dice (C11 6.3.1.1):
Si un
int
puede representar todos los valores del tipo original (según lo restringido por el ancho, para un campo de bits), el valor se convierte en unint
; de lo contrario, se convierte en ununsigned int
. Estas se llaman promociones enteras .
Esto significa que todos los tipos enteros pequeños, sin importar la firma, se convierten implícitamente a (firmado)
int
cuando se usan en la mayoría de las expresiones.
Este texto a menudo se malinterpreta como: "todos los tipos enteros pequeños con signo se convierten en int con signo y todos los tipos enteros pequeños sin signo se convierten en int sin signo".
Esto es incorrecto.
La parte sin signo aquí solo significa que si tenemos, por ejemplo, un operando
unsigned short
, y
int
tiene el mismo tamaño que
short
en el sistema dado, entonces el operando
unsigned short
se convierte en
unsigned int
.
Como en, nada de nota realmente sucede.
Pero en caso de que
short
sea un tipo más pequeño que
int
, siempre se convierte en
int
(firmado),
independientemente de si el short fue firmado o no
.
La dura realidad causada por las promociones de enteros significa que casi ninguna operación en C se puede llevar a cabo en tipos pequeños como
char
o
short
.
Las operaciones siempre se llevan a cabo en
int
o tipos más grandes.
Esto puede parecer una tontería, pero afortunadamente el compilador puede optimizar el código.
Por ejemplo, una expresión que contenga dos operandos
unsigned char
conseguiría que los operandos se promocionen a
int
y la operación se lleve a cabo como
int
.
Pero el compilador puede optimizar la expresión para que se realice como una operación de 8 bits, como era de esperar.
Sin embargo, aquí viene el problema: al compilador
no
se
le
permite optimizar el cambio implícito de firma causado por la promoción de enteros.
Porque no hay forma de que el compilador sepa si el programador confía intencionalmente en la promoción implícita o si no es intencional.
Es por eso que el ejemplo 1 en la pregunta falla.
Ambos operandos de caracteres sin signo se promueven al tipo
int
, la operación se realiza en el tipo
int
y el resultado de
x - y
es del tipo
int
.
Lo que significa que obtenemos
-1
lugar de
255
que podría haberse esperado.
El compilador puede generar código de máquina que ejecuta el código con instrucciones de 8 bits en lugar de
int
, pero puede no optimizar el cambio de firma.
Lo que significa que terminamos con un resultado negativo, que a su vez da como resultado un número extraño cuando se invoca
printf("%u
Se invoca el ejemplo 1 volviendo el resultado de la operación a
unsigned char
.
Con la excepción de algunos casos especiales como los operadores
++
y
sizeof
, las promociones de enteros se aplican a casi todas las operaciones en C, sin importar si se utilizan operadores unarios, binarios (o ternarios).
Las conversiones aritméticas habituales
Siempre que una operación binaria (una operación con 2 operandos) se realiza en C, ambos operandos del operador deben ser del mismo tipo. Por lo tanto, en caso de que los operandos sean de diferentes tipos, C impone una conversión implícita de un operando al tipo del otro operando. Las reglas sobre cómo se hace esto se denominan las conversiones artihmetic habituales (a veces denominadas informalmente como "equilibrio"). Estos se especifican en C11 6.3.18:
(Piense en esta regla como una declaración
if-else if
larga y anidada y podría ser más fácil de leer :))
6.3.1.8 Conversiones aritméticas usuales
Muchos operadores que esperan operandos de tipo aritmético provocan conversiones y producen tipos de resultados de manera similar. El propósito es determinar un tipo real común para los operandos y el resultado. Para los operandos especificados, cada operando se convierte, sin cambio de dominio de tipo, a un tipo cuyo tipo real correspondiente es el tipo real común. A menos que se indique explícitamente lo contrario, el tipo real común es también el tipo real correspondiente del resultado, cuyo dominio de tipo es el dominio de tipo de los operandos si son iguales, y complejo de lo contrario. Este patrón se llama las conversiones aritméticas habituales :
- Primero, si el tipo real correspondiente de cualquiera de los operandos es
long double
, el otro operando se convierte, sin cambio de dominio de tipo, a un tipo cuyo tipo real correspondiente eslong double
.- De lo contrario, si el tipo real correspondiente de cualquiera de los operandos es
double
, el otro operando se convierte, sin cambio de dominio de tipo, a un tipo cuyo tipo real correspondiente esdouble
.- De lo contrario, si el tipo real correspondiente de cualquiera de los operandos es
float
, el otro operando se convierte, sin cambio de dominio de tipo, a un tipo cuyo tipo real correspondiente es flotante.De lo contrario, las promociones de enteros se realizan en ambos operandos. Luego, se aplican las siguientes reglas a los operandos promocionados:
- Si ambos operandos tienen el mismo tipo, no se necesita más conversión.
- De lo contrario, si ambos operandos tienen tipos enteros con signo o ambos tienen tipos enteros sin signo, el operando con el tipo de rango de conversión de entero menor se convierte al tipo del operando con mayor rango.
- De lo contrario, si el operando que tiene un tipo entero sin signo tiene un rango mayor o igual al rango del tipo del otro operando, entonces el operando con tipo entero con signo se convierte al tipo del operando con tipo entero sin signo.
- De lo contrario, si el tipo de operando con tipo entero con signo puede representar todos los valores del tipo de operando con tipo entero sin signo, entonces el operando con tipo entero sin signo se convierte en el tipo de operando con tipo entero con signo.
- De lo contrario, ambos operandos se convierten al tipo entero sin signo correspondiente al tipo del operando con tipo entero con signo.
Es notable aquí que las conversiones aritméticas habituales se aplican tanto a las variables de punto flotante como a las enteras.
En el caso de los enteros, también podemos observar que las promociones de enteros se invocan desde las conversiones aritméticas habituales.
Y después de eso, cuando ambos operandos tienen al menos el rango de
int
, los operadores se equilibran con el mismo tipo, con la misma firma.
Esta es la razón por la cual
a + b
en el ejemplo 2 da un resultado extraño.
Ambos operandos son enteros y son al menos de rango
int
, por lo que las promociones de enteros no se aplican.
Los operandos no son del mismo tipo:
a
unsigned int
tiene
unsigned int
y
b
tiene
signed int
.
Por lo tanto, el operador
b
se convierte temporalmente a tipo
unsigned int
.
Durante esta conversión, pierde la información del signo y termina como un gran valor.
La razón por la cual cambiar el tipo a
short
en el ejemplo 3 soluciona el problema, es porque
short
es un tipo entero pequeño.
Lo que significa que ambos operandos son enteros promovidos al tipo
int
que está firmado.
Después de la promoción de enteros, ambos operandos tienen el mismo tipo (
int
), no se necesita más conversión.
Y luego la operación se puede llevar a cabo en un tipo firmado como se esperaba.
Esta publicación está destinada a ser utilizada como una pregunta frecuente con respecto a la promoción de enteros implícitos en C, particularmente la promoción implícita causada por las conversiones aritméticas habituales y / o las promociones de enteros.
Ejemplo 1)
¿Por qué esto da un número entero grande y extraño y no 255?
unsigned char x = 0;
unsigned char y = 1;
printf("%u/n", x - y);
Ejemplo 2)
¿Por qué esto da "-1 es mayor que 0"?
unsigned int a = 1;
signed int b = -2;
if(a + b > 0)
puts("-1 is larger than 0");
Ejemplo 3)
¿Por qué cambiar el tipo en el ejemplo anterior para solucionar el problema?
unsigned short a = 1;
signed short b = -2;
if(a + b > 0)
puts("-1 is larger than 0"); // will not print
(Estos ejemplos estaban destinados a una computadora de 32 o 64 bits con 16 bits cortos).
De acuerdo con la publicación anterior, quiero dar más información sobre cada ejemplo.
Ejemplo 1)
int main(){
unsigned char x = 0;
unsigned char y = 1;
printf("%u/n", x - y);
printf("%d/n", x - y);
}
Como unsigned char es más pequeño que int, aplicamos la promoción entera en ellos, entonces tenemos (int) x- (int) y = (int) (- 1) y unsigned int (-1) = 4294967295.
La salida del código anterior: (igual a lo que esperábamos)
4294967295
-1
¿Como arreglarlo?
Intenté lo que recomendaba la publicación anterior, pero realmente no funciona. Aquí está el código basado en la publicación anterior:
cambiar uno de ellos a unsigned int
int main(){
unsigned int x = 0;
unsigned char y = 1;
printf("%u/n", x - y);
printf("%d/n", x - y);
}
Dado que x ya es un entero sin signo, solo aplicamos la promoción de entero a y. Luego obtenemos (unsigned int) x- (int) y. Como todavía no tienen el mismo tipo, aplicamos las conversiones aritméticas habituales, obtenemos (unsigned int) x- (unsigned int) y = 4294967295.
La salida del código anterior: (igual a lo que esperábamos):
4294967295
-1
Del mismo modo, el siguiente código obtiene el mismo resultado:
int main(){
unsigned char x = 0;
unsigned int y = 1;
printf("%u/n", x - y);
printf("%d/n", x - y);
}
cambiar ambos a int sin firmar
int main(){
unsigned int x = 0;
unsigned int y = 1;
printf("%u/n", x - y);
printf("%d/n", x - y);
}
Dado que ambos son unsigned int, no se necesita ninguna promoción de enteros. Por la conversión aritmética habitual (tienen el mismo tipo), (unsigned int) x- (unsigned int) y = 4294967295.
La salida del código anterior: (igual a lo que esperábamos):
4294967295
-1
Una de las formas posibles de arreglar el código: (agregue un tipo de conversión al final)
int main(){
unsigned char x = 0;
unsigned char y = 1;
printf("%u/n", x - y);
printf("%d/n", x - y);
unsigned char z = x-y;
printf("%u/n", z);
}
La salida del código anterior:
4294967295
-1
255
Ejemplo 2)
int main(){
unsigned int a = 1;
signed int b = -2;
if(a + b > 0)
puts("-1 is larger than 0");
printf("%u/n", a+b);
}
Como ambos son enteros, no se necesita ninguna promoción de enteros. Por la conversión aritmética habitual, obtenemos (unsigned int) a + (unsigned int) b = 1 + 4294967294 = 4294967295.
La salida del código anterior: (igual a lo que esperábamos)
-1 is larger than 0
4294967295
¿Como arreglarlo?
int main(){
unsigned int a = 1;
signed int b = -2;
signed int c = a+b;
if(c < 0)
puts("-1 is smaller than 0");
printf("%d/n", c);
}
La salida del código anterior:
-1 is smaller than 0
-1
Ejemplo 3)
int main(){
unsigned short a = 1;
signed short b = -2;
if(a + b < 0)
puts("-1 is smaller than 0");
printf("%d/n", a+b);
}
El último ejemplo solucionó el problema ya que a y b se convirtieron a int debido a la promoción de enteros.
La salida del código anterior:
-1 is smaller than 0
-1
Si tengo algunos conceptos mezclados, hágamelo saber. Gracias ~