Является ли fgets() возвратом NULL с коротким буфером?

В модульном тестировании функция, содержащая fgets(), встретила неожиданный результат при размере буфера n < 2. Очевидно, что такой размер буфера является глупым, но тест исследует угловые случаи.

Упрощенный код:

#include <error.h>
#include <stdio.h>

void test_fgets(char * restrict s, int n) {
  FILE *stream = stdin;
  s[0] = 42;
  printf("< s:%p n:%d stream:%p\n", s, n, stream);
  char *retval = fgets(s, n, stream);
  printf("> errno:%d feof:%d ferror:%d retval:%p s[0]:%d\n\n",
    errno, feof(stream), ferror(stream), retval, s[0]);
}

int main(void) {
  char s[100];
  test_fgets(s, sizeof s);  // Entered "123\n" and works as expected
  test_fgets(s, 1);         // fgets() --> NULL, feof() --> 0, ferror() --> 0
  test_fgets(s, 0);         // Same as above
  return 0;
}

Удивительно, что fgets() возвращает NULL, и ни feof(), ни ferror() не являются 1.

В этом редком случае, как показано ниже, C spec кажется тихим.

Вопросы:

  • Возвращает NULL без установки feof() и ferror() совместимого поведения?
  • Может ли другой результат быть совместимым поведением?
  • Имеет ли значение, если n равно 1 или меньше 1?

Платформа: версия gcc 4.5.3 Цель: i686-pc-cygwin

Вот аннотация из стандарта C11, некоторые мои замечания:

7.21.7.2 Функция fgets

Функция fgets читает не более, чем число символов, указанное n [...]

Функция fgets возвращает s в случае успеха. Если конец файла встречается и в массив не считываются символы, содержимое массива остается неизменным и возвращается нулевой указатель. Если во время операции возникает ошибка чтения, содержимое массива является неопределенным и возвращается нулевой указатель.

Связанные публикации
Как использовать feof и ferror для fgets (minishell in C)
Проблема с созданием оболочки в C (Seg-Fault и ferror)
функции fputs(), fgets(), ferror() и эквиваленты С++
Возвращаемое значение fgets()


[Изменить] Комментарии к ответам

@Shafik Yaghmour хорошо представил общую проблему: поскольку спецификация C не упоминает, что делать, когда она не читает никаких данных и не записывает никаких данных в s, когда (n <= 0), это Undefined Поведение, Поэтому любой разумный ответ должен быть приемлемым, например return NULL, не устанавливать флаги, оставлять буфер самостоятельно.

Что касается того, что должно произойти, когда n==1, ответ @Oliver Matthews и комментарий @Matt McNabb указывают на отсутствие ясности C spec с учетом буфера n == 1. Спектр C, похоже, поддерживает буфер n == 1, который должен возвращать указатель буфера с помощью s[0] == '\0', но не достаточно явным.

Ответ 1

В новых версиях glibc поведение отличается от glibc, оно возвращает s, что указывает на успех, это не необоснованное чтение 7.19.7.2 Функция абзаца fgets 2, которая гласит (это как на C99, так и на C11, акцент мой):

char * fgets (char * ограничивает s, int n, поток ограничения FILE *);

Функция fgets читает не более, чем число символов, указанных nиз потока, направленного потоком в массив, на который указывает s. Нет дополнительных символы читаются после символа новой строки (который сохраняется) или после окончания файла. Нулевой символ записывается сразу после того, как последний символ считывается в массив.

Не очень полезно, но не нарушает ничего, что указано в стандарте, оно будет читать не более 0 символов и нуль-конец. Таким образом, результаты, которые вы видите, выглядят как ошибка, которая была исправлена ​​в последующих выпусках glibc. Это также явно не конец файла или ошибка чтения, как описано в пункте 3:

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

Что касается финального случая, когда n == 0 выглядит так, это просто поведение undefined. В проекте стандартного раздела C99 4. Соответствие параграфа 2 гласит (акцент мой):

Если требование '' должно или '' не должно требовать, чтобы внешнее ограничение было нарушено, поведение undefined. Undefined поведение в противном случае указано в этом международном стандарте словами "undefined поведение " или отсутствием какого-либо явного определения поведения. В этих трех различиях нет разницы; все они описывают поведение 'undefined.

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

Ответ 2

tl; dr:, что версия glibc имеет ошибку для n = 1, спецификация имеет (возможно) двусмысленность для n < 1; но я думаю, что более новый glibc использует наиболее разумный вариант.

Итак, спецификация c99 в основном одинакова.

Поведение для test_fgets(s, 1) неверно. glibc 2.19 дает правильный результат (retval!=null, s[0]==null.

Поведение для test_fgets(s,0) действительно undefined. Это не удалось (вы не можете прочитать не более -1 символов), но он не попадает ни в один из двух критериев "return null" (EOF & 0 read, read error).

Однако поведение GCC возможно правильно (возврат указателя к неизменному s тоже будет ОК) - feof не установлен, потому что он не ударил eof; ferror не установлен, потому что не было ошибки чтения.

Я подозреваю, что логика в gcc (не полученная от источника) имеет "if n <= 0 return null" в верхней части.

[править:]

При отражении я на самом деле думаю, что поведение glibc для n=0 является наиболее правильным ответом, который он может дать:

  • Нет чтения eof, поэтому feof()==0
  • Не читается, поэтому ошибки чтения не было, поэтому ferror=0

Теперь что касается возвращаемого значения  - fgets не может прочитать -1 символов (это невозможно). Если fgets вернул переданный в указатель, это будет выглядеть как успешный вызов.  - Игнорируя этот угловой случай, fgets обязуется возвращать строку с нулевым завершением. Если бы в этом случае этого не было, вы не могли бы положиться на это. Но fgets установит символ после того, как последний символ прочитает в массив значение null. учитывая, что мы читаем в -1 символах (apparantly) на этом вызове, который заставит его установить 0-й символ равным нулю?

Итак, самый правильный выбор - вернуть null (на мой взгляд).

Ответ 3

Стандарт C (проект C11 n1570) определяет fgets() таким образом (некоторые мои замечания):

7.21.7.2 Функция fgets

Сводка

   #include <stdio.h>
   char *fgets(char * restrict s, int n,
               FILE * restrict stream);

Описание

Функция fgets считывает не более, чем число символов, указанных n из потока, на который указывает stream, в массив, на который указывает s. Никакие дополнительные символы не читаются после символа новой строки (который сохраняется) или после окончания файла. Нулевой символ записывается сразу после последнего символа, который считывается в массив.

Возвращает

Функция fgets возвращает s в случае успеха. Если конец файла встречается и в массив не считываются символы, содержимое массива остается неизменным и возвращается нулевой указатель. Если во время операции возникает ошибка чтения, содержимое массива неопределенно и возвращается нулевой указатель.

Фраза читает не более, чем количество символов, указанных n, недостаточно точной. Отрицательное число не может представлять * количество символов **, но 0 означает отсутствие символов. чтение не более -1 символов не представляется возможным, поэтому случай n <= 0 не указан.

Для n = 1, fgets указывается как чтение не более 0 символов, что должно быть успешным, если поток недействителен или в состоянии ошибки. Фраза Нулевой символ записывается сразу после того, как последний символ, прочитанный в массив, неоднозначен, поскольку никакие символы не были прочитаны в массиве, но имеет смысл интерпретировать это значение как значение s[0] = '\0';. Спецификация для gets_s предлагает такое же чтение с той же неточностью.

Спецификация snprintf более точно, случай n = 0 явно указан, при этом добавлена ​​полезная семантика. К сожалению, такая семантика не может быть реализована для fgets:

7.21.6.5 Функция snprintf

Сводка

#include <stdio.h>
int snprintf(char * restrict s, size_t n,
     const char * restrict format, ...);

Описание

Функция snprintf эквивалентна fprintf, за исключением того, что вывод записывается в массив (заданный аргументом s), а не в поток. Если n равно нулю, ничего не записывается, а s может быть нулевым указателем. В противном случае выходные символы за пределами n-1 st отбрасываются, а не записываются в массив, а нулевой символ записывается в конце символов, фактически записанных в массив. Если копирование происходит между перекрывающимися объектами, поведение undefined.

Спецификация для get_s() также разъясняет случай n = 0 и делает это нарушением ограничения времени выполнения:

K.3.5.4.1 Функция gets_s

Сводка

#define __STDC_WANT_LIB_EXT1__ 1
#include <stdio.h>
char *gets_s(char *s, rsize_t n);

<сильные > Runtime-ограничения

s не должен быть нулевым указателем. n не должно быть равно нулю и не больше, чем RSIZE_MAX. При чтении n-1 символов из stdin должен произойти символ новой строки, конец файла или ошибка чтения.

Если существует нарушение ограничения времени выполнения, s[0] устанавливается в нулевой символ, а символы считываются и отбрасываются из stdin до тех пор, пока не будет прочитан символ новой строки или конец файла или прочитанный возникает ошибка.

Описание

Функция gets_s считывает не более чем число символов, указанных n из потока, на которое указывает stdin, в массив, на который указывает s. Никакие дополнительные символы не читаются после символа новой строки (который отбрасывается) или после окончания файла. Отброшенный символ новой строки не учитывается в количестве прочитанных символов. Нулевой символ записывается сразу после последнего символа, который считывается в массив.

Если встречается конец файла и не считываются символы в массиве или если во время операции возникает ошибка чтения, тогда s[0] устанавливается на нулевой символ, а остальные элементы s введите неопределенные значения.

Рекомендуемая практика

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

Возвращает

Функция gets_s возвращает s в случае успеха. Если произошло нарушение ограничения времени выполнения или встречается конец файла, и никакие символы не были прочитаны в массиве, или если во время операции возникает ошибка чтения, возвращается нулевой указатель.

Библиотека C, которую вы тестируете, кажется, имеет ошибку для этого случая, которая была исправлена ​​в более поздних версиях glibc. Возврат NULL должен означать какое-либо условие отказа (противоположное успеху): конец файла или ошибка чтения. Другие случаи, такие как неверный поток или поток, не открытый для чтения, более или менее явно описаны как поведение undefined.

Случай n = 0 и n < 0 не задан. Возвращение NULL является разумным выбором, но было бы полезно прояснить описание fgets() в Стандарте, чтобы требовать n > 0, как в случае gets_s.

Обратите внимание, что существует еще одна проблема спецификации для fgets: тип аргумента n должен быть size_t вместо int, но эта функция была первоначально указана авторами C до size_t даже придумал и не изменился в первом стандарте C (C89). Изменение его тогда считалось неприемлемым, поскольку они пытались стандартизировать существующее использование: изменение подписи создало бы несогласованности в библиотеках C и сломанный хорошо написанный существующий код, который использует указатели функций или непротетируемые функции.