Какая разница между "(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
по мере необходимости.