Несколько вопросов касались того, что вы сделали неправильно и как это исправить, но вы также сказали (выделение мое):
может кто-нибудь объяснить, почему, и , почему этот стиль кодирования плохой
Я думаю, scanf
- ужасный способ чтения ввода. Он несовместим с printf
, позволяет легко забыть проверять ошибки, затрудняет восстановление после ошибок и несовместим с обычными (и их проще делать правильно) операциями чтения (такими как fgets
и компания).
Во-первых, обратите внимание, что формат "%s"
будет считываться только до тех пор, пока не увидит пробел. Почему пробел? Почему "%s"
распечатывает всю строку, но читает в строках с такой ограниченной емкостью?
Если вы хотите читать всю строку, как это часто бывает, scanf
предоставляет ... "%[^\n]"
. Какие? Что это такое? Когда это стало Perl?
Но настоящая проблема в том, что ни один из них не является безопасным. Они оба свободно переполняются без проверки границ. Хотите проверить границы? Хорошо, вы поняли: "%10s"
(а "%10[^\n]"
начинает выглядеть еще хуже). Это будет читать только 9 символов и автоматически добавлять завершающий nul-символ. Так что это хорошо ... когда размер нашего массива никогда не нужно менять .
Что если мы хотим передать размер нашего массива в качестве аргумента scanf
? printf
может сделать это:
char string[] = "Hello, world!";
printf("%.*s\n", sizeof string, string); // prints whole message;
printf("%.*s\n", 6, string); // prints just "Hello,"
Хотите сделать то же самое с scanf
? Вот как это сделать:
static char tmp[/*bit twiddling to get the log10 of SIZE_MAX plus a few*/];
// if we did the math right we shouldn't need to use snprintf
snprintf(tmp, sizeof tmp, "%%%us", bufsize);
scanf(tmp, buffer);
Это верно - scanf
не поддерживает "%.*s"
переменную точность printf
, поэтому для динамической проверки границ с помощью scanf
мы должны построить нашу собственную строку формата в временный буфер. Это все виды плохих, и хотя здесь на самом деле безопасно, это будет выглядеть очень плохой идеей для любого, кто только что зашел.
А пока давайте посмотрим на другой мир. Давайте посмотрим на мир fgets
. Вот как мы читаем строку данных с fgets
:
fgets(buffer, bufsize, stdin);
Бесконечно меньше головной боли, не тратится впустую процессорное время, преобразующее целочисленную точность в строку, которая будет перечитываться только библиотекой обратно в целое число, и все соответствующие элементы находятся там на одной строке для нас чтобы увидеть, как они работают вместе.
Конечно, это может не прочитать всю строку. Он будет читать всю строку, только если она короче bufsize - 1
символов. Вот как мы можем прочитать всю строку:
char *readline(FILE *file)
{
size_t size = 80; // start off small
size_t curr = 0;
char *buffer = malloc(size);
while(fgets(buffer + curr, size - curr, file))
{
if(strchr(buffer + curr, '\n')) return buffer; // success
curr = size - 1;
size *= 2;
char *tmp = realloc(buffer, size);
if(tmp == NULL) /* handle error */;
buffer = tmp;
}
/* handle error */;
}
Переменная curr
- это оптимизация, предотвращающая перепроверку уже прочитанных нами данных, и она не нужна (хотя и полезна, когда мы читаем больше данных). Мы могли бы даже использовать возвращаемое значение strchr
, чтобы удалить конечный символ "\n"
, если хотите.
Также обратите внимание, что size_t size = 80;
в качестве отправной точки совершенно произвольно. Мы могли бы использовать 81, или 79, или 100, или добавить его в качестве предоставленного пользователем аргумента функции. Мы могли бы даже добавить аргумент int (*inc)(int)
и изменить size *= 2;
на size = inc(size);
, позволяя пользователю контролировать скорость роста массива. Это может быть полезно для повышения эффективности, когда перераспределение становится дорогостоящим, и необходимо прочитать и обработать множество строк данных.
Мы могли бы написать то же самое с scanf
, но подумайте, сколько раз нам пришлось бы переписывать строку формата. Мы могли бы ограничить его постоянным приращением вместо удвоения (легко), реализованного выше, и никогда не пришлось бы корректировать строку формата; мы могли бы дать и просто сохранить число, выполнить математику, как указано выше, и использовать snprintf
для преобразования его в строку формата каждый раз, когда мы перераспределяем , чтобы scanf
мог преобразовать его обратно в такое же количество; мы могли бы ограничить наш рост и начальную позицию таким образом, чтобы мы могли вручную настроить строку формата (скажем, просто увеличить цифры), но через некоторое время это может стать проблематичным и может потребовать рекурсии (!) для чистой работы.
Кроме того, трудно совмещать чтение с scanf
с чтением с другими функциями. Зачем? Скажем, вы хотите прочитать целое число из строки, а затем прочитать строку из следующей строки. Вы попробуйте это:
int i;
char buf[BUSIZE];
scanf("%i", &i);
fgets(buf, BUFSIZE, stdin);
Это будет читать "2", но тогда fgets
будет читать пустую строку, потому что scanf
не читал новую строку!Хорошо, возьмем два:
...
scanf("%i\n", &i);
...
Вы думаете, что это съедает новую строку, и это делает - но это также пожирает ведущие пробелы на следующей строке, потому что scanf
не может определить разницу между новыми строкамии другие формы пробелов.(Кроме того, оказывается, что вы пишете парсер Python, и начальные пробелы в строках важны.) Чтобы это работало, вам нужно вызвать getchar
или что-то для чтения в новой строке и выбросить его:
...
scanf("%i", &i);
getchar();
...
Разве это не глупо?Что происходит, если вы используете scanf
в функции, но не вызываете getchar
, потому что вы не знаете, будет ли следующее чтение scanf
или что-то более разумное (или даже следующий символбудет перевод строки)?Внезапно лучший способ справиться с ситуацией, кажется, выбрать один или другой: используем ли мы исключительно scanf
и никогда не имеем доступа к входу полного управления в стиле fgets
, или мы используем исключительно fgets
и делаемсложнее выполнить сложный разбор?
На самом деле, ответ: мы не .Мы используем fgets
(или не scanf
функции) исключительно, и когда нам нужна scanf
-подобная функциональность, мы просто вызываем sscanf
для строк! Нам не нужно иметьscanf
испортить наши файловые потоки без необходимости!Мы можем иметь точный контроль над нашим вводом, который мы хотим, и все еще получить всю функциональность scanf
форматирования.И даже если бы мы не могли, многие опции формата scanf
имеют почти прямые соответствующие функции в стандартной библиотеке, такие как бесконечно более гибкие функции strtol
и strtod
(и друзья).Кроме того, i = strtoumax(str, NULL)
для целочисленных типов размера C99 выглядит намного чище, чем scanf("%" SCNuMAX, &i);
, и намного безопаснее (мы можем использовать эту строку strtoumax
неизменной для меньших типов и позволить неявному преобразованию обрабатывать дополнительные биты, но с scanf
мы должны сделать временное uintmax_t
для чтения).
Мораль этой истории: избегать scanf
.Если вам нужно предоставляемое форматирование, и вы не хотите (или не можете) сделать это (более эффективно) самостоятельно, используйте fgets
/ sscanf
.