Ввод-вывод: printf и scanf
Урок учит общаться с пользователем: выводить данные через printf со спецификаторами формата и читать ввод через scanf, понимая, почему scanf — источник классических уязвимостей.
printf и scanf работают через «спецификаторы формата» — шаблоны вроде %d или %f. Несоответствие спецификатора и реального типа аргумента — частая причина загадочных багов в C.
Функция printf печатает форматированный текст. Внутри строки-шаблона стоят спецификаторы, которые заменяются значениями аргументов:
int age = 30;
double height = 1.85;
char letter = 'X';
printf("Возраст: %d\n", age); // %d — целое
printf("Рост: %.2f м\n", height); // %.2f — дробное, 2 знака
printf("Буква: %c\n", letter); // %c — символ
printf("Строка: %s\n", "привет"); // %s — строка
Парная функция scanf читает ввод пользователя и раскладывает его по переменным. Ключевая деталь: scanf должен знать, КУДА записать прочитанное, поэтому перед именем переменной ставят знак & — оператор «взять адрес»:
int n;
printf("Введите число: ");
scanf("%d", &n); // &n — адрес переменной n
printf("Вы ввели: %d\n", n);
Как работает под капотом
Почему scanf требует &n, а printf — нет? Потому что printf только читает значение переменной, а scanf должен его изменить. Чтобы изменить переменную внутри другой функции, нужно передать её адрес — место в памяти, куда писать. Схематично:
Память:
адрес 0x7ffe1234 -> [ n: ??? ] переменная n живёт здесь
scanf("%d", &n):
&n = 0x7ffe1234 (передаём АДРЕС, не значение)
|
v
scanf читает "42" с клавиатуры
|
v
записывает 42 по адресу 0x7ffe1234
|
v
адрес 0x7ffe1234 -> [ n: 42 ]
Это первое прикосновение к идее указателей, на которой держится весь C. &n — это адрес, а не значение. Запомните пару: printf берёт значения, scanf берёт адреса.
Частые ошибки
- Забыли
&в scanf.scanf("%d", n)вместоscanf("%d", &n)передаёт значение вместо адреса — программа запишет данные по случайному адресу и упадёт. - Несоответствие спецификатора.
printf("%d", 3.14)— печатьdoubleкак целого даёт мусор: типы не совпадают. - Чтение строки через
%sбез ограничения длины.scanf("%s", buf)запишет сколько угодно символов и переполнит буфер — это классическая уязвимость. - Игнорирование результата scanf. Если пользователь ввёл буквы вместо числа,
scanfничего не прочитает, а переменная останется с мусором.
Best practices
- Всегда проверяйте, сколько значений реально прочитал scanf:
if (scanf("%d", &n) != 1) { /* ошибка ввода */ }. - Для строк ограничивайте длину:
scanf("%19s", buf)для буфера на 20 символов — это защита от переполнения. - Для надёжного чтения строк предпочитайте
fgetsвместоscanf("%s"):fgetsпозволяет явно задать максимальный размер.
В браузере мы не можем прочитать ввод C, поэтому смоделируем логику scanf на Python: «прочитать значение и записать его по адресу переменной». Адрес сымитируем словарём-памятью.
# Эмулируем "запись по адресу", как делает scanf
memory = {} # это наша "память"
def scanf_int(addr, raw_input):
# пытаемся разобрать ввод как целое, как настоящий scanf
try:
memory[addr] = int(raw_input)
return 1 # успешно прочитано 1 значение
except ValueError:
return 0 # ввод некорректен
ok = scanf_int("n", "42")
print("scanf вернул:", ok, "| memory['n'] =", memory.get("n"))
ok = scanf_int("n", "abc")
print("scanf вернул:", ok, "(некорректный ввод не записан)")
Та же логика на Python ▶ — обратите внимание, как важно проверять возвращаемое значение: при вводе «abc» scanf вернул 0 и ничего не записал.
Буферизация вывода
Есть тонкость, которая удивляет новичков при отладке: printf не всегда выводит текст мгновенно. Стандартный поток вывода буферизуется — символы накапливаются и печатаются пачкой, обычно при переводе строки или заполнении буфера. Если программа упадёт до сброса буфера, вы не увидите последних printf — и решите, что до них код не дошёл, хотя на самом деле дошёл. Поэтому при отладке падений ставят \n в конце или вызывают fflush(stdout), чтобы принудительно вытолкнуть буфер. Это же объясняет, почему вывод и ввод иногда «перемешиваются» не в том порядке, в каком вы их написали: разные потоки буферизуются по-разному.
Итоги
printf выводит данные по спецификаторам формата (%d, %f, %c, %s), а scanf читает ввод, требуя адрес переменной через &. Это первое знакомство с указателями. Главные опасности — забытый &, несовпадение спецификатора и типа, и переполнение буфера при чтении строк. Защита — проверка результата scanf и ограничение длины ввода.