Menu

printf в C: строки формата, спецификаторы и вывод чисел с плавающей точкой

Как на самом деле работает printf в C - модель строки формата, спецификаторы на каждый день, ширина и точность для выровненных столбцов, разумный вывод дробных чисел, возвращаемое значение и почему printf(user_input) - дыра в безопасности.

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

printf - первая функция, которую изучает каждый C-программист, и та, руководство по которой большинство так и не дочитывает до конца. Десять минут на неё потратить стоит: её строка формата - это маленький отдельный язык, а ошибка в спецификаторе - не опечатка, а неопределённое поведение.

Она живёт в <stdio.h>, поэтому любая программа, которая её использует, начинается с #include <stdio.h>.

Модель строки формата

printf проходит по строке формата по одному символу. Всё, что не является %, выводится ровно так, как написано. % начинает спецификацию преобразования, которая съедает следующий аргумент и печатает его в запрошенном виде.

Аргументы сопоставляются со спецификаторами строго по позиции: первый % забирает count, второй - name и так далее. У printf нет способа проверить, что вы передали обещанное, - типов он не видит, - поэтому строка формата это контракт, который обязаны соблюдать вы. Именно поэтому стоит включить -Wall: GCC читает строковый литерал формата на этапе компиляции и предупреждает, когда аргумент не совпадает.

Ничто не добавляет перевод строки автоматически. После printf("Hello") курсор остаётся сразу за o - поэтому почти каждая строка формата заканчивается на \n.

Спецификаторы на каждый день

Пять покрывают большую часть кода:

СпецификаторЧто печатаетПример вызоваВывод
%dзнаковый intprintf("%d", -42)-42
%fdoubleprintf("%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).

Coddy programming languages illustration

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

НАЧАТЬ