Недействительный указатель становится действительным снова

int *p;
{
    int x = 0;
    p = &x;
}
// p is no longer valid
{
    int x = 0;
    if (&x == p) {
        *p = 2;  // Is this valid?
    }
}

Доступ к указателю после освобождения вещи, на которую он указывает, был undefined, но что произойдет, если какое-то последующее распределение происходит в той же области, и вы явно сравниваете старый указатель с указателем на новую вещь? Было бы важно, если бы я использовал &x и p до uintptr_t, прежде чем сравнивать их?

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

Ответ 1

По моему пониманию стандарта (6.2.4. (2))

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

у вас есть поведение undefined при сравнении

if (&x == p) {

поскольку это соответствует этим пунктам, перечисленным в Приложении J.2:

- Используется значение указателя на объект, срок жизни которого закончился (6.2.4).
- Значение объекта с автоматической продолжительностью хранения используется, пока оно неопределено (6.2.4, 6.7.9, 6.8).

Ответ 2

Хорошо, это, кажется, интерпретируется как двухэтажный вопрос, который задают некоторые три вопроса.

Во-первых, были проблемы, если вообще использовать определение poitner для сравнения.

Как указано в комментариях, простым использованием указателя является UB, так как $J.2: говорит, что использование указателя на объект, срок жизни которого закончился, - UB.

Однако, если это препятствие пройдено (что хорошо в диапазоне UB, оно может работать в конце концов и будет на многих платформах), вот что я нашел о других проблемах:

Учитывая, что указатели действительно сравнивают значение, код действителен:

Стандарт C, §6.5.3.2.4:

[...] Если для указателя присвоено недопустимое значение, поведение унарного * оператора undefined.

Хотя сноска в этом месте явно говорит. что адрес объекта после окончания его жизни является недопустимым значением указателя, здесь это не применяется, поскольку if гарантирует, что значение указателя является адресом x и, следовательно, является действительным.

Стандарт С++, §3.9.2,3:

Если объект типа T расположен по адресу A, указатель типа cv T *, значение которого является адресом A, называется указывать на этот объект независимо от того, как это значение было получено. [Примечание. Например, адрес, расположенный за концом массива (5.7), будет считаться указывать на несвязанный объект типа элементов массива, который может быть расположен по этому адресу.

Акцент мой.

Ответ 3

Вероятно, он будет работать с большинством компиляторов, но он по-прежнему является undefined. Для языка C эти x являются двумя разными объектами, каждый из которых закончил свою жизнь, поэтому у вас есть UB.

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

В стандарте C говорится:

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

Обратите внимание, в частности, фраза "оба являются указателями на один и тот же объект". В смысле стандарта два "х" - это не один и тот же объект. Возможно, они реализуются в одном месте памяти, но это зависит от усмотрения компилятора. Поскольку они явно представляют собой два разных объекта, объявленных в разных областях, сравнение на самом деле никогда не будет истинным. Таким образом, оптимизатор вполне может полностью отключить эту ветвь.

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

{
    int x = 0;
    p = &x;
  BLURB: ;
}
...
if (...)
...
if (something) goto BLURB;

время жизни будет продолжаться до тех пор, пока область действия первого x достижима. Тогда все допустимое поведение, но ваш тест всегда будет ложным и оптимизирован достойным компилятором.

Из всего, что вы видите, лучше оставить его в аргументе для UB и не играть в такие игры в реальном коде.

Ответ 4

Это сработает, если по работе вы используете очень либеральное определение, примерно эквивалентное тому, что оно не сработает.

Однако это плохая идея. Я не могу представить ни одной причины, по которой проще скрестить пальцы и надеяться, что две локальные переменные хранятся в одном адресе памяти, чем писать p=&x снова. Если это всего лишь академический вопрос, то да, это действительный C, но независимо от того, является ли утверждение if истинным или нет, не гарантируется согласование между платформами или даже разными программами.

Изменить. Чтобы быть ясным, поведение undefined - это &x == p во втором блоке. Значение p не изменится, оно все еще является указателем на этот адрес, этот адрес больше не принадлежит вам. Теперь компилятор может (вероятно, будет) поместить второй x на тот же адрес (при условии, что нет другого промежуточного кода). Если это правда, это совершенно законно для разыменования p так же, как вы бы &x, если он type является указателем на int или чем-то меньшим. Точно так же, как законно говорить p = 0x00000042; if (p == &x) {*p = whatever;}.

Ответ 5

Поведение undefined. Однако ваш вопрос напоминает мне о другом случае, когда применялась несколько схожая концепция. В упомянутом случае были эти потоки, которые получали разное количество процессорных времен из-за их приоритетов. Итак, поток 1 получит немного больше времени, потому что поток 2 ожидает ввода-вывода или что-то в этом роде. Как только его работа была выполнена, поток 1 будет записывать значения в память для потока, потребляемого двумя. Это не "разделяет" память контролируемым образом. Он будет записывать сам вызывающий стек. Где переменным в потоке 2 будет выделена память. Теперь, когда поток 2 в конечном итоге завершался до исполнения, всем его объявленным переменным никогда не нужно было присваивать значения, потому что места, в которых они занимали, имели допустимые значения. Я не знаю, что они сделали, если что-то пошло не так в этом процессе, но это одна из самых адских оптимизаций в коде C, который я когда-либо видел.

Ответ 6

Отложите в сторону факт, если он действителен (и теперь я уверен, что это не так, см. Арне Мерц). Я все еще думаю, что это академический.

Алгоритм, о котором вы думаете, не принесет очень полезных результатов, так как вы могли бы сравнить только два указателя, но у вас нет шансов определить, указывают ли эти указатели на один и тот же объект или на нечто совершенно другое. Теперь указателем на struct может быть адрес одного char.

Ответ 7

Победитель # 2 в этом undefined конкурсе поведения скорее похож на ваш код:

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

int main() {
  int *p = (int*)malloc(sizeof(int));
  int *q = (int*)realloc(p, sizeof(int));
  *p = 1;
  *q = 2;
  if (p == q)
    printf("%d %d\n", *p, *q);
}

Согласно сообщению:

Использование последней версии Clang (r160635 для x86-64 в Linux):

$clang -O realloc.c;./a.out

1 2

Это можно объяснить только в том случае, если разработчики Clang считают, что этот пример и ваш, демонстрируют поведение undefined.