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, а %lf — double. В 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 годится для небольших упражнений и корректно сформированного машинного ввода.