switch - ¿Cómo se evalúa el operador de cambio en C?
switch c++ (3)
Recientemente noté un comportamiento (extraño) cuando realicé operaciones usando shift >>
<<
!
Para explicarlo, permítame escribir este pequeño código ejecutable que realiza dos operaciones que se supone que son idénticas (en mi opinión), ¡pero estoy sorprendido con resultados diferentes!
#include <stdio.h>
int main(void) {
unsigned char a=0x05, b=0x05;
// first operation
a = ((a<<7)>>7);
// second operation
b <<= 7;
b >>= 7;
printf("a=%X b=%X/n", a, b);
return 0;
}
Cuando se ejecuta, a = 5
y b = 1
. ¡Espero que ambos sean iguales a 1! ¿Puede alguien explicar amablemente por qué obtuve tal resultado?
PD: en mi entorno, el tamaño de caracteres unsigned char
es de 1 byte
En el primer ejemplo:
-
a
se convierte en unint
, se desplaza a la izquierda, luego a la derecha y luego se convierte de nuevo ausigned char
.
Esto dará como resultado a a=5
obviamente.
En el segundo ejemplo:
-
b
se convierte aint
, se desplaza a la izquierda y luego se convierte de nuevo aunsigned char
. -
b
se convierte aint
, se desplaza a la derecha y, a continuación, se vuelve a convertir enunsigned char
.
La diferencia es que se pierde información en el segundo ejemplo durante la conversión a caracteres unsigned char
Explicación detallada de las cosas que suceden entre las líneas:
Caso a:
- En la expresión
a = ((a<<7)>>7);
,a<<7
se evalúa primero. - El estándar de C indica que cada operando de los operadores de desplazamiento se promueve implícitamente de enteros, lo que significa que si son de los tipos bool, char, short, etc. (colectivamente, los "tipos de enteros pequeños"), se ascienden a un
int
. - Esta es una práctica estándar para casi todos los operadores en C. Lo que hace que los operadores de turnos sean diferentes de los demás operadores es que no usan el otro tipo de promoción implícita común llamada "balanceo". En cambio, el resultado de un turno siempre tiene el tipo del operando izquierdo promovido. En este caso
int
. - Por lo tanto,
a
se promueve al tipoint
, que aún contiene el valor 0x05. El7
literal ya era de tipoint
por lo que no se promociona. - Cuando dejas Shift este
int
por 7, obtienes 0x0280. El resultado de la operación es de tipoint
. - Tenga en cuenta que
int
es un tipo con signo, por lo que si hubiera seguido desplazando los datos aún más hacia los bits de signo, habría invocado un comportamiento indefinido. De manera similar, si el operando izquierdo o derecho fuera un valor negativo, también invocaría un comportamiento indefinido. - Ahora tienes la expresión a =
0x280 >> 7;
. No se realizan promociones para la siguiente operación de turno, ya que ambos operandos ya están int. - El resultado es 5 y del tipo int. Luego, convierte este int en un carácter sin signo, lo cual está bien, ya que el resultado es lo suficientemente pequeño como para caber.
Caso b:
-
b <<= 7;
es equivalente ab = b << 7;
. - Como antes,
b
es promovido a unint
. El resultado será nuevamente 0x0280. - A continuación, intenta almacenar este resultado en un carácter sin firmar. No encajará, por lo que se truncará para que solo contenga el byte menos significativo
0x80
. - En la siguiente línea,
b
vuelve a ascender a un int, que contiene 0x80. - Y luego cambia 0x80 por 7, obteniendo el resultado 1. Esto es de tipo int, pero puede caber en un carácter sin signo, por lo que cabrá en b.
Buen consejo:
- Nunca utilice operadores de bits en tipos de enteros con signo. Esto no tiene ningún sentido en el 99% de los casos, pero puede provocar varios errores y un comportamiento mal definido.
- Cuando utilice operadores de bit-bit, use los tipos en
stdint.h
en lugar de los tipos predeterminados primitivos en C. - Cuando se utilizan operadores de bit bitwise, use conversiones explícitas al tipo deseado, para evitar errores y cambios de tipo no intencionados, pero también para dejar en claro que realmente comprende cómo funcionan las promociones de tipo implícito y que no acaba de hacer funcionar el código. por accidente.
Una forma mejor y más segura de escribir tu programa hubiera sido:
#include <stdio.h>
#include <stdint.h>
int main(void) {
uint8_t a=0x05;
uint8_t b=0x05;
uint32_t tmp;
// first operation
tmp = (uint32_t)a << 7;
tmp = tmp >> 7;
a = (uint8_t)tmp;
// second operation
tmp = (uint32_t)b << 7;
tmp = tmp >> 7;
b = (uint8_t)tmp;
printf("a=%X b=%X/n", a, b);
return 0;
}
Las operaciones de cambio harían promociones enteras a sus operandos, y en su código, el int
resultante se convierte de nuevo a char
siguiente manera:
// first operation
a = ((a<<7)>>7); // a = (char)((a<<7)>>7);
// second operation
b <<= 7; // b = (char) (b << 7);
b >>= 7; // b = (char) (b >> 7);
Cita del borrador N1570 (que luego se convirtió en el estándar de C11):
6.5.7 Operadores de cambio bitwise:
- Cada uno de los operandos tendrá tipo entero.
- Las promociones enteras se realizan en cada uno de los operandos. El tipo de resultado es el del operando izquierdo promovido. Si el valor del operando derecho es negativo o es mayor o igual que el ancho del operando izquierdo promovido, el comportamiento no está definido.
Y se supone que en C99 y C90 hay declaraciones similares.