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

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

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

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

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

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

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

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

Edit:

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

Edit2:

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

Ответ 1

В принципе:

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

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

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

Ответ 2

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

Кроме того, существует четыре случая, когда литье указателя дает результаты, определенные реализацией:

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

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

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

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

Ответ 3

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

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

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

Одно действие, которое должно всегда срабатывать, должно быть (unsigned char*). Через такой указатель вы можете затем исследовать отдельные байты вашего объекта.

Ответ 4

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

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

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

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

Я не думаю, что авторы Стандарта предполагали, что такие операции могут потерпеть неудачу на платформах, где такие конверсии ничего не стоят. Они описали Дух С в уставе и логических документах, включая принцип "Не мешайте программисту (или не нужно) препятствовать выполнению того, что нужно сделать". Учитывая этот принцип, не будет необходимости в том, чтобы Стандарт налагал мандат на то, что реализации выполняют действия таким образом, чтобы помочь программистам выполнить то, что им нужно делать в случаях, когда это будет стоить ничего, поскольку реализации, которые делают добросовестную попытку отстоять Дух C будет вести себя таким образом с мандатом или без него.

Ответ 5

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

Что-то вроде следующего ниже, приведет к поведению undefined:

#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;
}

EDIT: Что, кстати, сказал Оли о указателях void *. Вы можете указать между любым указателем void и другим указателем.