Ввод-вывод: 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 и ограничение длины ввода.

Проверьте себя
1. Почему перед именем переменной в scanf ставят знак &?
AЭто требование синтаксиса без смысла
BЧтобы передать адрес переменной — место в памяти, куда scanf запишет прочитанное значение
CЧтобы ускорить чтение
DЧтобы вывести значение на экран
2. Почему scanf(\"%s\", buf) без ограничения длины опасен?
AОн работает слишком медленно
BОн всегда читает только один символ
CПользователь может ввести больше символов, чем вмещает буфер, и переполнить его — классическая уязвимость
DОн не компилируется