Menu

scanf в C: чтение ввода, правило & и почему он перестаёт работать

Как читать ввод с помощью scanf в C: зачем нужен &, как обрабатываются пробелы, переполнение буфера в %s и ограничение ширины, проверка возвращаемого значения, баг с оставшимся переводом строки и fgets как надёжная альтернатива.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

scanf — зеркало printf: тот же заголовок, похожие строки формата, противоположное направление. Он читает текст из стандартного ввода, преобразует его согласно формату и сохраняет результаты по переданным вами указателям.

А ещё это функция, которая запутывает новичков сильнее любой другой в C, потому что три её особенности неожиданны, а одна из них — дыра в безопасности. Эта страница разбирает все четыре.

Базовая форма и почему &

Наберите число и нажмите Enter, чтобы это заработало. &age — не украшение. C передаёт каждый аргумент по значению, поэтому, напиши вы scanf("%d", age), функция получила бы копию текущего (мусорного) значения age и никак не смогла бы добраться до самой переменной. &age передаёт адрес переменной, по которому scanf и пишет. Полная картина — в статье указатели.

Пропуск & — самая частая ошибка при работе с scanf. Компилятор с -Wall её ловит (format '%d' expects argument of type 'int *'); без включённых предупреждений код собирается и портит память по случайному адресу.

Чтение нескольких значений

Один вызов может прочитать несколько значений. Пробельные символы во вводе — пробелы, табуляции, переводы строк — разделяют их, а числовые спецификаторы пропускают их в любом количестве.

Буквальные пробелы в "%d %d %d" на самом деле избыточны — %d и так пропускает ведущие пробельные символы, — но они делают формат читаемым. А вот что не избыточно, так это проверка возвращаемого значения; о ней ниже.

Непробельные символы в формате должны совпадать с вводом в точности — так и читают структурированный ввод:

Двоеточие в формате требует двоеточия во вводе. Наберите 9:30 — и всё разберётся; наберите 9 30 — и разбор провалится, прочитав 9.

Всегда проверяйте возвращаемое значение

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

Наберите в ответ hello. scanf вернёт 0, n останется нетронутой — с тем же мусором, что и была, — а вызвавший проблему текст по-прежнему лежит во входном потоке. Именно последнее превращает один неудачный ввод в бесконечный цикл:

/* БАГ: крутится вечно на нечисловом вводе */
while (scanf("%d", &n) != 1) {
    printf("Попробуйте снова: ");
}

scanf никогда не съедает текст, который не смог преобразовать, поэтому следующий вызов спотыкается о те же символы — и так бесконечно. Чтобы восстановиться, нужно самому выбросить остаток строки:

Проверка на EOF важна: когда поток ввода заканчивается (пользователь нажал Ctrl+D или файл из канала исчерпан), scanf после этого возвращает EOF всегда, поэтому цикл, который проверяет только != 1, будет повторять попытки вечно. Обрабатывайте «плохой ввод» и «ввода больше нет» как два разных случая, которыми они и являются. С этим на месте такую вспомогательную функцию discard_line стоит держать под рукой в любой программе, где scanf читает ввод человека.

Переполнение буфера в %s

%s читает символы в массив char. Обратите внимание: & здесь нет — имя массива и так превращается в указатель на первый элемент, поэтому &name имел бы неверный тип.

Проблема в том, что голый %s понятия не имеет о размере вашего массива:

char name[10];
scanf("%s", name);      /* ОПАСНО: наберут 40 символов - запишется 41 байт */

Ничто не останавливает запись на конце массива. Она уходит дальше в то, что лежит в памяти следом — другие переменные, адрес возврата, — и это классическое переполнение буфера, исторически самый эксплуатируемый класс ошибок в C. Решение — ширина между % и s, на единицу меньше размера массива, чтобы оставить место под завершающий '\0':

Ширину приходится писать литералом внутри строки формата, из-за чего её неудобно держать согласованной с размером массива, — ещё одна причина, по которой в промышленном коде на C чаще используют fgets.

Второй сюрприз %s: он останавливается на первом пробельном символе. Наберите Ada Lovelace — и в name окажется Ada, а Lovelace останется в потоке до следующего чтения. %s читает слово, а не строку.

Баг с оставшимся переводом строки

Это то самое «scanf не работает», на которое натыкаются все. Числовые спецификаторы пропускают ведущие пробельные символы, а %c — нет.

Вот что происходит без этого пробела. Вы набираете 30 и нажимаете Enter. %d съедает 3 и 0 и останавливается на переводе строки, оставляя '\n' в буфере. Следующий scanf("%c", &initial) читает этот перевод строки как символ и возвращается мгновенно — приглашение появляется и тут же проскакивает.

Пробел в строке формата scanf означает «пропустить здесь любое количество пробельных символов», поэтому " %c" перешагивает через застрявший перевод строки и ждёт настоящий символ. Пробел нужен только перед %c и %[; все остальные спецификаторы пропускают пробельные символы сами.

Чтение чисел с плавающей точкой и символов

Спецификаторы в основном общие с printf, но с одним важным отличием: scanf должен знать точный размер приёмника, потому что пишет через указатель.

Попробуйте 1.5 2.25 7 x.

%f читает float, а %lfdouble. В printf оба печатаются одинаково, потому что float перед вызовом повышается до double, но в scanf никакого повышения нет, и %f с указателем на double запишет четыре байта в восьмибайтовую переменную, оставив её испорченной. %c здесь не нуждается в ведущем пробеле, потому что стоящий перед ним %d уже остановился на пробеле, а не на переводе строки. (Полная таблица — на странице спецификаторы формата.)

fgets: надёжная альтернатива

Для всего, что набирает человек, читайте строку целиком, а разбирайте её потом. fgets принимает размер буфера аргументом, поэтому физически не может переполниться, и, когда строка поместилась, съедает ещё и перевод строки, так что следующему чтению ничего не мешает. (Строка длиннее буфера оставляет свой хвост непрочитанным — отсутствие '\n' в полученном тексте и есть способ это обнаружить.)

Три замечания к этому приёму:

  • sizeof line передаёт настоящий размер, поэтому изменение размера массива не требует других правок — в отличие от литерала %19s.
  • fgets сохраняет перевод строки, если строка поместилась, — отсюда и обрезка через strcspn. Это же способ обнаружить строку, которая не влезла в буфер: в полученном тексте нет '\n'.
  • Разбирайте потом. strtol для целых (он ещё и подскажет, где разбор остановился), strtod для чисел с плавающей точкой либо sscanf(line, "%d %d", &a, &b), чтобы применить формат scanf к строке, которая уже безопасно лежит у вас.

sscanf заслуживает отдельного упоминания: это тот же scanf, но читающий из строки, а не из входного потока, и он прекрасно сочетается с fgets. Вы получаете удобный разбор scanf без единой его проблемы с потоком.

Частые ошибки

  • Забытый & у аргумента, не являющегося массивом. Собирайте с -Wall.
  • %s без ширины. Переполнение буфера, ждущее длинного ввода.
  • Игнорирование возвращаемого значения. А затем чтение переменной, которую scanf так и не присвоил.
  • Цикл по неудачному scanf без выбрасывания ввода. Бесконечный цикл.
  • %c без ведущего пробела после чтения числа. Тот самый баг с проскочившим приглашением.
  • %f для double. Тихая порча данных; scanf требует %lf.
  • Ожидание, что %s прочитает строку. Он читает одно слово, ограниченное пробельными символами.

Часто задаваемые вопросы

Зачем scanf нужен & в C?

Потому что C передаёт аргументы по значению. scanf должен изменить вашу переменную, поэтому ему нужен её адрес, а не копия значения: scanf("%d", &age). Единственное исключение — массив символов, чьё имя само является адресом: scanf("%19s", name) пишется без &.

Почему scanf пропускает ввод для char или строки?

Предыдущий scanf("%d", ...) оставил во входном буфере перевод строки от клавиши Enter. %d и %f пропускают ведущие пробельные символы, а %c — нет, поэтому он читает этот перевод строки вместо ожидания. Решение — пробел в строке формата: scanf(" %c", &ch) велит сначала пропустить пробельные символы.

Как безопасно считать строку с помощью scanf в C?

Задайте %s максимальную ширину на единицу меньше размера буфера: char name[20]; scanf("%19s", name);. Без ширины %s запишет столько символов, сколько наберёт пользователь, и спокойно выйдет за конец массива. Помните также, что %s останавливается на первом пробельном символе, поэтому полное имя с пробелом им не прочитать.

Что использовать в C — scanf или fgets?

Для всего, что набирает человек, используйте fgets. Он читает целую строку в буфер известного размера, не может переполниться и (когда строка поместилась) съедает ещё и перевод строки, поэтому классические баги смешанного ввода исчезают. Строка длиннее буфера оставляет свой хвост непрочитанным во входном потоке. Разбирайте строку потом через sscanf или strtol. scanf годится для небольших упражнений и корректно сформированного машинного ввода.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ