printf — это стандартная библиотечная функция языка C , которая форматирует текст и записывает его в стандартный вывод .
Название printf является сокращением от print formatted , где print относится к выводу на принтер, хотя функции не ограничиваются выводом на принтер.
Стандартная библиотека предоставляет множество других подобных функций, которые образуют семейство функций типа printf . Эти функции принимают параметр строки формата и переменное число параметров-значений, которые функция сериализует по строке формата и записывает в выходной поток или буфер строк .
Строка формата кодируется как язык шаблонов, состоящий из дословного текста и спецификаторов формата , каждый из которых определяет, как сериализовать значение. Поскольку строка формата обрабатывается слева направо , для каждого найденного спецификатора формата используется последующее значение. Спецификатор формата начинается с %
символа и имеет один или несколько следующих символов, которые определяют, как сериализовать значение.
Синтаксис и семантика строки формата одинаковы для всех функций семейства printf-подобных.
Несоответствие между спецификаторами формата, количеством и типом значений может привести к сбою или уязвимости .
Форматная строка printf является дополнительной к форматной строке scanf , которая обеспечивает форматированный ввод ( лексический анализ, также известный как парсинг ). Обе форматные строки обеспечивают относительно простую функциональность по сравнению с другими шаблонизаторами, лексическими анализаторами и парсерами.
Дизайн форматирования был скопирован в других языках программирования .
Ранние языки программирования, такие как Fortran, использовали специальные операторы с синтаксисом, отличным от других вычислений, для построения описаний форматирования. [1] В этом примере формат указан в строке 601, а команда PRINT [a] ссылается на него по номеру строки:
ПЕЧАТЬ 601 , IA , IB , ОБЛАСТЬ 601 ФОРМАТ ( 4 H A = , I5 , 5 H B = , I5 , 8 H ОБЛАСТЬ = , F10 . 2 , 13 H КВАДРАТНЫЕ ЕДИНИЦЫ )
Настоящим:
4H
обозначает строку из 4 символов " A= "
( H
означает поле Холлерита );I5
указывает на целочисленное поле шириной 5;F10.2
указывает поле с плавающей точкой шириной 10 с 2 цифрами после десятичной точки.Вывод с входными аргументами 100, 200 и 1500.25 может выглядеть следующим образом:
A= 100 B= 200 ПЛОЩАДЬ= 1500,25 КВАДРАТНЫХ ЕДИНИЦ
В 1967 году появился BCPL . [2] Его библиотека включала writef
процедуру. [3] Пример приложения выглядит так:
WRITEF("%I2-ЗАДАЧА С ФЕРЗЯМИ ИМЕЕТ %I5 РЕШЕНИЙ*N", NUMQUEENS, COUNT)
Настоящим:
%I2
указывает целое число шириной 2 (порядок ширины и типа поля спецификации формата обратный по сравнению с C printf
);%I5
обозначает целое число шириной 5;*N
— это управляющая последовательность языка BCPL , представляющая символ новой строки (в языке C для этого используется управляющая последовательность \n
).В 1968 году ALGOL 68 имел более функциональный API , но по-прежнему использовал специальный синтаксис ( $
разделители окружали специальный синтаксис форматирования):
printf (( $ "Цвет " g ", number1 " 6 d , ", number2 " 4 zd , ", hex " 16 r2d , ", float " - d .2 d , ", беззнаковое значение" -3 d "." l$ , "red" , 123456 , 89 , BIN 255 , 3.14 , 250 ));
В отличие от Фортрана, использование обычных вызовов функций и типов данных упрощает язык и компилятор, а также позволяет реализовать ввод/вывод на том же языке.
Считалось, что эти преимущества перевешивают недостатки (такие как полное отсутствие безопасности типов во многих случаях) вплоть до 2000-х годов, и в большинстве новых языков той эпохи ввод-вывод не являлся частью синтаксиса.
С тех пор люди на собственном горьком опыте убедились [4] , что это убеждение ложно, что приводит к множеству нежелательных последствий, начиная от уязвимостей безопасности и заканчивая сбоями оборудования (например, сетевые возможности телефона навсегда отключаются после попытки подключения к точке доступа с именем «%p%s%s%s%s%n». [5] ).
Современные языки, такие как C++20 и более поздние, поэтому предпринимают шаги для исправления этой ошибки и включают спецификации формата как часть синтаксиса языка [6] , что в некоторой степени восстанавливает безопасность типов при форматировании и позволяет компилятору обнаруживать некоторые недопустимые комбинации спецификаторов формата и типов данных во время компиляции.
В 1973 году printf
включен в качестве процедуры C в состав версии 4 Unix . [7]
В 1990 году printf
команда оболочки была аттестована как часть 4.3BSD-Reno . Она была создана по образцу стандартной библиотечной функции. [8]
В 1991 году printf
команда была включена в состав GNU shellutils (теперь часть GNU Core Utilities ).
Необходимость решения целого ряда проблем, возникающих из-за отсутствия безопасности типов, побудила к попыткам сделать компилятор C++ поддерживающим эту printf
технологию.
Опция -Wformat
GCC позволяет выполнять проверки вызовов во время компиляции , позволяяprintf
компилятору обнаруживать подмножество недопустимых вызовов (и выдавать либо предупреждение, либо ошибку, полностью останавливая компиляцию, в зависимости от других флагов). [9] .
Поскольку компилятор проверяет printf
спецификаторы формата, включение этой опции фактически расширяет синтаксис C++, делая форматирование его частью.
Как было сказано выше, многочисленные проблемы [10], связанные с printf()
отсутствием безопасности типов, привели к пересмотру [11] подхода к форматированию, а в C++20 и далее в язык были включены спецификации формата [12] , обеспечивающие безопасное форматирование типов.
Подход (и синтаксис) C++20 std::format
возник в результате эффективного включения API Виктора Зверовича libfmt
[13] в спецификацию языка [14] (Зверович написал [15] первый черновик предложения нового формата); следовательно, libfmt
он является реализацией спецификации формата C++20.
Функция форматирования была объединена с выводом в C++23, что предоставляет [16] команду std::print
в качестве замены printf()
.
Поскольку спецификация формата стала частью синтаксиса языка, компилятор C++ способен предотвращать недопустимые комбинации типов и спецификаторов формата во многих случаях. В отличие от опции -Wformat
, это не опциональная функция.
Спецификация формата libfmt
и std::format
сама по себе является расширяемым «мини-языком» (так называемым в спецификации [17] примером предметно-ориентированного языка ).
Включение в синтаксис языка C++ отдельного, предметно-ориентированного мини-языка, специально предназначенного для форматирования std::print
, завершает исторический цикл, возвращая современное состояние (по состоянию на 2024 год) к тому, каким оно было в случае первой PRINT
реализации FORTRAN в 1950-х годах, обсуждавшейся в начале этого раздела.
Форматирование значения указывается как разметка в строке формата. Например, следующий код выводит "Ваш возраст составляет ", а затем значение переменной age
в десятичном формате.
printf ( "Ваш возраст %d" , age );
Синтаксис спецификатора формата следующий:
%[ параметр ][ флаги ][ ширина ][. точность ][ длина ] тип
Поле параметра является необязательным. Если включено, то сопоставление спецификаторов со значениями не является последовательным. Числовое значение n выбирает n-й параметр значения.
Характер | Описание |
---|---|
н $ | n — индекс параметра значения для сериализации с использованием этого спецификатора формата. |
Это расширение POSIX , а не C99 .
Это поле позволяет использовать одно и то же значение несколько раз в строке формата вместо того, чтобы передавать значение несколько раз. Если спецификатор включает это поле, то последующие спецификаторы также должны его включать.
Например,
printf ( "%2$d %2$#x; %1$d %1$#x" , 16 , 17 )
выходы: 17 0x11; 16 0x10
.
Это поле особенно полезно для локализации сообщений на разных естественных языках , которые часто используют разный порядок слов.
В Microsoft Windows поддержка этой возможности осуществляется через другую функцию — printf_p
.
Поле флагов может содержать ноль или более значений (в любом порядке):
Характер | Описание |
---|---|
- (минус) | Выровняйте вывод этого заполнителя по левому краю. (По умолчанию вывод выравнивается по правому краю.) |
+ (плюс) | Добавляет плюс к положительным знаковым числовым типам. positive = + , negative = - . (По умолчанию ничего не добавляется перед положительными числами.) |
(космос) | Добавляет пробел для положительных знаковых числовых типов. positive = , negative = - . Этот флаг игнорируется, если существует флаг + . (По умолчанию ничего не добавляется перед положительными числами.) |
0 (ноль) | Если указан параметр «ширина», для числовых типов добавляются нули. (По умолчанию добавляются пробелы.) Например, выдает , а выдает . printf("%4X",3) 3 printf("%04X",3) 0003 |
' (апостроф) | К целому числу или показателю десятичной дроби применяется разделитель тысяч. |
# (хеш) | Альтернативная форма: Для типов g и G конечные нули не удаляются. Для типов f , F , e , E , g , G вывод всегда содержит десятичную точку. Для типов o , x , X текст 0 , 0x , 0X , соответственно, добавляется к ненулевым числам. |
Поле ширины указывает минимальное количество символов для вывода. Если значение может быть представлено меньшим количеством символов, то значение дополняется пробелами слева, так что вывод равен указанному количеству символов. Если значение требует больше символов, то вывод длиннее указанной ширины. Значение никогда не усекается.
Например, указывает ширину 3 и выводит с пробелом слева, чтобы вывести 3 символа. Вызов выводит, который имеет длину 4 символа, поскольку это минимальная ширина для этого значения, даже если указанная ширина равна 3.printf("%3d", 12)
12
printf("%3d", 1234)
1234
Если поле ширины опущено, выводится минимальное количество символов для значения.
Если поле указано как *
, то значение ширины считывается из списка значений в вызове. [18] Например, выводит , где второй параметр, 3, — это ширина (соответствует *), а 10 — это значение для сериализации (соответствует d).printf("%*d", 3, 10)
10
Хотя начальный ноль не является частью поля ширины, он интерпретируется как упомянутый выше флаг дополнения нулями, а отрицательное значение рассматривается как положительное значение в сочетании с -
упомянутым выше флагом выравнивания по левому краю.
Поле ширины можно использовать для форматирования значений в виде таблицы (табличный вывод). Но столбцы не выравниваются, если какое-либо значение больше, чем помещается в указанную ширину. Например, обратите внимание, что последнее значение строки (1234) не помещается в первый столбец шириной 3, и поэтому столбец не выравнивается.
1 1 12 12 123 123 1234 123
Поле точности обычно определяет максимальный предел вывода в зависимости от конкретного типа форматирования. Для числовых типов с плавающей точкой оно определяет количество цифр справа от десятичной точки, до которого вывод должен быть округлен. Для строкового типа оно ограничивает количество символов, которые должны быть выведены, после чего строка усекается.
Поле точности может быть опущено или может быть целочисленным значением или динамическим значением при передаче в качестве другого аргумента, если указано звездочкой *
. Например, выводитprintf("%.*s", 3, "abcdef")
азбука.
Поле длины может быть опущено или иметь одно из следующих значений:
Характер | Описание |
---|---|
чч | Для целочисленных типов заставляет printf ожидать целочисленный аргумент размера int , который был повышен из char . |
час | Для целочисленных типов заставляет printf ожидать целочисленный аргумент размера int , который был повышен из short . |
л | Для целочисленных типов заставляет printf ожидать длинный целочисленный аргумент. Для типов с плавающей точкой это игнорируется. Аргументы float всегда повышаются до double при использовании в вызове varargs. [19] |
лл | Для целочисленных типов заставляет printf ожидать длинный целочисленный аргумент. |
Л | Для типов с плавающей точкой заставляет printf ожидать длинный двойной аргумент. |
з | Для целочисленных типов заставляет printf ожидать целочисленный аргумент размером size_t . |
дж | Для целочисленных типов заставляет printf ожидать целочисленный аргумент размером intmax_t . |
т | Для целочисленных типов заставляет printf ожидать целочисленный аргумент размером ptrdiff_t . |
Варианты длины, зависящие от платформы, появились еще до широкого использования расширений ISO C99, в том числе:
Персонажи | Описание | Часто встречающиеся платформы |
---|---|---|
я | Для знаковых целых типов printf ожидает целочисленный аргумент размером ptrdiff_t ; для беззнаковых целых типов printf ожидает целочисленный аргумент размером size_t . | Win32/Win64 |
И32 | Для целочисленных типов заставляет printf ожидать 32-битный (двойное слово) целочисленный аргумент. | Win32/Win64 |
И64 | Для целочисленных типов заставляет printf ожидать 64-битный (четверное слово) целочисленный аргумент. | Win32/Win64 |
д | Для целочисленных типов заставляет printf ожидать 64-битный (четверное слово) целочисленный аргумент. | БСД |
ISO C99 включает inttypes.h
заголовочный файл, который включает ряд макросов для платформенно-независимого printf
кодирования. Например: определяет десятичный формат для 64-битного знакового целого числа. Поскольку макросы вычисляются как строковый литерал, а компилятор объединяет соседние строковые литералы, выражение компилируется в одну строку.printf("%" PRId64, t);
"%" PRId64
Макросы включают в себя:
Макро | Описание |
---|---|
PRId32 | Обычно эквивалентно I32d ( Win32/Win64 ) или d |
PRId64 | Обычно эквивалентен I64d ( Win32/Win64 ), lld ( 32-битные платформы ) или ld ( 64-битные платформы ) |
ПРИи32 | Обычно эквивалентно I32i ( Win32/Win64 ) или i |
ПРИи64 | Обычно эквивалентен I64i ( Win32/Win64 ), lli ( 32-битные платформы ) или li ( 64-битные платформы ) |
ПРИу32 | Обычно эквивалентно I32u ( Win32/Win64 ) или u |
ПРИу64 | Обычно эквивалентно I64u ( Win32/Win64 ), llu ( 32-битные платформы ) или lu ( 64-битные платформы ) |
PRIx32 | Обычно эквивалентно I32x ( Win32/Win64 ) или x |
PRIx64 | Обычно эквивалентно I64x ( Win32/Win64 ), llx ( 32-битные платформы ) или lx ( 64-битные платформы ) |
Поле типа может быть любым из:
Характер | Описание |
---|---|
% | Печатает буквенный символ % (этот тип не принимает никаких флагов, полей ширины, точности и длины). |
д , я | int как знаковое целое число . %d и %i являются синонимами для выходных данных, но различаются при использовании с входными данными (при использовании %i число будет интерпретироваться как шестнадцатеричное, если ему предшествует 0x , и как восьмеричное, если ему предшествует 0 ).scanf |
ты | Распечатать десятичное беззнаковое целое число . |
ж , Ж | double в обычной ( с фиксированной точкой ) нотации. f и F отличаются только тем, как выводятся строки для бесконечного числа или NaN ( inf , infinity и nan для f ; INF , INFINITY и NAN для F ). |
е , Е | Значение double в стандартной форме ( d . ddd e± dd ). Преобразование E использует букву E (а не e ) для представления показателя степени. Показатель всегда содержит не менее двух цифр; если значение равно нулю, показатель равен 00 . В Windows показатель степени по умолчанию содержит три цифры, например, 1.5e002 , но это можно изменить с помощью специфичной для Microsoft функции._set_output_format |
гарантированная победа | double в нормальной или экспоненциальной записи, в зависимости от того, какая из них больше подходит для его величины. g использует строчные буквы, G использует заглавные буквы. Этот тип немного отличается от записи с фиксированной точкой тем, что незначащие нули справа от десятичной точки не включаются. Кроме того, десятичная точка не включается в целые числа. |
х , Х | unsigned int как шестнадцатеричное число. x использует строчные буквы, а X использует заглавные. |
о | беззнаковое целое число в восьмеричном формате. |
с | Строка с нулевым завершением . |
с | char (персонаж). |
п | void* (указатель на void) в формате, определяемом реализацией. |
а , А | double в шестнадцатеричной системе счисления, начинающейся с 0x или 0X . a использует строчные буквы, A использует заглавные буквы. [20] [21] (в C++11 iostreams есть hexfloat , который работает так же). |
н | Ничего не выводить, а записывать количество символов, записанных на данный момент, в параметр указателя целого числа. В Java это выводит новую строку. [22] |
Распространенным способом обработки форматирования с использованием пользовательского типа данных является форматирование значения пользовательского типа данных в строку, а затем использование %s
спецификатора для включения сериализованного значения в более крупное сообщение.
Некоторые функции типа printf допускают расширения мини-языка на основе escape-символов , что позволяет программисту использовать определенную функцию форматирования для не встроенных типов. Одна из них — это register_printf_function() из glibc (теперь устаревшая ). Однако она редко используется из-за того, что конфликтует со статической проверкой формата строки. Другая — это пользовательские форматировщики Vstr, которые позволяют добавлять многосимвольные имена форматов.
Некоторые приложения (например, Apache HTTP Server ) включают собственную функцию, похожую на printf, и встраивают в нее расширения. Однако все они, как правило, имеют те же проблемы, что и register_printf_function()
.
Функция ядра Linux printk
поддерживает несколько способов отображения структур ядра с использованием общей %p
спецификации путем добавления дополнительных символов формата. [23] Например, %pI4
печатает адрес IPv4 в десятичной форме с точками. Это позволяет проводить статическую проверку строки формата (части %p
) за счет полной совместимости с обычной printf.
Варианты printf
предоставляют функции форматирования, но с дополнительным или немного отличающимся поведением.
fprintf
выводит в системный файловый объект, который допускает вывод на устройство, отличное от стандартного вывода.
sprintf
записывает в строковый буфер вместо стандартного вывода.
snprintf
обеспечивает определенный уровень безопасности, sprintf
поскольку вызывающая сторона предоставляет параметр длины (n), который указывает максимальное количество символов для записи в буфер.
Для большинства функций семейства printf есть вариант, который принимает va_list
вместо списка параметров переменной длины. Например, есть vfprintf
, vsprintf
, vsnprintf
.
Дополнительные параметры значения игнорируются, но если строка формата имеет больше спецификаторов формата, чем переданных параметров значения, поведение не определено . Для некоторых компиляторов C дополнительный спецификатор формата приводит к потреблению значения, даже если его нет. Это может позволить провести атаку на строку формата . Обычно для C аргументы передаются в стеке. Если передано слишком мало аргументов, то printf может выполнить чтение за пределами стекового фрейма, что позволяет злоумышленнику прочитать стек.
Некоторые компиляторы, такие как GNU Compiler Collection , статически проверяют строки формата printf-подобных функций и предупреждают о проблемах (при использовании флагов -Wall
или ). GCC также предупреждает о пользовательских функциях в стиле printf, если к функции применен -Wformat
нестандартный «формат» .__attribute__
Форматная строка часто является строковым литералом , что позволяет статический анализ вызова функции. Однако форматная строка может быть значением переменной, что позволяет выполнять динамическое форматирование, но также является уязвимостью безопасности, известной как неконтролируемый эксплойт форматной строки .
Хотя на первый взгляд это функция вывода, printf
она позволяет записывать в область памяти, указанную аргументом через %n
. Эта функциональность иногда используется как часть более сложных атак форматной строки. [24]
Функциональность %n
также делает printf
случайным образом Тьюринг-полным даже с хорошо сформированным набором аргументов. Игра в крестики-нолики, записанная в строке формата, является победителем 27-го IOCCC . [25]
Известные языки программирования, включающие printf или подобную printf функциональность.
Исключены языки, использующие строки формата, которые отклоняются от стиля, описанного в этой статье (например, AMPL и Elixir ), языки, которые наследуют свою реализацию от JVM или другой среды (например, Clojure и Scala ), а также языки, которые не имеют стандартной собственной реализации printf, но имеют внешние библиотеки, эмулирующие поведение printf (например, JavaScript ).
%
оператор) [28]printf
, sprintf
, и fmt
)print()
и FileStream.printf()
)iostream
printf
(Юникс)printk
(вывести сообщения ядра)scanf