Можете ли вы когда-нибудь предположить, что типизация указателей безопасна?


Я слышал от многих людей, что вы не можете гарантировать, что типизация будет выполнена без потерь. Верно ли это только в том случае, если вы не знаете свой процессор, то есть вы не проверили количество байтов, используемых для ваших типов данных? Приведу пример:

Если вы выполните следующее:

typedef struct
{
    int i;
    char c;
    float f;
    double d;
} structure;

size_t voidPtrSz = sizeof(void *);
size_t charPtrSz = sizeof(char *);
size_t intPtrSz = sizeof(char *);
size_t floatPtrSz = sizeof(float *);
size_t doublePtrSz = sizeof(double *);
size_t structPtrSz = sizeof(structure *);
size_t funcPtrSz = sizeof(int (*)(float, char));

printf("%lu\n", voidPtrSz);
printf("%lu\n", charPtrSz);
printf("%lu\n", intPtrSz);
printf("%lu\n", floatPtrSz);
printf("%lu\n", doublePtrSz);
printf("%lu\n", structPtrSz);
printf("%lu\n", funcPtrSz);

... и выход следующий ...

4
4
4
4
4
4
4

Можно ли считать, что во всех случаях можно безопасно типизировать указатель определенного типа данных на указатель другого типа данных? Например, если выполняется следующим образом:

int foo(float, char)
{
}

void *bar(void)
{
    return (void *)foo;
}

int (*pFunc)(float, char) = bar();
Можете ли вы с уверенностью предположить, что pFunc имеет адрес foo?
3   4  

3 ответа:

Можно ли считать, что во всех случаях можно безопасно типизировать указатель определенного типа данных на указатель другого типа данных?

Любой указатель данных можно безопасно привести к char* или void*. Любой char* или void*, созданный таким образом, может быть возвращен к своему исходному типу. Любое другое приведение указателя данных приводит к неопределенному поведению при выполнении косвенного обращения к указателю.

Любой тип указателя функции может быть приведен к любому другому типу указателя функции, хотя вы не должны вызывать функция через неправильный тип. Приведение указателя функции к void* или любому другому типу указателя данных приводит к неопределенному поведению.

Верно ли это только в том случае, если вы не знаете свой процессор, то есть вы не проверили количество байтов, используемых для ваших типов данных?

Даже тогда ты не в безопасности. Когда стандарт C говорит, что конструкция имеет неопределенное поведение, авторы компилятора вольны обрабатывать конструкцию по своему усмотрению. В результате получается, что даже если вы думайте Вы знаете, что конструкция с UB будет обработана, потому что вы знаете целевой процессор, оптимизирующие компиляторы могут срезать углы и генерировать совсем другой код, чем вы ожидаете.

Что касается вашего конкретного примера кода, давайте обратимся к разделу 6.3.2.3 стандарта языка C99:

Указатель на void может быть преобразован в или из указателя на любой неполный или объектный тип. Указатель на любой неполный или объектный тип может быть преобразован в указатель на void и обратно; результат должен быть равен исходному указателю.

Обратите внимание, что указатель на функцию-это не то же самое, что указатель на объект. Единственное упоминание о указателе на функцию преобразования:

Указатель на функцию одного типа может быть преобразован в указатель на функцию другого типа и обратно; результат должен быть равен исходному указателю. Если преобразованный указатель используется для вызова функции, тип которой не совместим с типом, на который указывает указатель, поведение не определено.

Таким образом, ваш пример кода вызывает неопределенное поведение.

Если мы избегаем преобразований функции-указателя, то следующий абзац объясняет: все:

Указатель на объект или неполный тип может быть преобразован в указатель на другой объект или неполный тип. Если результирующий указатель не выровнен правильно для типа, на который указывают, поведение не определено. В противном случае при обратном преобразовании результат сравнивается с исходным указателем.

Примечание: преобразование между типами указателей является отдельной проблемой от преобразования и затем разыменование (В общем случае, это допустимо только при преобразовании в char * и последующем разыменовании.)

@Oli Charlesworth дает вам отличный ответ.

Я надеюсь, что смогу пролить немного света на то, что такое указатель, чтобы вы могли лучше понять механику указателя:

Указатель-это адрес. Этот адрес является адресом первого байта ваших данных. Тип указателя определяет, сколько байтов, начиная с этого первого байта, являются частью данных и как эти байты кодируют данные.

Например, на gcc x86, если у вас есть int * p, значение, удерживаемое p, сообщает о начале адрес данных, и тип p (int *) говорит, что по этому адресу он интерпретирует 4 байта (в малом конечном байтовом порядке) в дополнительном представлении числа со знаком два.

Указатель void * является "универсальным указателем". Указатель по-прежнему содержит адрес, но тип указателя не указывает, какие данные вы там найдете, или даже сколько байт образуют данные, поэтому вы никогда не сможете получить доступ к данным через указатель void *, но, как было сказано ранее, вы можете безопасно преобразовать указатель в void и указатель на любой неполный или объектный тип.

Указатель на функцию содержит адрес функции, а тип указателя указывает, как вызвать эту функцию (какие параметры и какого рода) и что функция возвращает.