Определено ли поведение вычитания двух указателей NULL? - PullRequest
75 голосов
/ 15 ноября 2011

Определена ли разница двух переменных, не являющихся пустыми указателями (для C99 и / или C ++ 98), если они оба NULL оценены?

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

struct buf {
  char *buf;
  char *pwrite;
  char *pread;
} ex;

Скажем, ex.buf указывает на массив или некоторую память, выделенную из памяти.Если мой код всегда гарантирует, что pwrite и pread указывают внутри этого массива или за ним, то я вполне уверен, что ex.pwrite - ex.pread всегда будет определен.Однако, что если pwrite и pread оба равны NULL.Могу ли я ожидать, что вычитание этих двух значений определено как (ptrdiff_t)0, или же в строго совместимом коде необходимо проверить указатели на NULL?Обратите внимание, что единственный интересующий меня случай - это когда оба указателя имеют значение NULL (что представляет собой буфер без инициализации).Причина связана с полностью совместимой «доступной» функцией, учитывая, что выполнены предыдущие предположения:

size_t buf_avail(const struct s_buf *b)
{     
    return b->pwrite - b->pread;
}

Ответы [ 4 ]

96 голосов
/ 15 ноября 2011

В C99 это технически неопределенное поведение. C99 §6.5.6 говорит:

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

[...]

9) Когда вычитаются два указателя, оба должны указывать на элементы одного и того же объекта массива, или один за последним элементом массива объекта; Результатом является разница индексы двух элементов массива. [...]

И в §6.3.2.3 / 3 говорится:

Целочисленное константное выражение со значением 0 или такое выражение, приведенное к типу void *, называется константой нулевого указателя. 55) Если константа нулевого указателя преобразуется в тип указателя, результирующий указатель, называемый нулевым указателем , гарантированно сравнивается не соответствует указателю на какой-либо объект или функцию.

Таким образом, поскольку нулевой указатель является неравным для любого объекта, он нарушает предварительные условия 6.5.6 / 9, поэтому его поведение не определено. Но на практике я был бы готов поспорить, что почти каждый компилятор выдаст результат 0 без каких-либо побочных эффектов.

В C89 это также неопределенное поведение, хотя формулировка стандарта немного отличается.

C ++ 03, с другой стороны, имеет определенное поведение в этом случае. Стандарт делает специальное исключение для вычитания двух нулевых указателей. C ++ 03 §5.7 / 7 говорит:

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

C ++ 11 (а также последний черновик C ++ 14, n3690) имеют идентичную формулировку C ++ 03, с небольшим изменением std::ptrdiff_t вместо ptrdiff_t.

36 голосов
/ 15 ноября 2011

Я нашел это в стандарте C ++ (5.7 [expr.add] / 7):

Если два указателя [...] оба равны нулю, а два указателя вычтены,результат сравнивается равным значению 0, преобразованному в тип std :: ptrdiff_t

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

23 голосов
/ 15 ноября 2011

Редактировать : Этот ответ действителен только для C, я не видел тег C ++, когда отвечал.

Нет, арифметика указателей разрешена только для указателей, которые указывают в пределахтот же объект.Так как по определению стандартные нулевые указатели C не указывают на какой-либо объект, это неопределенное поведение.

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

1 голос
/ 12 апреля 2016

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

В любой соответствующей реализации C и почти во всех (если не во всех) реализациях C-подобных диалектов для любого указателя p будут соблюдаться следующие гарантии, так что *p или *(p-1) идентифицирует некоторый объект:

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

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

Если бы была реализация, в которой затраты на поддержание гарантий были бы велики, но немногие, если какие-либо программы получили бы от них какую-либо выгоду, имело бы смысл позволить ей отлавливать вычисления «ноль + ноль» и требовать, чтобыПользовательский код для такой реализации включает в себя ручные проверки на нуль, которые гарантии могли бы сделать ненужными.Ожидается, что такой резерв не повлияет на другие 99,44% реализаций, где стоимость поддержки гарантий будет превышать стоимость.Такие реализации должны поддерживать такие гарантии, но их авторам не нужно, чтобы авторы стандарта сообщали им об этом.

Авторы C ++ решили, что соответствующие реализации должны поддерживать вышеуказанные гарантии любой ценой, даже наплатформы, где они могут существенно снизить производительность арифметики указателей.Они посчитали, что стоимость гарантий даже на платформах, где их было бы дорого поддерживать, превысила бы стоимость.На такое отношение могло повлиять желание обращаться с C ++ как с языком более высокого уровня, чем с C. Можно ожидать, что программист AC узнает, когда конкретная целевая платформа будет обрабатывать случаи типа (ноль + ноль) необычным образом, но программисты C ++не ожидалось, что они будут заниматься такими вещами.Таким образом, гарантирование согласованной поведенческой модели было признано оправданным.

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

void out_characters(unsigned char *p, int n)
{
  unsigned char *end = p+n;
  while(p < end)
    out_byte(*p++);
}

, старые компиляторы генерировали бы код, который надежно ничего не выводил бы без побочных эффектов,если p == NULL и n == 0, то нет необходимости в особом случае n == 0.Однако на более новых компиляторах придется добавить дополнительный код:

void out_characters(unsigned char *p, int n)
{
  if (n)
  {
    unsigned char *end = p+n;
    while(p < end)
      out_byte(*p++);
  }
}

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

...