Вопрос о союзе в C

Я читал о объединении в C от K & R, насколько я понял, единственная переменная в объединении может содержать любой из нескольких типов, и если что-то хранится как один тип и извлекается как другое, результат получается чисто реализация определена.

Теперь проверьте этот фрагмент кода:

#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;
 }

Вывод:

3 2 515

Здесь я присваиваю значения в u.ch, но получая как из u.ch, так и u.i, определена ли реализация? или я делаю что-то действительно глупо?

Я знаю, что это может показаться очень новичком для большинства других людей, но я не могу понять причину этого вывода.

Спасибо,

Ответ 1

Это поведение undefined. u.i и u.ch расположены по одному и тому же адресу памяти. Таким образом, результат записи в один и чтение от другого зависит от компилятора, платформы, архитектуры, а иногда и от уровня оптимизации компилятора. Поэтому вывод для u.i не всегда может быть 515.

Пример

Например, gcc на моей машине создает два разных ответа для -O0 и -O2.

  • Так как моя машина имеет 32-разрядную малоконечную архитектуру, -O0 я заканчиваю двумя наименее значимыми байтами, инициализированными до 2 и 3, два самых значимых байта не инициализируются. Таким образом, память объединения выглядит так: {3, 2, garbage, garbage}

    Следовательно, я получаю вывод, похожий на 3 2 -1216937469.

  • С -O2 я получаю вывод 3 2 515, как вы, что делает память объединения {3, 2, 0, 0}. Случается, что gcc оптимизирует вызов printf с фактическими значениями, поэтому вывод сборки выглядит как эквивалент:

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

    Значение 515 может быть получено как другое объяснение в других ответах на этот вопрос. По сути, это означает, что когда gcc оптимизировал вызов, он выбрал нули в качестве случайного значения потенциального неинициализированного объединения.

Запись в один член профсоюза и чтение из другого обычно не имеет большого смысла, но иногда оно может быть полезно для программ, скомпилированных со строгим псевдонимом.

Ответ 2

Ответ на этот вопрос зависит от исторического контекста, поскольку спецификация языка изменилась со временем. И этот вопрос случается с тем, что повлияло на изменения.

Вы сказали, что читаете K & R. В последнем выпуске этой книги (на данный момент) описана первая стандартизованная версия языка C - C89/90. В этой версии языка C одним членом объединения и чтением другого члена является поведение undefined. Не реализована определенная реализация (это совсем другое), но поведение undefined. Соответствующая часть языкового стандарта в этом случае равна 6.5/7.

Теперь, в какой-то более поздний момент в эволюции C (версия спецификации языка C99 с Техническим исправлением 3), стало неожиданным использование союза для типа punning, то есть для записи одного члена объединения, а затем для чтения другого.

Обратите внимание, что попытка сделать это может привести к поведению undefined. Если прочитанное вами значение окажется недопустимым (так называемое "представление ловушки" ) для типа, который вы прочитали, то поведение по-прежнему undefined. В противном случае значение, которое вы читаете, определяется реализацией.

Ваш конкретный пример относительно безопасен для типа punning от int до char[2]. В языке C всегда легально переосмыслить содержимое любого объекта как массив char (опять же, 6.5/7).

Однако обратное неверно. Запись данных в член массива char[2] вашего объединения, а затем чтение его как int может потенциально создать представление ловушки и привести к поведению undefined. Потенциальная опасность существует, даже если ваш массив char имеет достаточную длину для покрытия всего int.

Но в вашем конкретном случае, если int окажется больше, чем char[2], прочитанный int будет охватывать неинициализированную область за пределами массива, что снова приведет к поведению undefined.

Ответ 3

Причина вывода заключается в том, что на вашем компьютере целые числа хранятся в формате little-endian: сначала сохраняются младшие значащие байты. Следовательно, последовательность байтов [3,2,0,0] представляет собой целое число 3 + 2 * 256 = 515.

Этот результат зависит от конкретной реализации и платформы.

Ответ 4

Это зависит от реализации, и результаты могут отличаться от другой платформы/компилятора, но, похоже, это происходит:

515 в двоичном формате

1000000011

Заполняющие нули, чтобы сделать это двумя байтами (предполагая 16 бит int):

0000001000000011

Два байта:

00000010 and 00000011

Что такое 2 и 3

Надеюсь, что кто-то объяснит, почему они обращены вспять - я предполагаю, что символы не меняются, но int немного аргументирован.

Объем памяти, выделенной объединению, равен памяти, необходимой для хранения самого большого члена. В этом случае у вас есть массив int и char длины 2. Предполагая, что int - 16 бит, а char - 8 бит, оба требуют одинакового пространства, и, следовательно, союзу выделяются два байта.

Когда вы назначаете три (00000011) и два (00000010) в массив char, состояние объединения равно 0000001100000010. Когда вы читаете int из этого объединения, он преобразует всю вещь в и целое. Предполагая little-endian, где LSB хранится с наименьшим адресом, int, считанный из объединения, будет 0000001000000011, который является двоичным для 515.

ПРИМЕЧАНИЕ. Это верно, даже если int был 32 бит - проверьте ответ Amnon

Ответ 5

Выход из такого кода будет зависеть от вашей платформы и реализации компилятора C. Ваш вывод заставляет меня думать, что вы используете этот код в системе litte-endian (возможно, x86). Если бы вы поставили 515 в я и посмотрели на него в отладчике, вы увидите, что младший байт будет 3, а следующий байт в памяти будет 2, который точно соответствует тому, что вы положили в ch.

Если вы сделали это в системе с большим числом сторон, вы бы (вероятно) получили 770 (предполагая 16-битные int) или 50462720 (предполагая 32-битные ints).

Ответ 6

Если вы используете 32-разрядную систему, тогда int составляет 4 байта, но вы только инициализируете только 2 байта. Доступ к неинициализированным данным - это поведение undefined.

Предполагая, что вы находитесь в системе с 16-битными ints, то то, что вы делаете, по-прежнему определяется реализацией. Если ваша система немного ориентирована, то u.ch [0] будет соответствовать наименее значащему байту ui и u.ch 1 будет самый старший байт. В большой системе, это наоборот. Кроме того, стандарт C не заставляет реализацию использовать два дополнения для представления знаковых целочисленных значений, хотя два дополнения являются наиболее распространенными. Очевидно, что размер целого также определяется реализацией.

Подсказка: легче видеть, что происходит, если вы используете шестнадцатеричные значения. В маленькой системе endian результат в hex будет 0x0203.