Доступ к членам профсоюза 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

Конечно, вам все еще нужно убедиться, что представление типа, используемого для записи, также может быть интерпретировано как допустимое (не прерывистое) представление для типа, позже используемого для чтения.

Другие вопросы по тегам