На самом деле вы правы и неправы одновременно.
В C есть возможность безопасно типизировать любое lvalue к любому lvalue.Однако синтаксис немного отличается от вашего прямого подхода:
lvalue-указатели могут быть преобразованы в lvalue-указатели другого типа , например, в C:
char *ptr;
ptr = malloc(20);
assert(ptr);
*(*(int **)&ptr)++ = 5;
Поскольку malloc()
требуется для выполнения всех требований выравнивания, это также является приемлемым использованием.Однако следующее не является переносимым и может привести к исключению из-за неправильного выравнивания на некоторых машинах:
char *ptr;
ptr = malloc(20);
assert(ptr);
*ptr++ = 0;
*(*(int **)&ptr)++ = 5; /* can throw an exception due to misalignment */
Подводя итог:
- Если вы приведете указатель, этоприводит к r-значению.
- Использование
*
для указателя приводит к l-значению (*ptr
может быть назначено). ++
(как в *(arg)++
) требуетсяlvalue для работы (arg
должно быть lvalue)
Следовательно, ((int *)ptr)++
терпит неудачу, потому что ptr
является lvalue, а (int *)ptr
- нет.++
можно переписать как ((int *)ptr += 1, ptr-1)
, и это (int *)ptr += 1
, который завершается неудачно из-за приведения, приводящего к чистому значению.
Обратите внимание, что это не является языковым недостатком.Литье не должно производить lvalues.Посмотрите на следующее:
(double *)1 = 0;
(double)ptr = 0;
(double)1 = 0;
(double *)ptr = 0;
Первые 3 не компилируются.Зачем кому-то ожидать, что 4-я строка будет скомпилирована?Языки программирования никогда не должны демонстрировать такое удивительное поведение.Более того, это может привести к неясному поведению программ.Подумайте:
#ifndef DATA
#define DATA double
#endif
#define DATA_CAST(X) ((DATA)(X))
DATA_CAST(ptr) = 3;
Это не скомпилируется, верно?Однако, если ваши ожидания оправдались, это неожиданно компилируется с cc -DDATA='double *'
!С точки зрения стабильности важно не вводить такие контекстные l-значения для определенных приведений.
Правильно для C то, что есть либо l-значения, либо их нет, и это не должно зависеть от некоторого произвольного контекста.что может быть удивительным.
Как отметил Дженс , уже есть один оператор для создания lvalue.Это оператор разыменования указателя, «унарный *
» (как в *ptr
).
Обратите внимание, что *ptr
можно записать как 0[ptr]
, а *ptr++
можно записать как 0[ptr++]
,Индексы массива являются lvalue, поэтому *ptr
также является lvalue.
Подождите, что?0[ptr]
должно быть ошибкой, верно?
На самом деле, нет.Попытайся!Это допустимо C. Следующая C-программа действительна на 32/64-битной Intel во всех отношениях, поэтому она компилируется и успешно работает:
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
int
main()
{
char *ptr;
ptr = malloc(20);
assert(ptr);
0[(*(int **)&ptr)++] = 5;
assert(ptr[-1]==0 && ptr[-2]==0 && ptr[-3]==0 && ptr[-4]==5);
return 0;
}
В C мы можем иметь и то, и другое.Приведения, которые никогда не создают lvalues.И возможность использовать приведение таким образом, чтобы мы могли поддерживать свойство lvalue в живых.
Но чтобы получить lvalue из приведения, необходимы еще два шага:
- Передбросьте, получите адрес оригинального lvalue.Поскольку это lvalue, вы всегда можете получить этот адрес.
- Приведение к указателю нужного типа (обычно желаемый тип также является указателем, поэтому у вас есть указатель на этот указатель).
- После приведения разыменуйте этот дополнительный указатель, который снова даст вам lvalue.
Следовательно, вместо неправильного *((int *)ptr)++
мы можем написать *(*(int **)&ptr)++
.Это также гарантирует, что ptr
в этом выражении уже должно быть lvalue.Или написать это с помощью препроцессора C:
#define LVALUE_CAST(TYPE,PTR) (*((TYPE *)&(PTR)))
Так что для любого переданного в void *ptr
(который может маскироваться под char *ptr
), мы можем написать:
*LVALUE_CAST(int *,ptr)++ = 5;
За исключением обычных арифметических предостережений с указателями (ненормальное завершение программы или неопределенное поведение на несовместимых типах, что в основном связано с проблемами aligment), это правильный C.