Какая разница между "(type) variable" и "* ((type *) & variable)", если таковая имеется?

Я хотел бы знать, есть ли разница между:

  • Приведение примитивной переменной в другой примитивный тип
  • Выявление разметки примитивного адреса переменной указателю другого примитивного типа

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

/* compile with gcc -lm */
#include <stdio.h>
#include <math.h>

int main(void)
{
    unsigned max_unsigned = pow(2, 8 * sizeof(unsigned)) - 1;

    printf("VALUES:\n");
    printf("%u\n", max_unsigned + 1);
    printf("%lu\n", (unsigned long)max_unsigned + 1);          /* case 1 */
    printf("%lu\n", *((unsigned long *)&max_unsigned) + 1);    /* case 2 */

    printf("SIZES:\n");
    printf("%d\n", sizeof(max_unsigned));
    printf("%d\n", sizeof((unsigned long)max_unsigned));       /* case 1 */
    printf("%d\n", sizeof(*((unsigned long *)&max_unsigned))); /* case 2 */

    return 0;
}

Вывод:

VALUES:
0
4294967296
4294967296
SIZES:
4
8
8

С моей точки зрения, не должно быть никаких различий между (1) и (2), но я хотел проконсультироваться с экспертами SO для проверки работоспособности.

Ответы

Ответ 1

Первый актерский состав является законным; второй актерский состав не может быть законным.

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

Второй листинг говорит компилятору забыть его знание типа переменной и повторить интерпретацию его внутреннего представления как другого типа *. Это имеет ограниченную применимость: если двоичное представление совпадает с типом, указанным целевым указателем, это преобразование будет работать. Однако это не эквивалентно первому исполнению, потому что в этой ситуации преобразование значения никогда не происходит.

Переключение типа переменной, передаваемой чему-то с другим представлением, например, float, хорошо иллюстрирует эту точку: первое преобразование приводит к правильному результату, тогда как второе преобразование создает мусор:

float test = 123456.0f;
printf("VALUES:\n");
printf("%f\n", test + 1);
printf("%lu\n", (unsigned long)test + 1);
printf("%lu\n", *((unsigned long *)&test) + 1); // Undefined behavior

Отпечатает

123457.000000
123457
1206984705

(demo)

<ч/" > * Это допустимо только в том случае, если один из типов является типом символа и правильность указателя действительна, преобразование типа тривиально (т.е. когда нет преобразования), когда вы меняете квалификаторы или подпись, или когда вы добавляете в/из struct/union, причем первый член является действительным источником/целевым источником преобразования. В противном случае это приведет к поведению undefined. Для полного описания см. C 2011 (N1570), 6.5 7. Спасибо, Eric Postpischil, чтобы указать ситуации, когда определено второе преобразование.

Ответ 2

Посмотрите на два простых примера: int и float на современном оборудовании (без смешного бизнеса).

float x = 1.0f;
printf("(int) x = %d\n", (int) x);
printf("*(int *) &x = %d\n", *(int *) &x);

Выход, возможно... (ваши результаты могут отличаться)

(int) x = 1
*(int *) &x = 1065353216

Что происходит с (int) x, вы преобразовываете значение, 1.0f, в целое число.

Что происходит с *(int *) &x, вы делаете вид, что это значение уже целое. Это не было целым числом.

Представление с плавающей запятой 1.0 означает следующее (в двоичном формате):

00111111 100000000 00000000 0000000

Это то же представление, что и целое число 1065353216.

Ответ 3

Это:

(type)variable

принимает значение variable и преобразует его в тип type. Это преобразование не обязательно просто копирует биты представления; он следует правилам языка для конверсий. В зависимости от исходного и целевого типов результат может иметь то же математическое значение, что и variable, но он может быть представлен совершенно по-другому.

Это:

*((type *)&variable)

делает что-то, называемое aliasing, иногда неофициально называемое type-punning. Он занимает кусок памяти, занимаемый variable, и обрабатывает его, как если бы это был объект типа type. Это может привести к нечетным результатам или даже к сбою вашей программы, если исходные и целевые типы имеют разные представления (например, целое число и тип с плавающей запятой) или даже если они имеют разные размеры. Например, если variable - это 16-разрядное целое число (например, тип short), а type - это 32-разрядный целочисленный тип, то в лучшем случае вы получите 32-разрядный результат, содержащий 16 бит мусора - тогда как простое преобразование значения дало бы вам математически правильный результат.

Форма литья указателя также может дать вам проблемы с выравниванием. Если variable выровнено по байтам, а type требует 2-байтного или 4-байтового выравнивания, например, вы можете получить поведение undefined, что может привести либо к результату мусора, либо к сбою программы. Или, что еще хуже, может показаться, что он работает (что означает, что у вас есть скрытая ошибка, которая может появиться позже и будет очень трудно отследить).

Вы можете проверить представление объекта, взяв его адрес и преобразовать его в unsigned char*; язык специально разрешает обработку любого объекта в виде массива типа символа.

Но если преобразование простого значения выполняет задание, то что вы должны использовать.

Если variable и type являются арифметическими, то, вероятно, не требуется; вы можете назначить выражение любого арифметического типа объекту любого арифметического типа, и преобразование будет выполнено неявно.

Здесь пример, где две формы имеют совершенно другое поведение:

#include <stdio.h>
int main(void) {
    float x = 123.456;

    printf("d = %g, sizeof (float) = %zu, sizeof (unsigned int) = %zu\n",
           x, sizeof (float), sizeof (unsigned int));
    printf("Value conversion: %u\n", (unsigned int)x);
    printf("Aliasing        : %u\n", *(unsigned int*)&x);
}

Вывод в моей системе (может отличаться от вашего):

d = 123.456, sizeof (float) = 4, sizeof (unsigned int) = 4
Value conversion: 123
Aliasing        : 1123477881

Ответ 4

Какая разница между "(type) variable" и "* ((type *) & variable)", если есть?

Второе выражение может привести к проблемам выравнивания и сглаживания.

Первая форма - естественный способ преобразования значения в другой тип. Но при условии, что нет никакого нарушения выравнивания или сглаживания, в некоторых случаях второе выражение имеет преимущество перед первой формой. *((type *)&variable) даст значение lvalue, тогда как (type)variable не даст lvalue (результат литья никогда не будет lvalue).

Это позволяет делать такие вещи, как:

(*((type *)& expr)))++

См., например, эту опцию из руководства Apple gcc, которое выполняет аналогичный трюк:

-fnon-lvalue-assign (ТОЛЬКО ДЛЯ APPLE): всякий раз, когда встречается выражение lvalue или условное выражение lvalue, компилятор выдает предупреждение об отказе а затем переписать выражение следующим образом:

  (type)expr                ---becomes--->      *(type *)&expr

  cond ? expr1 : expr2      ---becomes--->      *(cond ? &expr1 : &expr2)

Ответ 5

Использование указателя имеет значение при работе над структурой:

struct foo {
    int a;
};

void foo()
{
    int c;

    ((struct foo)(c)).a = 23;           // bad
    (*(struct foo *)(&c)).a = 42;       // ok
}

Ответ 6

Первый ((type)variable - простое литье переменной в желаемый тип, а второй (*(type*)&variable) - дешифрование указателя после того, как он был задан желаемым типом указателя.

Ответ 7

Разница в том, что во втором случае вы можете иметь поведение undefined. Причина, заключающаяся в том, что unsinged совпадает с unsigned int, а unsigned long может быть больше, чем unsigned int, а при нажатии на указатель, который вы разыскиваете, вы также читаете неинициализированную часть unsigned long.

Первый случай просто преобразует unsigned int в unsigned long с расширением unsigned int по мере необходимости.