Когда приведение между типами указателей не неопределенное поведение в C? - PullRequest
31 голосов
/ 27 января 2011

Будучи новичком в C, я запутался, когда приведение указателя в порядке.

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

int a = 5;
int* intPtr = &a;
char* charPtr = (char*) intPtr; 

Однако, в общем, это вызывает неопределенное поведение (хотя это работает на многих платформах). При этом, похоже, есть некоторые исключения:

  • вы можете свободно разыгрывать void* (?)
  • вы можете свободно разыгрывать char* (?)

(по крайней мере, я видел это в коде ...).

Итак, какие приведения между типами указателей не неопределенное поведение в C?

Edit:

Я пытался заглянуть в стандарт C (раздел "6.3.2.3 Указатели", по адресу http://c0x.coding -guidelines.com / 6.3.2.3.html ), но на самом деле не понял его, кроме от бита около void*.

Edit2:

Просто для пояснения: я явно спрашиваю только о «нормальных» указателях, то есть , а не о указателях функций. Я понимаю, что правила для наведения указателей на функции очень ограничительны. На самом деле, я уже спрашивал об этом :-): Что произойдет, если я приведу указатель на функцию, изменив число параметров

Ответы [ 5 ]

27 голосов
/ 27 января 2011

В основном:

  • T * может быть свободно преобразовано в void * и обратно (где T * не является указателем функции), и вы получите исходный указатель.
  • a T * может быть свободно преобразовано в U * и обратно (где T * и U * не являются функциональными указателями), и вы получите исходный указатель, если требования выравниваниятак же.Если нет, поведение не определено.
  • указатель на функцию может быть свободно преобразован в любой другой тип указателя на функцию и обратно, и вы получите исходный указатель.

Примечание: T * (для нефункциональных указателей) всегда удовлетворяет требованиям выравнивания для char *.

Внимание: Ни одно из этих правил не говорит что-нибудь о том, что произойдет, если вы преобразуете, скажем, T * в U *, а затем попытаетесь разыменовать его.Это совершенно другая область стандарта.

9 голосов
/ 27 января 2011

Отличный ответ Оли Чарльзуорта перечисляет все случаи, когда приведение указателя к указателю другого типа дает четко определенный результат.

Кроме того, в четырех случаях приведение указателя дает реализацию -defined results:

  • Вы можете привести указатель к достаточно большому (!) целочисленному типу.C99 имеет дополнительные типы intptr_t и uintptr_t для этой цели.Результат определяется реализацией.На платформах, которые обращаются к памяти как к непрерывному потоку байтов («линейная модель памяти», используемая большинством современных платформ), она обычно возвращает числовое значение адреса памяти, на который указывает указатель, таким образом, просто число байтов.Однако не все платформы используют линейную модель памяти, поэтому это определяется реализацией: -).
  • И наоборот, вы можете привести целое число к указателю.Если целое число имеет тип, достаточно большой для intptr_t или uintptr_t, и было создано путем приведения указателя, приведение его к тому же типу указателя вернет вам этот указатель (который, однако, может быть недействительным).В противном случае результат определяется реализацией.Обратите внимание, что на самом деле разыменование указателя (в отличие от простого чтения его значения) все еще может быть UB.
  • Вы можете привести указатель на любой объект к char*.Затем результат указывает на младший адресуемый байт объекта, и вы можете прочитать оставшиеся байты объекта, увеличив указатель до размера объекта.Конечно, значения, которые вы на самом деле получаете, снова определяются реализацией ...
  • Вы можете свободно приводить нулевые указатели, они всегда будут оставаться нулевыми указателями независимо от типа указателя: -).

Источник: стандарт C99, разделы 6.3.2.3 «Указатели» и 7.18.1.4 «Целочисленные типы, способные содержать указатели объектов».

Насколько я могу судить, вседругие приведения указателя к указателю другого типа - неопределенное поведение.В частности, если вы не приводите к char или достаточно большому целочисленному типу, он может всегда быть UB для приведения указателя к другому типу указателя - даже без разыменования его.

Это связано с тем, что типы могут иметь различное выравнивание, и не существует общего переносимого способа убедиться, что разные типы имеют совместимое выравнивание (за исключением некоторых особых случаев, таких как пары целочисленных типов со знаком / без знака).

5 голосов
/ 27 января 2011

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

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

Для других приведений между типами объектов нет гарантии, что доступ к объекту через такой указатель может вызвать всевозможные проблемы, такие как выравнивания (ошибка шины), представление ловушек целых чисел. Различные типы указателей даже не гарантируют одинаковую ширину, поэтому теоретически вы можете даже потерять информацию.

Один бросок, который всегда должен работать, - это (unsigned char*). С помощью такого указателя вы можете исследовать отдельные байты вашего объекта.

0 голосов
/ 03 декабря 2018

Авторы Стандарта не предпринимали попыток взвесить затраты и выгоды от поддержки преобразований среди большинства комбинаций типов указателей на платформах, где такая поддержка будет дорогой, поскольку:

  1. Большинство платформ, где такие преобразования были бы дороги, вероятно, были бы неясными, о которых авторы Стандарта не знали.

  2. Люди, использующие такие платформы, были бы лучше, чем авторы стандарта, с затратами и преимуществами такой поддержки.

Если какая-то конкретная платформа использует другое представление для int* и double*, я думаю, что Стандарт намеренно учел бы вероятность того, что, например, Круглое преобразование из double* в int* и обратно в double* будет работать согласованно, но преобразование из int* в double* и обратно в int* может завершиться неудачно.

Я не думаю, что авторы Стандарта предполагали, что такие операции могут потерпеть неудачу на платформах, где такие преобразования ничего не стоят. Они описали Дух C в уставных и обосновательных документах как включающий принцип «Не препятствуйте [или не создавайте ненужных препятствий] программисту делать то, что нужно сделать». Принимая во внимание этот принцип, у Стандарта не будет необходимости предписывать реализациям обрабатывать действия таким образом, чтобы помочь программистам выполнять то, что им нужно делать, в тех случаях, когда это будет стоить ничего, поскольку реализации, которые прилагают добросовестные усилия для поддержки Дух C будет вести себя таким образом с мандатом или без него.

0 голосов
/ 27 января 2011

Это неопределенное поведение, когда вы приводите к типу с другим размером. Например, приведение от char к int. Длина символа составляет 1 байт. Целые числа имеют длину 4 байта (в 32-битной системе Linux). Так что если у вас есть указатель на символ и вы приведете его к указателю на int, это вызовет неопределенное поведение . Надеюсь, это поможет.

Что-то вроде следующего ниже, может вызвать неопределенное поведение:

#include <stdio.h>
#include <stdlib.h>

int main() {

    char *str = "my str";
    int *val;

    val = calloc(1, sizeof(int));
    if (val == NULL) {
        exit(-1);
    }
    *val = 1;

    str = (char) val;

    return 0;
}

РЕДАКТИРОВАТЬ: Между прочим, Оли сказал о пустых * указатели правильно. Вы можете привести между любым указателем void и другим указателем.

...