Определено ли поведение вычитания двух указателей NULL?

Является ли разница двух переменных невоичного указателя (на C99 и/или С++ 98), если они оба NULL value?

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

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

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

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

Ответ 1

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

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

[...]

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

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

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

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

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

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

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

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

Ответ 2

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

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

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

Ответ 3

Изменить: этот ответ действителен только для C, я не видел тег С++, когда я отвечал.

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

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

Ответ 4

Стандарт 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 "не может быть пустым", любые последующие проверки нулевого указателя могут быть опущены, что приводит к тому, что код разбивается на пятно, не связанное с реальной "проблемой".