Вопрос об объединении в 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
, Определена ли реализация? Или я делаю что-то действительно глупое?
Я знаю, что это может показаться новичком большинству других людей, но я не могу понять причину этого выхода.
Благодарю.
6 ответов
Это неопределенное поведение. 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
оптимизировал вызов, он выбрал нули в качестве случайного значения потенциального неинициализированного объединения.
Запись одному члену союза и чтение другого обычно не имеет особого смысла, но иногда это может быть полезно для программ, скомпилированных со строгим псевдонимом.
Ответ на этот вопрос зависит от исторического контекста, поскольку спецификация языка менялась со временем. И это дело, как раз то, что затронуто изменениями.
Вы сказали, что читаете K&R. Последнее издание этой книги (на данный момент) описывает первую стандартизированную версию языка Си - C89/90. В этой версии языка Си написание одного члена союза и чтение другого члена является неопределенным поведением. Не определенная реализация (что другое), но неопределенное поведение. Соответствующая часть языкового стандарта в этом случае - 6,5/7.
Теперь, на более позднем этапе эволюции C (версия спецификации языка C99 с применением Технического исправления 3) внезапно стало законным использовать union для обозначения типов, то есть написать один член объединения, а затем прочитать другой.
Обратите внимание, что попытка сделать это может привести к неопределенному поведению. Если прочитанное вами значение оказывается недопустимым (так называемое "представление ловушки") для типа, через который вы его читаете, поведение по-прежнему не определено. В противном случае прочитанное вами значение определяется реализацией.
Ваш конкретный пример относительно безопасен для типа наказания от int
в char[2]
массив. На языке C всегда допустимо переосмысливать содержимое любого объекта как массив символов (опять же, 6.5/7).
Однако обратное неверно. Запись данных в char[2]
член массива вашего союза, а затем читать его как int
потенциально может создать представление ловушки и привести к неопределенному поведению. Потенциальная опасность существует, даже если ваш массив символов имеет достаточную длину, чтобы покрыть весь int
,
Но в вашем конкретном случае, если int
бывает больше, чем char[2]
, int
прочитанное будет охватывать неинициализированную область за концом массива, что снова приведет к неопределенному поведению.
Причина вывода в том, что на вашем компьютере целые числа хранятся в формате с прямым порядком байтов: младшие байты сохраняются первыми. Следовательно, последовательность байтов [3,2,0,0] представляет собой целое число 3+2*256=515.
Этот результат зависит от конкретной реализации и платформы.
Вывод из такого кода будет зависеть от вашей платформы и реализации компилятора Си. Ваш вывод заставляет меня думать, что вы запускаете этот код в системе с прямым порядком байтов (вероятно, x86). Если бы вы поместили 515 в i и посмотрели на него в отладчике, вы бы увидели, что младший байт будет 3, а следующий байт в памяти будет 2, что точно соответствует тому, что вы положили в ch.
Если бы вы сделали это в системе с прямым порядком байтов, вы бы (вероятно) получили 770 (при условии 16-битных целых) или 50462720 (при условии 32-битных целых).
Это зависит от реализации, и результаты могут отличаться на разных платформах / компиляторах, но, похоже, именно это и происходит:
515 в двоичном является
1000000011
Заполнение нулями, чтобы сделать его двумя байтами (при условии 16-битного int):
0000001000000011
Два байта:
00000010 and 00000011
Который 2
а также 3
Надеюсь, кто-то объясняет, почему они меняются местами - я предполагаю, что символы не меняются местами, а int имеет младший порядок.
Объем памяти, выделенной объединению, равен памяти, необходимой для хранения самого большого члена. В этом случае у вас есть массив int и char длиной 2. Предполагая, что int 16-битный, а char 8-битный, оба требуют одинакового пространства и, следовательно, объединению выделяется два байта.
Когда вы назначаете три (00000011) и два (00000010) в массив символов, состояние объединения 0000001100000010
, Когда вы читаете int из этого объединения, оно конвертирует все в целое число. Предполагая представление с прямым порядком байтов, где LSB хранится по самому низкому адресу, чтение int из объединения будет 0000001000000011
который является двоичным для 515.
ПРИМЕЧАНИЕ. Это верно даже в том случае, если int был 32-разрядным. Проверьте ответ Амнона.
Если вы работаете в 32-битной системе, тогда int равен 4 байта, но вы инициализируете только 2 байта. Доступ к неинициализированным данным - неопределенное поведение.
Предполагая, что вы работаете в системе с 16-битными значениями, то, что вы делаете, по-прежнему определяется реализацией. Если ваша система имеет прямой порядок байтов, то u.ch[0] будет соответствовать младшему байту ui, а u.ch 1 будет самым старшим байтом. В системе с прямым порядком байтов все наоборот. Кроме того, стандарт C не заставляет реализацию использовать дополнение до двух для представления целочисленных значений со знаком, хотя дополнение двух является наиболее распространенным. Очевидно, что размер целого числа также определяется реализацией.
Подсказка: легче увидеть, что происходит, если вы используете шестнадцатеричные значения. В системе с прямым порядком байтов результат в hex будет 0x0203.