Является ли 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 и сломанный хорошо написанный существующий код, который использует указатели функций или непротетируемые функции.