Una pregunta sobre la unión en C: almacenar como un tipo y leer como otro: ¿se define su implementación?

StackOverflow https://stackoverflow.com/questions/1812348

Pregunta

Estaba leyendo acerca de la unión en C de K & amp; R, por lo que entendí, una sola variable en unión puede contener cualquiera de los varios tipos y si algo se almacena como un tipo y se extrae como otro, el resultado es puramente implementación definida.

Ahora compruebe este fragmento de código:

#include<stdio.h>

int main(void)
{
  union a
  {
     int i;
     char ch[2];
  };

  union a u;
  u.ch[0] = 3;
  u.ch[1] = 2;

  printf("%d %d %d\n", u.ch[0], u.ch[1], u.i);

  return 0;
}

Salida:

3 2 515

Aquí estoy asignando valores en u.ch pero recuperando tanto de u.ch como de u.i . ¿Está definida la implementación? ¿O estoy haciendo algo realmente tonto?

Sé que puede parecer muy principiante para la mayoría de las personas, pero no puedo entender la razón detrás de esa salida.

Gracias.

¿Fue útil?

Solución

Este es un comportamiento indefinido. u.i y u.ch se encuentran en la misma dirección de memoria. Por lo tanto, el resultado de escribir en uno y leer del otro depende del compilador, la plataforma, la arquitectura y, a veces, incluso el nivel de optimización del compilador. Por lo tanto, la salida para u.i puede no ser siempre 515 .

Ejemplo

Por ejemplo, gcc en mi máquina produce dos respuestas diferentes para -O0 y -O2 .

  1. Debido a que mi máquina tiene una arquitectura little-endian de 32 bits, con -O0 termino con dos bytes menos significativos inicializados en 2 y 3, los dos bytes más significativos no se inicializan. Entonces la memoria de la unión se ve así: {3, 2, basura, basura}

    Por lo tanto, obtengo una salida similar a 3 2 -1216937469 .

  2. Con -O2 , obtengo la salida de 3 2 515 como usted, lo que hace que la memoria de unión {3, 2, 0, 0} . Lo que sucede es que gcc optimiza la llamada a printf con valores reales, por lo que la salida del ensamblaje parece un equivalente de:

    #include <stdio.h>
    int main() {
        printf("%d %d %d\n", 3, 2, 515);
        return 0;
    }
    

    El valor 515 se puede obtener como otro explicado en otras respuestas a esta pregunta. En esencia, significa que cuando gcc optimizó la llamada, eligió ceros como el valor aleatorio de una posible unión no inicializada.

Escribir a un miembro del sindicato y leer de otro generalmente no tiene mucho sentido, pero a veces puede ser útil para programas compilados con alias estricto .

Otros consejos

La respuesta a esta pregunta depende del contexto histórico, ya que la especificación del lenguaje cambió con el tiempo. Y este asunto es el afectado por los cambios.

Dijiste que estabas leyendo K & amp; R. La última edición de ese libro (a partir de ahora), describe la primera versión estandarizada del lenguaje C: C89 / 90. En esa versión del lenguaje C, escribir un miembro de la unión y leer a otro miembro es comportamiento indefinido . No implementación definida (que es una cosa diferente), sino comportamiento indefinido . La porción relevante del estándar de idioma en este caso es 6.5 / 7.

Ahora, en algún momento posterior en la evolución de C (se aplicó la versión C99 de la especificación del lenguaje con el Corrigendum técnico 3), de repente se convirtió en legal usar union para el tipo de castigo, es decir, escribir un miembro de la unión y luego leer otro.

Tenga en cuenta que intentar hacerlo todavía puede conducir a un comportamiento indefinido. Si el valor que lee no es válido (lo que se denomina " representación de trampa ") para el tipo que lo leyó, entonces el comportamiento aún no está definido. De lo contrario, el valor que lee es la implementación definida.

Su ejemplo específico es relativamente seguro para la escritura de tipos de int a char [2] array. Siempre es legal en lenguaje C reinterpretar el contenido de cualquier objeto como una matriz de caracteres (nuevamente, 6.5 / 7).

Sin embargo, lo contrario no es cierto. Escribir datos en el miembro de la matriz char [2] de su unión y luego leerlos como un int puede potencialmente crear una representación de trampa y conducir a un comportamiento indefinido . El peligro potencial existe incluso si su matriz de caracteres tiene la longitud suficiente para cubrir todo el int .

Pero en su caso específico, si int es más grande que char [2] , el int que lea cubrirá el área no inicializada más allá del final de la matriz, lo que nuevamente conduce a un comportamiento indefinido.

La razón detrás de la salida es que en su máquina los enteros se almacenan en little-endian formato: los bytes menos significativos se almacenan primero. De ahí la secuencia de bytes [3,2,0,0] representa el número entero 3 + 2 * 256 = 515.

Este resultado depende de la implementación específica y la plataforma.

El resultado de dicho código dependerá de su plataforma y la implementación del compilador de C. Su salida me hace pensar que está ejecutando este código en un sistema litte-endian (probablemente x86). Si pusiera 515 en i y lo mirara en un depurador, vería que el byte de orden más bajo sería un 3 y el siguiente byte en la memoria sería un 2, que se asigna exactamente a lo que puso en ch.

Si hicieras esto en un sistema big-endian, probablemente (7) obtendrías 770 (suponiendo entradas de 16 bits) o 50462720 (suponiendo entradas de 32 bits).

Depende de la implementación y los resultados pueden variar en una plataforma / compilador diferente, pero parece que esto es lo que está sucediendo:

515 en binario es

1000000011

Relleno de ceros para que sea de dos bytes (suponiendo int 16 bit):

0000001000000011

Los dos bytes son:

00000010 and 00000011

Que es 2 y 3

Espero que alguien explique por qué se invierten. Supongo que los caracteres no se invierten, pero el int es little endian.

La cantidad de memoria asignada a una unión es igual a la memoria requerida para almacenar el miembro más grande. En este caso, tiene una matriz int y char de longitud 2. Suponiendo que int es de 16 bits y char es de 8 bits, ambos requieren el mismo espacio y, por lo tanto, a la unión se le asignan dos bytes.

Cuando asigna tres (00000011) y dos (00000010) a la matriz de caracteres, el estado de la unión es 0000001100000010 . Cuando lees el int de esta unión, convierte todo en un entero. Asumiendo little-endian representación donde LSB se almacena en la dirección más baja, la lectura interna desde la unión sería 0000001000000011 que es el binario para 515.

NOTA: Esto es válido incluso si el int era de 32 bits - Verifique La respuesta de Amnon

Si está en un sistema de 32 bits, entonces un int es de 4 bytes pero solo inicializa solo 2 bytes. Acceder a datos no inicializados es un comportamiento indefinido.

Suponiendo que está en un sistema con entradas de 16 bits, lo que está haciendo todavía está definido en la implementación. Si su sistema es little endian, entonces u.ch [0] corresponderá con el byte menos significativo de ui y u.ch 1 será el byte más significativo. En un sistema endian grande, es al revés. Además, el estándar C no obliga a la implementación a usar complemento de dos para representar un entero con signo valores, aunque el complemento a dos es el más común. Obviamente, el tamaño de un número entero también está definido en la implementación.

Sugerencia: es más fácil ver qué sucede si usa valores hexadecimales. En un pequeño sistema endian, el resultado en hexadecimal sería 0x0203.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top