Печать нулевых указателей с% p - это поведение undefined?

Является ли поведение undefined для печати нулевых указателей спецификатором преобразования %p?

#include <stdio.h>

int main(void) {
    void *p = NULL;

    printf("%p", p);

    return 0;
}

Вопрос относится к стандарту C, а не к реализациям C.

Ответ 1

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


Код в вопросе показывает четко определенное поведение.

Поскольку [7.1.4] лежит в основе вопроса, начинаем там:

Каждое из следующих утверждений применяется, если явно не указано иное в следующих подробных описаниях: Если аргумент функции имеет недопустимое значение (, например), значение вне домена функции или указатель за пределами адресного пространства программы, или нулевой указатель, [... другие примеры...]) [...] поведение undefined. [... другие заявления...]

Это неуклюжий язык. Одна из интерпретаций заключается в том, что элементы в списке являются UB для всех функций библиотеки, если они не переопределены отдельными описаниями. Но список начинается с "например", указывая, что он иллюстративный, а не исчерпывающий. Например, в нем не упоминается правильное нулевое завершение строк (критическое для поведения, например, strcpy).

Таким образом, ясно, что цель/область применения 7.1.4 состоит в том, что "недопустимое значение" приводит к UB (если не указано иное). Мы должны посмотреть на каждое описание функции, чтобы определить, что считается "недопустимым значением".

Пример 1 - strcpy

[7.21.2.3] говорит только следующее:

Функция strcpy копирует строку, на которую указывает s2 (включая завершающий нулевой символ), в массив, на который указывает s1. Если копирование происходит между перекрывающимися объектами, поведение undefined.

В нем нет явного упоминания о нулевых указателях, но он не упоминает и о нулевых терминаторах. Вместо этого из "string, на который указывает s2", следует, что единственными допустимыми значениями являются строки (т.е. Указатели на массивы символов с нулевым символом).

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

  • [7.6.4.1 (fenv)] сохраните текущую среду с плавающей запятой в объекте , на который указывает, на envp

  • [7.12.6.4 (frexp)] сохраните целое число в объекте int , на которое указывает, на exp

  • [7.19.5.1 (fclose)] поток , указывающий на на stream

Пример 2 - printf

[7.19.6.1] говорит об %p:

p - Аргумент должен быть указателем на void. Значение указателя преобразуется в последовательность символов печати в соответствии с реализацией.

Null - это действительное значение указателя, и в этом разделе явно не упоминается, что null является частным случаем, и что указатель должен указывать на объект. Таким образом, определяется поведение.


<суб > 1. Если автор стандартов не выйдет вперед, или если мы не сможем найти что-то похожее на документ rationale, который разъясняет вещи.

Ответ 2

Короткий ответ

Да. Печать нулевых указателей с помощью спецификатора преобразования %p имеет поведение undefined. Сказав это, я не знаю о какой-либо существующей соответствующей реализации, которая будет плохо себя вести.

Ответ применяется к любому из стандартов C (C89/C99/C11).


Длительный ответ

Спецификатор преобразования %p ожидает аргумент указателя типа на void, преобразование указателя на печатные символы определяется реализацией. Он не указывает, что ожидается нулевой указатель.

Введение в стандартные функции библиотеки гласит, что нулевые указатели в качестве аргументов (стандартные библиотеки) считаются недопустимыми значениями, если это явно не указано иначе.

C99/C11 §7.1.4 p1

[...] Если аргумент функции имеет недопустимое значение (например, [...] нулевой указатель, [...] поведение undefined.

Примеры для (стандартной библиотеки) функций, которые ожидают нулевые указатели в качестве допустимых аргументов:

  • fflush() использует пустой указатель для очистки всех потоков (которые применяются).
  • freopen() использует нулевой указатель для указания файла, "связанного в настоящее время" с потоком.
  • snprintf() позволяет передавать нулевой указатель, когда "n" равно нулю.
  • realloc() использует нулевой указатель для размещения нового объекта.
  • free() позволяет передавать нулевой указатель.
  • strtok() использует нулевой указатель для последующих вызовов.

Если мы возьмем случай для snprintf(), имеет смысл разрешить передачу нулевого указателя, когда "n" равно нулю, но это не относится к другим (стандартной библиотеке) функциям, которые допускают аналогичный нуль n, Например: memcpy(), memmove(), strncpy(), memset(), memcmp().

Он не только указан во введении к стандартной библиотеке, но еще раз во введении к этим функциям:

C99 §7.21.1 p2/C11 §7.24.1 p2

Если аргумент, объявленный как size_t n, задает длину массива для функции, n может иметь значение 0 при вызове этой функции. Если явно не указано иначе в описании конкретной функции в этом подпункте, аргументы указателя на такой вызов все равно должны иметь допустимые значения, как описано в 7.1.4.


Это намеренно?

Я не знаю, действительно ли UB %p с нулевым указателем преднамерен, но поскольку стандарт явно указывает, что нулевые указатели считаются недопустимыми значениями в качестве аргументов для стандартных функций библиотеки, а затем он идет и явно задает случаи, когда нулевой указатель является допустимым аргументом (snprintf, free и т.д.), а затем он идет и снова повторяет требование, чтобы аргументы были действительными даже в нулевых случаях (memcpy, memmove, memset), то я считаю разумным предположить, что комитет по стандартизации C не слишком озабочен такими вещами undefined.

Ответ 3

Авторы Стандарта C не прилагали никаких усилий, чтобы исчерпывающе перечислить все поведенческие требования, которые должна выполнить встреча, подходящая для какой-либо конкретной цели. Вместо этого они ожидали, что люди, пишущие компиляторы, будут обладать определенным здравым смыслом, требует ли это Стандарт или нет.

Вопрос о том, что-то вызывает UB, редко сам по себе полезен. Реальные вопросы важности:

  • Если кто-то, кто пытается написать качественный компилятор, заставляет его вести себя предсказуемым образом? Для описанного сценария ответ явно да.

  • Должны ли программисты иметь право ожидать, что качественные компиляторы для чего-либо, похожего на обычные платформы, будут вести себя предсказуемым образом? В описанном сценарии я бы сказал, что ответ "да".

  • Могут ли некоторые тупые авторы компилятора растянуть интерпретацию Стандарта, чтобы оправдать что-то странное? Надеюсь, что нет, но не исключаю.

  • Должна ли систематизировать компиляторы в отношении поведения? Это будет зависеть от уровня паранойи их пользователей; санитажный компилятор, вероятно, не должен дефолтно проклинать о таком поведении, но, возможно, предоставить возможность конфигурации, если программы могут быть перенесены на "умные" /немые компиляторы, которые ведут себя странно.

Если разумная интерпретация Стандарта будет означать, что поведение определено, но некоторые авторы компилятора растягивают интерпретацию, чтобы оправдать обратное, действительно ли имеет значение то, что говорит стандарт?