Непоследовательное поведение fscanf() в разных компиляторах (использование завершающего нулевого символа)

Я написал полное приложение на C99 и тщательно протестировал его на двух системах на основе GNU/Linux. Я был удивлен, когда попытка скомпилировать его с помощью Visual Studio в Windows привела к неправильной работе приложения. Сначала я не мог утверждать, что случилось, но я попытался использовать отладчик VC, а затем я обнаружил несоответствие, касающееся fscanf() функция объявлена ​​в stdio.h,

Для демонстрации проблемы достаточно следующего кода:

#include <stdio.h>

int main() {
    unsigned num1, num2, num3;

    FILE *file = fopen("file.bin", "rb");
    fscanf(file, "%u", &num1);
    fgetc(file); // consume and discard \0
    fscanf(file, "%u", &num2);
    fgetc(file); // ditto
    fscanf(file, "%u", &num3);
    fgetc(file); // ditto
    fclose(file);

    printf("%d, %d, %d\n", num1, num2, num3);

    return 0;
}

Предположим, что file.bin содержит точно 512\0256\0128\0:

$ hexdump -C file.bin
00000000  35 31 32 00 32 35 36 00  31 32 38 00              |512.256.128.|

Теперь при компиляции в GCC 4.8.4 на машине с Ubuntu результирующая программа считывает числа в соответствии с ожиданиями и печатает 512, 256, 128 на стандартный вывод
Компиляция с MinGW 4.8.1 под Windows дает тот же, ожидаемый результат.

Однако, кажется, есть большая разница, когда я компилирую код с помощью Visual Studio Community 2015; а именно:

512, 56, 28

Как видите, завершающие нулевые символы уже были использованы fscanf(), так fgetc() захватывает и отбрасывает символы, которые необходимы для целостности данных.

Комментируя fgetc() линии заставляют код работать в VC, но разбивают его в GCC (и, возможно, других компиляторах).

Что здесь происходит, и как я могу превратить это в переносимый код C? Я столкнулся с неопределенным поведением? Обратите внимание, что я предполагаю стандарт C99.

2 ответа

Решение

TL; DR: вас укусило несоответствие MSVC, давняя проблема, которую MS никогда не проявляла особого интереса к решению. Если вы должны поддерживать MSVC в дополнение к соответствующим реализациям C, то один из способов сделать это - задействовать директивы условной компиляции для подавления fgetc() вызовы, когда программа компилируется через MSVC.


Я склонен согласиться с комментариями о том, что чтение двоичных данных через отформатированные функции ввода-вывода является сомнительным планом. Однако еще более сомнительным является сочетание

скомпилировать его с помощью Visual Studio в Windows

а также

предполагая стандарт C99.

Насколько мне известно, ни одна версия MSVC не соответствует C99. Самые последние версии могут лучше соответствовать C2011, отчасти потому, что C2011 делает некоторые дополнительные функции необязательными, которые были обязательными в C99.

Какую бы версию MSVC вы не использовали, я думаю, что она не соответствует стандарту (как C99, так и C2011) в этой области. Вот соответствующий текст из C99, раздел 7.19.6.2

Спецификация преобразования выполняется в следующие шаги:

[...]

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

В стандарте совершенно ясно, что первый символ, который не соответствует входной последовательности, остается непрочитанным, поэтому единственный способ, которым MSVC можно считать соответствующим, - это если \0 символы могут быть истолкованы как часть (и завершающая) совпадающей входной последовательности, или если fgetc() было разрешено пропустить \0 персонажи. Я не вижу оправдания для последнего, особенно учитывая, что поток был открыт в двоичном режиме, поэтому давайте рассмотрим первый.

Для u спецификатор преобразования, совпадающая входная последовательность определяется как

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

"Субъектная последовательность функции strtoul" определяется в спецификациях этой функции:

Во-первых, они разбивают входную строку на три части: начальную, возможно, пустую, последовательность символов пробела (как определено функцией isspace), предметную последовательность, напоминающую целое число, представленное в некотором основании, определяемом значением base, и последняя строка из одного или нескольких нераспознанных символов, включая завершающий нулевой символ входной строки.

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

MSVC реализация fscanf по-видимому, "громит" NUL персонаж рядом с 512:

fscanf(file, "%u", &num1);

Согласно fscanf Документация, это не должно иметь место (выделение мое):

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

Обратите внимание, что это отличается от ситуации, когда нужно пропустить завершающие белые символы, как в следующем утверждении:

fscanf(file, "%u ", &num1); // notice "%u "

В спецификации сказано, что это происходит только тогда, когда isspace свойство, которое, как проверено, не содержит здесь (то есть isspace('\0') дает 0).

Хакерский, похожий на регулярные выражения обходной путь, который работает как в MSVC, так и в GCC, может заменить fgetc с:

fscanf(file, "%*1[^0-9+-]"); // skip at most one non-%u character

или более переносимым путем замены определенной реализацией 0-9 класс символов с буквенными цифрами:

fscanf(file, "%*1[^0123456789+-]"); // skip at most one non-%u character
Другие вопросы по тегам