Вопрос об объединении в C - хранить как один тип и читать как другой - это определяется реализацией? - PullRequest
28 голосов
/ 28 ноября 2009

Я читал об объединении в 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. Определена ли реализация? Или я делаю что-то действительно глупое?

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

Спасибо.

Ответы [ 6 ]

22 голосов
/ 28 ноября 2009

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

Пример

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

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

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

  2. С -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 оптимизировал вызов, он выбрал нули в качестве случайного значения потенциального неинициализированного объединения.

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

17 голосов
/ 28 ноября 2009

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

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

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

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

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

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

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

9 голосов
/ 28 ноября 2009

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

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

5 голосов
/ 28 ноября 2009

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

Если бы вы сделали это в системе с прямым порядком байтов, вы бы (вероятно) получили 770 (при условии 16-битных целых) или 50462720 (при условии 32-битных целых).

5 голосов
/ 28 ноября 2009

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

515 в двоичном виде -

1000000011

Заполнение нулями до двух байтов (при условии 16-битного целого):

0000001000000011

Два байта:

00000010 and 00000011

Что есть 2 и 3

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

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

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

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

2 голосов
/ 28 ноября 2009

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

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

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

...