Вопрос о союзе в 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.