Доступ к членам профсоюза C с помощью указателей
Приводит ли доступ к членам объединения через указатель, как в примере ниже, к неопределенному поведению в C99? Намерение кажется достаточно ясным, но я знаю, что есть некоторые ограничения в отношении псевдонимов и союзов.
union { int i; char c; } u;
int *ip = &u.i;
char *ic = &u.c;
*ip = 0;
*ic = 'a';
printf("%c\n", u.c);
3 ответа
Это неопределенное (слегка отличающееся от неопределенного) поведение для доступа к объединению любым элементом, кроме того, который был последний раз записан. Это подробно описано в приложении C99 J:
Следующее не указано:
:
Значение члена объединения, кроме последнего, сохраненного в (6.2.6.1).
Тем не менее, так как вы пишете c
через указатель, затем чтение c
этот конкретный пример хорошо определен. Неважно, как вы пишете в элемент:
u.c = 'a'; // direct write.
*(&(u.c)) = 'a'; // variation on yours, writing through element pointer.
(&u)->c = 'a'; // writing through structure pointer.
В комментариях был поднят один вопрос, который, кажется, противоречит этому, по крайней мере, на первый взгляд. пользователь davmac
предоставляет пример кода:
// Compile with "-O3 -std=c99" eg:
// clang -O3 -std=c99 test.c
// gcc -O3 -std=c99 test.c
// On clang v3.5.1, output is "123"
// On gcc 4.8.4, output is "1073741824"
//
// Different outputs, so either:
// * program invokes undefined behaviour; both compilers are correct OR
// * compiler vendors interpret standard differently OR
// * one compiler or the other has a bug
#include <stdio.h>
union u
{
int i;
float f;
};
int someFunc(union u * up, float *fp)
{
up->i = 123;
*fp = 2.0; // does this set the union member?
return up->i; // then this should not return 123!
}
int main(int argc, char **argv)
{
union u uobj;
printf("%d\n", someFunc(&uobj, &uobj.f));
return 0;
}
который выводит разные значения на разных компиляторах. Тем не менее, я считаю, что это потому, что он на самом деле нарушает правила здесь, потому что он пишет члену f
затем читает член i
и, как показано в Приложении J, это не указано.
Сноска 82 в 6.5.2.3
в котором говорится:
Если элемент, используемый для доступа к содержимому объекта объединения, не совпадает с элементом, который последний раз использовался для хранения значения в объекте, соответствующая часть представления объекта значения повторно интерпретируется как представление объекта в новом типе.
Однако, поскольку это, кажется, идет вразрез с комментарием Приложения J, и это сноска к разделу, касающемуся выражений в форме x.y
, это может не относиться к доступам через указатель.
Одна из основных причин, почему псевдонимы должны быть строгими, - предоставить компилятору больше возможностей для оптимизации. С этой целью стандарт предписывает, что обработка памяти типа, отличного от написанного, не определена.
В качестве примера рассмотрим предоставленную функцию:
int someFunc(union u * up, float *fp)
{
up->i = 123;
*fp = 2.0; // does this set the union member?
return up->i; // then this should not return 123!
}
Реализация может предположить, что, поскольку вы не должны псевдоним памяти, up->i
а также *fp
два разных объекта. Так что можно предположить, что вы не меняете значение up->i
после того, как вы установите его 123
так что он может просто вернуться 123
не глядя на фактическое содержимое переменной снова.
Если вместо этого вы изменили оператор установки указателя на:
up->f = 2.0;
тогда это применимо к сноске 82, а возвращаемое значение будет интерпретировать число с плавающей точкой как целое число.
Причина, по которой я не думаю, что это является проблемой для вопроса, заключается в том, что ваши письма читают один и тот же тип, следовательно, правила наложения имен не вступают в игру.
Интересно отметить, что неуказанное поведение вызвано не самой функцией , а вызванием ее таким образом:
union u up;
int x = someFunc (&u, &(up.f)); // <- aliasing here
Если бы вы вместо этого называли это так:
union u up;
float down;
int x = someFunc (&u, &down); // <- no aliasing
это не будет проблемой.
Нет, это не так, но вам нужно отслеживать, каким был последний тип, который вы добавили в объединение. Если бы я изменил порядок вашего int
а также char
с заданиями это будет совсем другая история
#include <stdio.h>
union { int i; char c; } u;
int main()
{
int *ip = &u.i;
char *ic = &u.c;
*ic = 'a';
*ip = 123456;
printf("%c\n", u.c); /* trying to print a char even though
it's currently storing an int,
in this case it prints '@' on my machine */
return 0;
}
РЕДАКТИРОВАТЬ: Некоторые объяснения, почему он мог печатать 64 ('@').
Двоичное представление 123456: 0001 1110 0010 0100 0000.
Для 64 это 0100 0000.
Вы можете видеть, что первые 8 битов идентичны и, так как printf
поручено прочитать первые 8 бит, он печатает только столько же.
Единственная причина, по которой это не UB, в том, что вам повезло / не повезло выбрать char
для одного из типов, а типы символов могут быть псевдонимами в C. Если типы были, например, int
а также float
доступ через указатели будет приводить к нарушениям псевдонимов и, следовательно, к неопределенному поведению. Для прямого доступа через объединение, поведение считалось четко определенным как часть интерпретации для Отчета о дефектах 283:
http://www.open-std.org/jtc1/sc22/wg14/www/docs/dr_283.htm
Конечно, вам все еще нужно убедиться, что представление типа, используемого для записи, также может быть интерпретировано как допустимое (не прерывистое) представление для типа, позже используемого для чтения.