нужен последний '\0' в fgets

Я видел несколько случаев использования fgets (например, здесь), которые выглядят следующим образом:

char buff[7]="";

(...)

fgets(buff, sizeof(buff), stdin);

Интерес заключается в том, что если я предоставлю длинный ввод, например «aaaaaaaaaaaa», fgets урежет его до «aaaaaa», потому что 7-й символ будет использоваться для хранения '\0'.

Однако при этом:

int i=0;
for (i=0;i<7;i++)
{
    buff[i]='a';
}
printf("%s\n",buff);

Я всегда буду получать 7 'a's, и программа не будет падать. Но если я попытаюсь написать 8 'a's, это будет.

Как я понял позже, причина этого в том, что, по крайней мере, в моей системе, когда я выделяю char buff[7]="" или без него), 8-й байт (считая с 1, а не с 0) устанавливается в 0. Из того, что я думаю, все сделано именно так, чтобы цикл for с 7 операциями записи, за которым следует чтение в формате строки, мог быть успешным, независимо от того, был ли последний записываемый символ '\0' или нет, и, таким образом, программисту не нужно было устанавливать сам последний '\0' при написании символов по отдельности.

Отсюда следует, что в случае

fgets(buff, sizeof(buff), stdin);

а затем предоставить слишком длинный ввод, результирующая buffstring автоматически будет иметь два символа '\0', один внутри массива и один сразу после него, который был записан системой.

Я также заметил, что выполнение

fgets(buff,(sizeof(buff)+17),stdin);

будет по-прежнему работать и выводить очень длинную строку без сбоев. Насколько я понял, это потому, что fgets будет продолжать запись до sizeof(buff)+17, а последним записываемым символом будет именно '\0', гарантируя, что любой предстоящий процесс чтения строки будет завершен правильно (хотя память все равно испорчена).

Но тогда как насчет fgets(buff, (sizeof(buff)+1),stdin);? это израсходует все пространство, которое было правильно выделено в buff, а затем запишет '\0' сразу после него, таким образом перезаписав... '\0' ранее записанное системой. Другими словами, да, fgets вышло бы за пределы допустимого, но можно доказать, что при добавлении к длине записи только единицы программа никогда не рухнет.

Итак, в конце возникает вопрос: почему fgets всегда заканчивает запись на '\0', когда другой '\0', размещенный системой сразу после массива, уже существует? почему бы не сделать так, как в записи на основе цикла for один за другим, которая может получить доступ ко всему массиву и записать все, что хочет программист, ничего не подвергая опасности?

Большое спасибо за ваш ответ!

РЕДАКТИРОВАТЬ: действительно, доказательство невозможно, пока я не знаю, является ли этот 8-й '\0', который таинственным образом появляется при выделении buff[7], частью стандарта C или нет, особенно для строковых массивов. Если нет, то... просто повезло, что это работает :-)


person MrBrody    schedule 28.08.2013    source источник
comment
Будьте осторожны, думая, что если что-то не падает, это означает, что это действительно правильно; обычно такие ошибки приводят к неопределенному поведению. Иногда вы получите segfault, иногда нет. Если у вас есть buff[7], нет гарантии, что 8-й байт будет \0, это может быть что угодно.   -  person PherricOxide    schedule 28.08.2013
comment
Я всегда буду получать 7 'a, и программа не рухнет. То, что вы ожидаете, что она должна/может рухнуть, как минимум предполагает, что вы понимаете неопределенное поведение. Что касается вашего вопроса, потому что именно так должен вести себя fgets. Если у вас есть char a; и вы передаете &a с некоторым произвольным размером больше 1, вы ожидаете что-нибудь определенное? Это C-api, и, как и большинство, либо полезный, либо неопределенный в своем поведении, в зависимости от того, как вы его называете.   -  person WhozCraig    schedule 28.08.2013
comment
Я понимаю, что любое тестирование на моей единственной машине никогда ничего не докажет. Я просто думал о строке, рассматриваемой как массив, где вы пишете все, что хотите, не думая о том, что будет содержать последняя ячейка (как в int[]), а затем думаете о строке, т.е. как о слове, с вездесущий страх перед отсутствующим завершающим персонажем. Из-за этого стандарт мог включить этот 8-й '\0' в качестве жестко заданного параметра. Поскольку я не знаю деталей стандарта... Я задавал вопрос: является ли эта восьмая '\0' частью стандарта C?   -  person MrBrody    schedule 28.08.2013
comment
Вы сказали: Я всегда буду получать 7 'а, и программа не будет падать. Но если я попытаюсь написать 8 'a', это произойдет. К сожалению, он не падает, когда вы пишете 7 'a', но вы вызываете неопределенное поведение, и сбой может легко произойти. Ошибки один за другим коварны, потому что они могут убаюкать вас ложным чувством безопасности. Вероятно, вы обнаружите, что после buff[6] в стеке есть неиспользуемый байт (поскольку следующую переменную необходимо выровнять по четной границе). Но на это нельзя полагаться...   -  person Jonathan Leffler    schedule 28.08.2013
comment
@Jonathan Leffler: Я понимаю, что вы говорите, однако я только что заметил, что этот 8-й байт не просто не использовался, он ВСЕГДА был установлен на 0, в то время как все байты вокруг могли быть чем угодно, если не были установлены должным образом раньше. Мне было интересно, было ли это совпадение частью стандарта или нет!   -  person MrBrody    schedule 28.08.2013
comment
@MrBrody Поскольку вы используете массив из 7 байтов, он, вероятно, дополняется нулями. Попробуйте с 8-байтовым массивом и посмотрите, не повезло ли вам. Когда вы запрашиваете n байт, вам гарантировано n байта, но вы можете получить больше.   -  person agbinfo    schedule 29.08.2013
comment
@agbinfo интересная часть заключается в том, что когда я просто выделяю (char buff[7]) без инициализации, то buff[6] оказывается чем угодно (32, 129 ... тем, что там оставалось раньше), но buff[7] был на удивление постоянным в своем значении. всегда 0. Вот что меня удивило!   -  person MrBrody    schedule 29.08.2013
comment
Дополнительный байт, равный нулю, был (плохой) удачей — стандарт ничего не говорит об этом байте, кроме того, что вы можете взять его адрес (но вы не можете разыменовать его без вызова неопределенного поведения).   -  person Jonathan Leffler    schedule 29.08.2013
comment
Понял! не является частью стандарта.   -  person MrBrody    schedule 29.08.2013


Ответы (2)


но можно доказать, что при добавлении только одного к длине записи программа никогда не рухнет.

Нет! Вы не можете доказать это! Не в смысле математического доказательства. Вы только показали, что в вашей системе, с вашим компилятором, с теми конкретными настройками компилятора, которые вы использовали, с определенной конфигурацией среды он может не давать сбоев. Это далеко не математическое доказательство!

На самом деле сам стандарт C, хотя и гарантирует, что вы можете получить адрес «на одно место после последнего элемента массива», также утверждает, что разыменование этого адреса (т. е. попытка чтения или записи с этого адреса) неопределенное поведение.

Это означает, что в данном случае реализация может делать все. Он может даже делать то, что вы ожидаете с наивными рассуждениями (т.е. работать - но это чистая удача), но также может давать сбой или форматировать ваш HD (если вам очень-очень не повезло). Это особенно актуально при написании системного программного обеспечения (например, драйвера устройства или программы, работающей на «голом железе»), т. е. когда нет ОС, защищающей вас от самых неприятных последствий написания плохого кода!

Изменить Это должно ответить на вопрос, заданный в комментарии (проект стандарта C99):

7.19.7.2 Функция fgets

Краткий обзор

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

Описание

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

Возврат

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

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

7.1.1 Определения терминов

Строка представляет собой непрерывную последовательность символов, заканчивающуюся первым нулевым символом и включающую его. Вместо этого иногда используется термин многобайтовая строка, чтобы подчеркнуть особую обработку многобайтовых символов, содержащихся в строке, или чтобы избежать путаницы с широкой строкой. Указатель на строку — это указатель на ее начальный (самый низкий адрес) символ. Длина строки – это количество байтов, предшествующих нулевому символу, а значение строки – это последовательность значений содержащихся символов по порядку.

person Lorenzo Donati -- Codidact.com    schedule 28.08.2013
comment
Вы имеете в виду, что запись 7 символов с помощью цикла for, а затем чтение результирующей строки также будет неопределенным поведением? Делая это, я не обращался напрямую ни к одному нелегальному адресу. Я мог бы написать что угодно в buff[6], а потом прочитать buff...или это запрещено стандартом? PS: вы правы насчет доказательства. Чтобы что-то доказать, мне нужно знать, является ли этот 8-й '\0', конкретно для строковых массивов, частью стандарта или нет! - person MrBrody; 28.08.2013
comment
Хорошо, я сделал запутанный комментарий. Мне было интересно, то ли 8-й '\0', который я мог видеть сразу после выделения (аллокации!), был написан из-за соответствия стандарту, или просто какая-то специфика компилятора на той системе! Если ВСЕГДА есть 8-й '\0', то этот последний '\0', написанный fgets, становится ненужным - person MrBrody; 28.08.2013
comment
Непонятно, что вы подразумеваете под \0 частью стандарта. Стандарты определяют как строки только последовательности символов, заканчивающиеся нулем, т. е. последовательности символов, последний символ которых является нулевым символом (т. е. символом с числовым кодом 0). Если у вас есть массив символов, он может содержать строку. Это происходит, если где-то в нем есть нулевой символ. Вы не можете принять во внимание символ за концом, который не существует концептуально. - person Lorenzo Donati -- Codidact.com; 28.08.2013
comment
Чтобы быть более конкретным: если вы определяете char ca[5], то ca содержит строку, если любой из ее элементов является '\0'. Точнее, строка начинается с ca[0] и заканчивается (первым) элементом, содержащим '\0', который действует как ограничитель строки. Этот нулевой символ должен быть в массиве. ca[6] нет. Если вы пишете такое выражение, вы вызываете неопределенное поведение. У вас может быть место в памяти, к которому может обращаться выражение ca[6], но это недопустимо. C не проверяет, является ли что-то с UB незаконным. Это по определению. Что бы ни случилось в этом случае, непредсказуемо. - person Lorenzo Donati -- Codidact.com; 29.08.2013
comment
ваши два последних комментария отвечают на мой вопрос. Я не знал об этом различии между массивами символов и строками. Таким образом, этот ca[6], установленный как 0, является просто удобством компилятора, который, таким образом, спасает программы от сбоев, будь то хорошие или плохие. Спасибо! - person MrBrody; 29.08.2013
comment
Нет, вы не можете сделать такой вывод. Вы не можете сказать, почему ca[6] оказывается равным 0. Компилятор не должен был помещать его туда (возможно, это среда выполнения или ОС). Есть десятки причин, по которым вы наблюдаете это явно детерминированное поведение. Вы могли бы сказать, только прочитав документы вашего компилятора (если это задокументированное поведение), или, что еще хуже, вы могли бы обнаружить это, только проанализировав исходный код вашего компилятора (при условии, что это поведение связано с компилятором). - person Lorenzo Donati -- Codidact.com; 29.08.2013
comment
ты прав. Я просто предположил. Я согласен с тем фактом, что очень трудно сказать истинную причину такого явно детерминированного поведения! Это было просто из любопытства, мне не нужно заходить так далеко и знать подробности - person MrBrody; 29.08.2013
comment
Кстати, если программа дает сбой при наличии неопределенного поведения, это хорошо (поскольку UB означает, что в программе есть ошибки). Здравомыслящий компилятор никогда не заставит ошибочную программу намеренно избежать сбоя. Принуждение программы к сбою на UB — это хорошо, но не во всех случаях это возможно и часто исключает некоторые оптимизации. Многие векторы атак вредоносных программ используют программы, которые не аварийно завершают работу при принудительном вызове UB! - person Lorenzo Donati -- Codidact.com; 29.08.2013
comment
Вот почему я был удивлен, что смог сделать fgets(buff, 17, stdin); и при этом не было сбоев. Я использую Code::Blocks в Windows и вижу немного Mingw_Nothrow в прототипах всех функций стандартной библиотеки. Это связано? - person MrBrody; 29.08.2013

Из стандартного проекта C11:

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

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

Описываемое вами поведение не определено.

person jev    schedule 28.08.2013
comment
Это я знаю. Мне просто интересно, является ли 8-й '\0', который я вижу еще до того, как запишу что-нибудь в строку (т. Е. При выделении), просто случайным или соответствует другому параграфу стандарта, утверждая, например, что дополнительный байт сразу после массива char[] , всегда будет записываться как '\0' при распределении массива... - person MrBrody; 28.08.2013
comment
Если вам повезло, массив дополняется так, что в конце может быть байт или два, к которым вы можете получить доступ, но вы не должны полагаться на это (неопределенное поведение). - person jev; 28.08.2013
comment
Лоренцо Донати рассказал мне, почему (разница между массивами символов и строками). Понял, это просто удача от компилятора, или еще что! - person MrBrody; 29.08.2013