Вопрос о объединении в C - store как одного типа и чтении как другого - определена ли это реализация?

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

Вопрос

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

Теперь, пожалуйста, проверьте этот фрагмент кода:

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

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

Спасибо.

Это было полезно?

Решение

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

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

Другие советы

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

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

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

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

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

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

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

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

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

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

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

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

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

1000000011

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

0000001000000011

Эти два байта являются:

00000010 and 00000011

Который является 2 и 3

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

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

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

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

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

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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top