printf - первая функция, которую изучает каждый C-программист, и та, руководство по которой большинство так и не дочитывает до конца. Десять минут на неё потратить стоит: её строка формата - это маленький отдельный язык, а ошибка в спецификаторе - не опечатка, а неопределённое поведение.
Она живёт в <stdio.h>, поэтому любая программа, которая её использует, начинается с #include <stdio.h>.
Модель строки формата
printf проходит по строке формата по одному символу. Всё, что не является %, выводится ровно так, как написано. % начинает спецификацию преобразования, которая съедает следующий аргумент и печатает его в запрошенном виде.
Аргументы сопоставляются со спецификаторами строго по позиции: первый % забирает count, второй - name и так далее. У printf нет способа проверить, что вы передали обещанное, - типов он не видит, - поэтому строка формата это контракт, который обязаны соблюдать вы. Именно поэтому стоит включить -Wall: GCC читает строковый литерал формата на этапе компиляции и предупреждает, когда аргумент не совпадает.
Ничто не добавляет перевод строки автоматически. После printf("Hello") курсор остаётся сразу за o - поэтому почти каждая строка формата заканчивается на \n.
Спецификаторы на каждый день
Пять покрывают большую часть кода:
| Спецификатор | Что печатает | Пример вызова | Вывод |
|---|---|---|---|
%d | знаковый int | printf("%d", -42) | -42 |
%f | double | printf("%f", 2.5) | 2.500000 |
%s | строку (char *) | printf("%s", "hi") | hi |
%c | один символ | printf("%c", 'A') | A |
%p | адрес указателя | printf("%p", (void*)&x) | 0x7ffd... |
В этом фрагменте спрятаны два правила. Литеральный знак процента пишется как %% - одиночный % в конце строки формата это неопределённое поведение. А %p требует void *, поэтому указатель нужно приводить: передать int * напрямую формально недопустимо, даже если на практике всё печатается нормально.
Полная таблица - каждый спецификатор, каждый модификатор длины, колонка для scanf - находится на странице спецификаторы формата. Здесь остаются те, к которым вы тянетесь постоянно.
Ширина, точность и выравнивание
Между % и буквой можно поставить флаги, ширину и точность. Именно так цикл превращается в таблицу.
Ширина - это минимум, а не максимум: %3d со значением 123456 напечатает все шесть цифр. Точность означает разное для разных типов: цифры после десятичной точки для %f, максимум символов для %s, минимум цифр для %d.
И то и другое можно задать во время выполнения через *, который читает аргумент типа int:
Изменение width перестраивает всю таблицу. Это стандартный способ печатать выровненные столбцы в C - никакой вспомогательной функции дополнения строк не нужно. (Заголовки здесь оставлены латиницей намеренно: ширина в printf считается в байтах, а не в символах, поэтому кириллический заголовок в столбце фиксированной ширины сломал бы выравнивание.)
Разумный вывод дробных чисел
%f без точности всегда печатает шесть цифр после точки, что нужно крайне редко:
Три варианта, и для каждого - своё правило:
%.Nfдля денег, измерений, всего, у чего есть естественное число знаков после запятой. Для валюты нужен именно%.2f.%eдля научной записи, всегда в видеd.dddddde±dd.%gкогда порядок величины заранее неизвестен. Он выбирает%fили%e- что короче - и убирает хвостовые нули: самый здравый вариант по умолчанию для логирования значения, которого вы ещё не видели.
Одну ловушку стоит назвать прямо: в printf нет %lf. Аргументы типа float повышаются до double ещё до того, как printf их увидит, поэтому %f обслуживает оба типа, а %lf имеет смысл только в scanf, где размер приёмника важен. (C99 действительно принимает %lf в printf как синоним, но привычка так писать приводит к тому, что люди пишут %f в scanf, а вот это уже ломается по-настоящему.)
Целые числа другой разрядности
%d - для int. Более крупным или беззнаковым типам нужен модификатор длины, и их несовпадение - это неопределённое поведение, а не ошибка округления:
%zu для size_t ошибаются чаще всего: sizeof возвращает size_t, а не int, поэтому printf("%d", sizeof x) - это баг, который случайно работает на 32-битных системах и ведёт себя неправильно на 64-битных.
Возвращаемое значение
printf возвращает количество выведенных символов или отрицательное число при ошибке. Обычно его игнорируют, и чаще всего это нормально - но оно по-настоящему полезно, когда нужно знать, какой ширины получился вывод:
Родственные sprintf и snprintf пишут в буфер вместо экрана и возвращают полученную длину. Всегда предпочитайте snprintf: она принимает размер буфера и не может его переполнить:
snprintf возвращает длину, которая понадобилась бы полной строке, поэтому возвращённое значение, равное размеру буфера или больше, говорит, что вывод был усечён.
Уязвимость строки формата
Это единственная проблема безопасности в printf, и она серьёзная. Никогда не передавайте в качестве строки формата данные, которые написали не вы:
/* ОПАСНО - так делать нельзя */
printf(user_input);
/* Правильно */
printf("%s", user_input);
Если в user_input есть %s, printf прочитает аргумент, который никто не передавал, и пойдёт по попавшемуся мусору, похожему на указатель, - обычно с падением. Повторённый %x вываливает содержимое стека на экран, а там могут оказаться пароли или ключи. А %n, который записывает счётчик символов в аргумент, исторически превращал всё это в произвольную порчу памяти и удалённое выполнение кода.
Правило абсолютное и не стоит ничего: строка формата - всегда написанный вами литерал. Всё переменное идёт аргументом. Ключ GCC -Wformat-security отмечает эту ошибку и входит в -Wall -Wextra.
Буферизация: почему вывод иногда появляется с опозданием
printf не пишет в терминал немедленно. Вывод буферизуется - сбрасывается, когда буфер заполнился, когда в терминале встретился перевод строки или когда программа нормально завершается. Отсюда два следствия:
- Упавшая программа может потерять вывод, который «уже напечатался». Если вы отлаживаетесь через
printfи последнее сообщение так и не появилось, падение произошло после этой строки, а не до неё. Добавьтеfflush(stdout)или отправляйте отладочный вывод вstderr, который не буферизуется. fprintf(stderr, ...)- правильный способ печатать ошибки. Он не буферизуется и идёт в отдельный поток, который пользователи могут перенаправлять независимо от обычного вывода.
Часто задаваемые вопросы
Как работает printf в C?
printf принимает строку формата и просматривает её символ за символом. Обычные символы выводятся как есть; каждый % начинает спецификацию преобразования, которая забирает один из следующих аргументов и печатает его в нужном виде. printf("%d apples\n", 5) выведет 5 apples и перевод строки.
Как вывести число с двумя знаками после запятой в C?
Укажите точность: printf("%.2f\n", 3.14159) выведет 3.14. Число после точки - это количество цифр после десятичной точки. %f без точности всегда печатает 6 цифр, поэтому неформатированные дробные числа выглядят такими шумными.
Почему printf выводит мусор или роняет программу?
Почти всегда из-за несоответствия спецификатора и аргумента. printf("%d", 3.5) или printf("%s", 42) - это неопределённое поведение: printf не видит настоящих типов, он доверяет строке формата. Компилируйте с -Wall, тогда GCC сверит формат с аргументами.
Почему printf(user_input) опасен?
Если в тексте пользователя есть последовательности с %, printf подчинится им и начнёт читать аргументы, которые никто не передавал, - утечёт содержимое стека, а с %n возможна и запись в память. Это классическая уязвимость строки формата. Всегда пишите printf("%s", user_input).