Share to: share facebook share twitter share wa share telegram print page

Printf

printf (от англ. print formatted, «форматированная печать») — обобщённое название семейства функций или методов стандартных или широко известных коммерческих библиотек, или встроенных операторов некоторых языков программирования, используемых для форматного вывода — вывода в различные потоки значений разных типов, отформатированных согласно заданному шаблону. Этот шаблон определяется составленной по специальным правилам строкой (форматной строкой).

Наиболее ярким представителем этого семейства является функция printf, а также ряд других функций с производными от printf названиями в стандартной библиотеке языка Си (являющейся также частью стандартной библиотеки Си++ и Objective-C).

В операционных системах семейства UNIX имеется также утилита printf, служащая тем же целям форматного вывода.

Ранним прототипом такой функции можно считать оператор FORMAT языка Фортран. Функция вывода, управляемая строкой, появилась в предшественниках языка Си (BCPL и Би). В спецификации стандартной библиотеки языка Си она получила свой наиболее известный вид (с флагами, шириной, точностью и размером). Синтаксис строки шаблона вывода (называемой иногда строкой форматирования, строкой формата или форматной строкой) в дальнейшем начал использоваться и другими языками программирования (с вариациями ради приспособления к особенностям этих языков). Как правило, соответствующие функции этих языков также носят название printf и/или производные от него.

Некоторые более поздние среды программирования (например, .NET) также используют концепцию вывода, управляемого форматной строкой, но с иным синтаксисом.

История

Появление

Уже Фортран I имел операторы, обеспечивавшие форматный вывод. Синтаксис операторов WRITE и PRINT предусматривал метку, отсылающую к неисполняемому оператору FORMAT, который содержал форматную спецификацию. Спецификаторы были частью синтаксиса оператора, и компилятор сразу мог сформировать код, непосредственно выполняющий форматирование данных, что обеспечивало наилучшую производительность на вычислительных машинах тех времён. Однако имелись следующие недостатки:

  • Усложнение без того запутанного синтаксиса языка.
  • Форматную спецификацию невозможно было сформировать в ходе исполнения программы.
  • Форматная спецификация была текстуально отделена от оператора вывода, что было неудобно для сопровождения программ.

Первый прототип будущей функции printf появляется в языке BCPL в 1960-х. Функция WRITEF принимает строку форматирования, в которой тип данных указывается отдельно от самих данных в строковой переменной (тип указывался без полей флагов, ширины, точности и размера, но уже предварялся символом процента %).[1] Основной целью появления строки форматирования была передача типов аргументов (в языках программирования со статической типизацией определение типа переданного аргумента для функции с нефиксированным списком формальных параметров требует сложного и неэффективного для общего случая механизма передачи информации о типах). Сама функция WRITEF была средством упрощения вывода: вместо набора функций WRCH (вывод символа), WRITES (вывод строки), WRITEN, WRITED, WRITEOCT, WRITEHEX (вывод чисел в различной форме) использовался единый вызов, в котором можно было чередовать «просто текст» с выходными значениями.

Появившийся вслед за ним в 1969 году язык Би уже использовал название printf с простейшей строкой форматирования (аналогичной BCPL), указывался только один из трёх возможных типов и два представления числа: десятичные (%d), восьмеричные (%o), строки (%s) и символы (%c), а единственной возможностью по форматированию вывода в этих функциях было добавление символов до и после вывода значения переменной.[2]

Си и производные

С момента появления первого варианта языка Си (1970) семейство printf стало основным средством форматного вывода. Затраты на разбор форматной строки при каждом вызове функции были сочтены приемлемыми, и альтернативные вызовы для каждого типа в отдельности в библиотеку введены не были. Спецификация функции вошла в оба существующих стандарта языка, вышедших в 1990 и 1999 годах. Спецификация 1999 года содержит некоторые нововведения по отношению к спецификации 1990 года.

Язык Си++ использует стандартную библиотеку Си (в соответствии со стандартом 1990 года), в том числе и всё семейство printf.

В качестве альтернативы стандартная библиотека Си++ предложила набор классов потокового ввода и вывода. Операторы вывода этой библиотеки типобезопасны и не требуют разбора форматных строк при каждом вызове. Тем не менее, многие программисты продолжают пользоваться семейством printf, так как запись последовательности вывода с ними обычно оказывается компактнее, а суть применяемого формата — яснее.

Objective-C является довольно «тонкой» надстройкой над Си, и программы на нём могут напрямую пользоваться функциями семейства printf.

Использование в других языках программирования

Помимо Си и его производных (Си++, Objective-C), printf-подобный синтаксис строки форматирования используют многие другие языки программирования:

  • Perl — sprintf[3];
  • Python — форматирующий оператор %[4] и функция str.format;
  • PHP — функция printf[5];
  • Java — функция java.io.PrintStream.printf[6];
  • Ruby — функция printf[7];
  • Lua — функция string.format[8];
  • TCL — функция format[9];
  • GNU Octave — функция printf[10];
  • Maple — функция printf (встроенный язык)[11];
  • AMPL[12];
  • Elisp (format string &rest objects)[13];
  • OCaml — функция printf[14];
  • Haskell — функция printf[15];
  • Rust — макрос println![16];
  • Pascal — функция Format[17].
  • Go — функция fmt.Printf (fmt.Sprintf для вывода в строку)[18]

Помимо этого, благодаря наличию утилиты printf в составе большинства UNIX-подобных систем, printf используется во многих shell-скриптах (для sh, bash, csh, zsh и т. д.).

Последователи

Некоторые более поздние языки и среды программирования также используют концепцию вывода управляемого форматной строкой, но с иным синтаксисом.

Например, в основной библиотеке классов (FCL) среды .Net имеется семейство методов System.String.Format, System.Console.Write и System.Console.WriteLine, некоторые перегрузки которых выполняют вывод своих данных в соответствии с форматной строкой. Так как в среде исполнения .Net доступна полная информация о типах объектов, необходимости передавать эту информацию в форматной строке нет.

Именование функций семейства

Все функции имеют в имени основу printf. Префиксы перед именем функции означают:

  • v (vprintf, vsnprintf и т. д.) — функция вместо переменного числа параметров принимает параметр типа va_list, хранящий произвольное количество аргументов.
  • f (fprintf, vfprintf) — вывод результата в передаваемый через параметр функции поток, вместо стандартного вывода.
  • s (sprintf, snprintf, vsprintf, vsnprintf) — запись результата в строку (буфер в памяти), а не поток.
  • n (snprintf, vsnprintf) — наличие параметра, ограничивающего максимальное количество символов для записи результата (используется только вместе с префиксом s). В Maple функция nprintf аналогична sprintf, но возвращает не текстовую строку, а имя.
  • w, перед остальными префиксами (wsprintf, wvsprintf, wnsprintf, wvnsprintf) — использующийся фирмой Microsoft префикс для реализаций семейства функций sprintf в операционных системах Windows.
  • w, после остальных префиксов (fwprintf, swprintf, wprintf) — функция использует строки из «широких» символов (wchar_t) вместо обычных строк. (при этом функция swprintf не имеет префикса «n», хотя и принимает параметр, ограничивающий размер результирующей строки).
  • a (asprintf, vasprintf) — расширения GNU; функции, аналогичные sprintf/vsprintf, но выделяющие достаточный объём памяти с помощью malloc для форматированной строки. Вместо указателя на строку эти функции принимают указатель на указатель на строку, освобождение памяти производит вызвавшая функция.

Общие соглашения

Все функции в качестве одного из параметров (format) принимают строку форматирования (описание синтаксиса строки ниже). Возвращают количество записанных (выведенных) символов, не включая нулевой символ в конце. Количество аргументов, содержащих данные для форматированного вывода, должно быть не менее, чем упомянуто в строке форматирования. «Лишние» аргументы игнорируются.

Функции семейства n (snprintf, vsnprintf) возвращают количество символов, которое было бы выведено, если бы параметр n (ограничивающий количество выводимых символов) был достаточно большим. В случае однобайтовых кодировок возвращаемое значение соответствует необходимому размеру строки (не включая нулевой символ в конце).

Функции семейства s (sprintf, snprintf, vsprintf, vsnprintf) первым параметром (s) принимают указатель на область памяти, куда будет записана результирующая строка. Функции, не имеющие ограничения по количеству записываемых символов, являются небезопасными функциями, так как могут привести к ошибке переполнения буфера, в случае, если выводимая строка окажется больше, чем размер выделенной для вывода области памяти.

Функции семейства f записывают строку в любой открытый поток (параметр stream), в частности, в стандартные потоки вывода (stdout, stderr). fprintf(stdout, format, …) эквивалентно printf(format, …).

Функции семейства v принимают аргументы не в виде переменного числа аргументов (как все остальные printf-функции), а в виде списка va list. При этом при вызове функции макрос va end не выполняется.

Функции семейства w (первым символом) являются ограниченной реализацией Microsoft семейства функций s: wsprintf, wnsprintf, wvsprintf, wvnsprintf. Эти функции реализованы в динамических библиотеках user32.dll и shlwapi.dll (n функции). Они не поддерживают вывод значений с плавающей запятой, кроме того, wnsprintf и wvnsprintf поддерживают выравнивание текста только по левому краю.

Функции семейства w (wprintf, swprintf) реализуют поддержку многобайтовых кодировок, все функции этого семейства работают с указателями на многобайтные строки (wchar_t).

Функции семейства a (asprintf, vasprintf) выделяют память для строки вывода при помощи функции malloc, освобождение памяти производится в вызвавшей процедуре, в случае ошибки при выполнении функции память не выделяется.

Описание функций

Имена параметров

  • format — строка форматирования (формат описан ниже)
  • stream — файловый поток для вывода
  • s — строка для помещения результата работы функции
  • n — переменная, содержащая максимальное допустимое количество символов для строки s
  • ap — список значений для вывода
  • strp — указатель на указатель на строку для помещения результатов работы функции

Описание функций

  • int printf( const char *format, ... );
    Вывод форматированной строки на стандартный вывод
  • int fprintf( FILE *stream, const char *format, ... );
    Вывод форматированной строки в поток
  • int sprintf( char *s, const char *format, ... );
    Запись форматированной строки в строку без ограничения по размеру строки
  • int snprintf( char *s, size_t n, const char *format, ... );
    Запись форматированной строки в строку с ограничением по размеру строки
  • int vprintf( const char *format, va_list ap );
    Вывод форматированной строки на стандартный вывод, значения для вывода передаются в функцию в виде списка va list
  • int vfprintf( FILE *stream, const char *format, va_list ap );
    Запись форматированной строки в поток, значения для вывода передаются в функцию в виде списка va list
  • int vsprintf( char *s, const char *format, va_list ap );
    Запись форматированной строки в строку без ограничения размера; значения для вывода передаются в функцию в виде списка va list
  • int vsnprintf( char *s, size_t n, const char *format, va_list ap );
    Запись форматированной строки в строку с ограничением на количество выводимых символов. Значения для вывода передаются в функцию в виде списка va list
  • int fwprintf(FILE *stream, const wchar_t *format, ... );
    Запись форматированной строки многобайтовых символов в файл
  • int swprintf(wchar_t *ws, size_t n, const wchar_t *format, ... );
    Запись форматированной многобайтовой строки в область памяти (Примечание: несмотря на отсутствие буквы n в названии, эта функция принимает параметр, ограничивающий максимальный размер выходной строки).
  • int wprintf(const wchar_t *format, ... );
    Вывод многобайтовой форматированной строки на терминал
  • int wsprintf( LPTSTR s, LPCTSTR format, ... );
    Реализация функции sprintf в операционной системе Windows. В отличие от sprintf, не поддерживает вывод значений с плавающей запятой, вывод указателей, дополнительно поддерживает обработку многобайтовых строк в однобайтовой версии функции), не поддерживает флаг '+'.
  • int wnsprintf( LPTSTR s, int n, LPCTSTR format, ... );
    Реализация функции snprintf в операционной системе Windows. Не поддерживает вывод значений с плавающей запятой и указателей, поддерживает только флаг выравнивания по левому краю.
  • int wvsprintf( LPTSTR s, LPCTSTR format, va_list ap );
  • int wvnsprintf( LPTSTR s, int n, LPCTSTR format, va_list ap );
  • int asprintf(char **strp, const char *format, ... );
    функция записывает результат в строку, память для которой выделяется при помощи malloc
  • int vasprintf(char **strp, const char *format, va_list ap);
    функция записывает результат в строку, память для которой выделяется при помощи malloc, значения для вывода передаются в функцию в виде списка va list

Возвращаемое значение: отрицательное значение — признак ошибки; в случае успеха функции возвращают количество записанных/выведенных байтов (без учёта нулевого байта в конце), функция snprintf выводит количество байт, которые были бы записаны, если бы n было достаточного размера.

При вызове snprintf, n может быть равно нулю (в этом случае s может быть нулевым указателем), в этом случае запись не производится, функция только возвращает правильное возвращаемое значение.

Синтаксис строки форматирования

Пример вызова printf с комментариями

В языке Си и Си++ строка форматирования представляет собой строку, завершённую нулевым символом. Все символы, кроме спецификаторов формата, копируются в итоговую строку без изменений. Стандартным признаком начала спецификатора формата является символ % (Знак процента), для вывода самого знака % используется его удвоение %%.

Структура спецификатора формата

Спецификатор формата имеет вид:

%[флаги][ширина][.точность][размер]тип

Обязательными составными частями являются символ начала спецификатора формата (%) и тип.

Флаги

Знак Название знака Значение В отсутствие этого знака Примечание
- минус выводимое значение выравнивается по левому краю в пределах минимальной ширины поля по правому
+ плюс всегда указывать знак (плюс или минус) для выводимого десятичного числового значения только для отрицательных чисел
  пробел помещать перед результатом пробел, если первый символ значения не знак Вывод может начинаться с цифры. Символ + имеет больший приоритет, чем пробел. Используется только для знаковых десятичных значений.
# решётка «альтернативная форма» вывода значения При выводе чисел в шестнадцатеричном или восьмеричном форматах перед числом будет указываться особенность формата (0x или 0 соответственно).
0 ноль дополнять поле до ширины, указанной в поле ширина управляющей последовательности, символом 0 дополнять пробелами Используется для типов d, i, o, u, x, X, a, A, e, E, f, F, g, G. Для типов d, i, o, u, x, X, если точность указана, этот флаг игнорируется. Для остальных типов поведение не определено.

Если указан флаг минус '-', этот флаг так же игнорируется.

Модификатор ширины

Ширина (десятичное число или символ звёздочка) указывает минимальную ширину поля (включая знак для чисел). Если представление величины больше, чем ширина поля, то запись выходит за пределы поля (например, %2i для величины 100 даст значение поля в три символа), если представление величины менее указанного числа, то оно будет дополнено (по умолчанию) пробелами слева, поведение может меняться в зависимости от других заданных флагов. Если в качестве ширины указана звёздочка, ширина поля указывается в списке аргументов перед значением для вывода (например, printf( "%0*x", 8, 15 ); выведет текст 0000000f). Если этим способом указан отрицательный модификатор ширины, считается что выставлен флаг -, а значение модификатора ширины установлено абсолютным.

Модификатор точности

  • указывает на минимальное количество символов, которое должно появиться при обработке типов d, i, o, u, x, X;
  • указывает на минимальное количество символов, которое должно появиться после десятичной запятой (точки) при обработке типов a, A, e, E, f, F;
  • максимальное количество значащих символов для типов g и G;
  • максимальное число символов, которые будут выведены для типа s;

Точность задаётся в виде точки с последующим десятичным числом или звёздочкой (*), если число или звёздочка отсутствует (присутствует только точка), то предполагается, что число равно нулю. Точка для указания точности используется даже в том случае, если при выводе чисел с плавающей запятой выводится запятая.

Если после точки указан символ «звёздочка», то при обработке строки форматирования значение для поля читается из списка аргументов. (При этом, если символ звёздочка и в поле ширины и в поле точности, сначала указывается ширина, потом точность и лишь потом значение для вывода). Например, printf( "%0*.*f", 8, 4, 2.5 ); выведет текст 002.5000. Если этим способом указан отрицательный модификатор точности, то считается, что модификатор точности отсутствует.[19]

Модификатор размера

Поле размер позволяет указать размер данных, переданных функции. Необходимость в этом поле объясняется особенностями передачи произвольного количества параметров в функцию в языке Си: функция не может «самостоятельно» определить тип и размер переданных данных, так что информация о типе параметров и точном их размере должна передаваться явно.

Рассматривая влияние размерных спецификаций на форматирование целочисленных данных, нужно отметить, что в языках Си и Си++ имеется цепочка пар знаковых и беззнаковых целых типов, которые в порядке неубывания размеров располагаются так:

Знаковый тип Беззнаковый тип
signed char unsigned char
signed short (short) unsigned short int (unsigned short)
signed int (int) unsigned int (unsigned)
signed long int (long) unsigned long int (unsigned long)
signed long long int (long long) unsigned long long int (unsigned long long)

Точные размеры типов неизвестны, за исключением типов signed char и unsigned char.

Парные знаковый и беззнаковый типы имеют одинаковый размер, а значения, представимые в обоих типах, имеют в них одинаковое представление.

Тип char имеет одинаковый размер с типами signed char и unsigned char и общий набор представимых значений с одним из этих типов. Далее считается, что char — другое имя одного из этих типов; такое допущение приемлемо для настоящего рассмотрения.

Кроме того, в языке Си присутствует тип _Bool, а в Си++ — bool.

При передаче в функцию аргументов, которым не соответствуют формальные параметры в прототипе функции (а таковыми являются все аргументы, содержащие выводимые значения), эти аргументы подвергаются стандартным продвижениям, а именно:

  • аргументы типа float приводятся к типу double;
  • аргументы типов unsigned char, unsigned short, signed char и short приводятся к одному из следующих типов:
    • int, если этот тип способен представить все значения исходного типа, или
    • unsigned в противном случае;
  • аргументы типов _Bool или bool приводятся к типу int.

Для краткости изложения ниже для обозначения типа результата стандартного продвижения типа T используется нотация PROMOTED(T).

Таким образом, функции printf не могут получать аргументов типов float, _Bool или bool или целочисленных типов, меньших, чем int или unsigned.

Набор применяемых спецификаторов размера зависит от спецификатора типа (см. ниже).

Спецификатор %d, %i %o, %u, %x, %X %n Примечание
отсутствует int unsigned int указатель на int
l long int unsigned long int указатель на long int
hh Аргумент имеет тип PROMOTED(signed char), но принудительно приводится к типу signed char Аргумент имеет тип PROMOTED(unsigned char), но принудительно приводится к типу unsigned char указатель на signed char

Формально существуют в языке Си начиная со стандарта от 1999 года, а в языке Си++ — начиная со стандарта от 2011 года.

В некоторых случаях компиляторы и среды программирования, выпущенные до издания соответствующих стандартов, занимают эти спецификаторы для других целей. Вместо этого они могут предоставлять другие спецификаторы.

h Аргумент имеет тип PROMOTED(short int), но принудительно приводится к типу short int Аргумент имеет тип PROMOTED(unsigned short int), но принудительно приводится к типу unsigned short int указатель на short int
ll long long int unsigned long long int указатель на long long int
j intmax_t uintmax_t указатель на intmax_t
z эквивалентный по размеру size_t знаковый тип size_t указатель на эквивалентный по размеру size_t знаковый тип (sic!)
t ptrdiff_t эквивалентный по размеру ptrdiff_t беззнаковый тип указатель на ptrdiff_t

Спецификации h и hh используются для компенсации стандартных продвижений типов в сочетании с переходами от знаковых типов к беззнаковым, или наоборот.

Например, рассмотрим реализацию Си, где тип char знаковый и имеет размер 8 бит, тип int имеет размер 32 бит, используется дополнительный способ кодирования отрицательных целых.

char c = 255;
printf("%X", c);

Такой вызов даст вывод FFFFFFFF, что, возможно, не то, чего ожидал программист. Действительно, значение c равно (char)(-1), а после продвижения типа оно оказывается равно -1. Применение формата %X вызывает интерпретацию данного значения как беззнакового, то есть 0xFFFFFFFF.

char c = 255;
printf("%X", (unsigned char)c);
char c = 255;
printf("%hhX", c);

Эти два вызова имеют один и тот же эффект и дают вывод FF. Первый вариант позволяет избежать размножения знака при продвижении типа, второй — компенсирует его уже «внутри» функции printf.

Спецификатор %a, %A, %e, %E, %f, %F, %g, %G
отсутствует double
L long double
Спецификатор %c %s
отсутствует Аргумент имеет тип int или unsigned int, но принудительно приводится к типу char char*
l Аргумент имеет тип wint_t, но принудительно приводится к типу wchar_t wchar_t*

Спецификатор типа

Тип указывает не только на тип величины (с точки зрения языка программирования Си), но и на конкретное представление выводимой величины (например, числа могут выводить в десятичном или шестнадцатеричном виде). Записывается в виде одного символа. В отличие от остальных полей, является обязательным. Максимальный поддерживаемый размер вывода от единичной управляющей последовательности составляет по стандартам как минимум 4095 символов; на практике большинство компиляторов поддерживают существенно большие объёмы данных.

Значения типов:

  • d, i — десятичное знаковое число, тип по умолчанию int. По умолчанию записывается с правым выравниванием, знак пишется только для отрицательных чисел. В отличие от функций семейства scanf, для функций семейства printf спецификации %d и %i полностью синонимичны;
  • o — восьмеричное беззнаковое число, тип по умолчанию unsigned int;
  • u — десятичное беззнаковое число, тип по умолчанию unsigned int;
  • x и X — шестнадцатеричное беззнаковое число, x использует маленькие буквы (abcdef), X большие (ABCDEF), тип по умолчанию unsigned int;
  • f и F — числа с плавающей запятой, тип по умолчанию double. По умолчанию выводятся с точностью 6, если число по модулю меньше единицы, перед десятичной точкой пишется 0. Величины ±∞ представляются в форме [-]inf или [-]infinity (в зависимости от платформы); величина Nan представляется как [-]nan или [-]nan(любой текст далее). Использование F выводит указанные величины заглавными буквами ([-]INF, [-]INFINITY, NAN).
  • e и E — числа с плавающей запятой в экспоненциальной форме записи (вида 1.1e+44), тип по умолчанию double. e выводит символ «e» в нижнем регистре, E — в верхнем (3.14E+0);
  • g и G — число с плавающей запятой, тип по умолчанию double. Форма представления зависит от значения величины (f или e). Формат немного отличается от числа с плавающей запятой тем, что незначащие нули справа от запятой не выводятся. Также часть с запятой не отображается, если число целое;
  • a и A (начиная со стандартов языка Си от 1999 года и Си++ от 2011 года) — число с плавающей запятой в шестнадцатеричном виде, тип по умолчанию double;
  • c — вывод символа с кодом, соответствующим переданному аргументу, тип по умолчанию int;
  • s — вывод строки с нулевым завершающим байтом; если модификатор длины — l, выводится строка wchar_t*. В Windows значения типа s зависят от типа используемых функций. Если используется семейство printf функций, то s обозначает строку char*. Если используется семейство wprintf функций, то s обозначает строку wchar_t*.
  • S — то же самое, что и s с модификатором длины l; В Windows значения типа S зависит от типа используемых функций. Если используется семейство printf функций, то S обозначает строку wchar_t*. Если используется семейство wprintf функций, то S обозначает строку char*.
  • p — вывод указателя, внешний вид может существенно различаться в зависимости от внутреннего представления в компиляторе и платформе (например, 16-битная платформа MS-DOS использует форму записи вида FFEC:1003, 32-битная платформа с плоской адресацией использует адрес вида 00FA0030);
  • n — запись по указателю, переданному в качестве аргумента, количества символов, записанных на момент появления командной последовательности, содержащей n;
  • % — символ для вывода знака процента (%), используется для возможности вывода символов процента в строке printf, всегда используется в виде %%.
Вывод чисел с плавающей запятой

В зависимости от текущей локали, при выводе чисел с плавающей запятой может использоваться как запятая, так и точка (а, возможно, и другой символ). Поведение printf в отношении разделяющего дробную и целую часть числа символа определяется использующейся локалью (точнее, переменной LC NUMERIC).[20]

Специальные макросы для расширенного набора псевдонимов целочисленных типов данных

Второй стандарт языка Си (1999) предусматривает расширенный набор псевдонимов целочисленных типов данных intN_t, uintN_t, int_leastN_t, uint_leastN_t, int_fastN_t, uint_fastN_t (где N — требуемая разрядность), intptr_t, uintptr_t, intmax_t, uintmax_t.

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

int64_t x = 100000000000;
int width=20;
printf("%0*lli", width, x);
Неправильно, так как тип int64_t может не совпадать с типом long long int.

Для того, чтобы иметь возможность переносимым и удобным образом выводить значения объектов или выражений этих типов, реализация определяет для каждого из этих типов набор макросов, значением которых являются строки, объединяющие спецификации размера и типа.

Имена макросов имеют следующий вид:

Пара знакового и беззнакового типов Имя макроса
intN_t и uintN_t PRITN
int_leastN_t и uint_leastN_t PRITLEASTN
int_fastN_t и uint_fastN_t PRITFASTN
intmax_t и uintmax_t PRITMAX
intptr_t и uintptr_t PRITPTR

Здесь T — одна из следующих спецификаций типа: d, i, u, o, x, X.

int64_t x = 100000000000;
int width=20;
printf("%0*"PRIi64, width, x);
Правильный способ вывода значения типа int64_t в языке Си.

Можно заметить, что для типов intmax_t и uintmax_t имеется стандартный спецификатор размера j, поэтому макрос PRITMAX, скорее всего, всегда определён как "jT".

XSI-расширения в стандарте Single Unix

В рамках стандарта Single UNIX (практически эквивалентного стандарту POSIX), определены следующие дополнения printf по отношению к ISO C, в рамках расширения XSI (X/Open System Interface):

  • Добавляется возможность вывода произвольного по номеру параметра (указывается в виде n$ сразу после символа начала управляющей последовательности, например, printf("%1$d:%2$.*3$d:%4$.*3$d\n", hour, min, precision, sec);).
  • Добавлен флаг «'» (одинарная кавычка), который для типов d, i, o, u предписывает разделять классы соответствующим символом.
  • тип C, эквивалентный lc ISO C (вывод символа типа wint_t).
  • тип S, эквивалентный ls ISO C (вывод строки типа wchar_t*)
  • Добавлены коды ошибок EILSEQ, EINVAL, ENOMEM, EOVERFLOW.

Нестандартные расширения

GNU C Library

В рамках GNU C Library (libc) добавлены следующие расширения:

  • тип m выводит значение глобальной переменной errno (код ошибки последней функции).
  • тип C эквивалентен lc.
  • флаг ' (одинарная кавычка) используется для разделения классов при выводе чисел. Формат разделения зависит от LC_NUMERIC
  • размер q указывает на тип long long int (на системах, где не поддерживается тип long long int, это то же самое, что и long int
  • размер Z является псевдонимом для z, был введён в libc до появления стандарта C99, не рекомендуется к использованию в новом коде.
Регистрация собственных типов

GNU libc поддерживает регистрацию пользовательских типов, позволяя программисту определять формат вывода для собственных структур данных. Для регистрации нового типа используется функция
int register_printf_function (int type, printf_function handler-function, printf_arginfo_function arginfo-function), где:

  • type — буква для типа (если type = 'Y', то вызов будет выглядеть как '%Y');
  • handler-function — указатель на функцию, которая вызывается printf-функциями, если в строке форматирования встречается тип, указанный в type;
  • arginfo-function — указатель на функцию, которая будет вызываться функцией parse_printf_format.

Помимо определения новых типов, регистрация позволяет переопределить существующие типы (такие, как s, i).

Microsoft Visual C

В составе Microsoft Visual Studio для языков программирования Си/Си++ в формате спецификации printf (и остальных функций семейства) предусмотрены следующие расширения:

  • поле размера:
значение поля тип
I32 signed __int32, unsigned __int32
I64 signed __int64, unsigned __int64
I ptrdiff_t, size_t
w эквивалентно l для строк и символов

Maple

В среде математических вычислений Maple также имеется функция printf, она имеет следующие особенности:

Форматирование
    • %a, %A: объект Maple будет выдан в текстовой нотации, это работает для всех объектов (например, матриц, функций, модулей и т. д.). Строчная буква предписывает окружать обратными апострофами символы (имена), которые должны быть окружены ими на входе printf.
    • %q, %Q: то же, что и %a/%A, но обрабатываться будет не один аргумент, а все начиная с того, которому соответствует флаг форматирования. Таким образом, флаг %Q/%q может стоять только последним в строке формата.
    • %m: форматировать объект в соответствии с его внутренним для Maple представлением. Практически используется для записи переменных в файл.

Пример:

> printf("%a =%A", `+`, `+`);
`+` = +
> printf("%a =%m", `+`, `+`);
`+` = I"+f*6"F$6#%(builtinGF$"$Q"F$F$F$F"%*protectedG
Вывод

Функция fprintf в maple в первом аргументе принимает либо дескриптор файла (возвращаемый функцией fopen), либо имя файла. В последнем случае имя должно иметь тип «символ», если имя файла содержит точки, то его обязательно нужно заключить в обратные апострофы или преобразовать функцией convert (имя_файла, symbol).

Уязвимости

Функции семейства printf принимают список аргументов и их размер отдельным параметром (в строке форматирования). Несоответствие строки форматирования и переданных аргументов может приводить к непредсказуемому поведению, повреждению стека и выполнению произвольного кода, приводить к разрушению областей динамической памяти. Многие функции семейства называются «небезопасными» (англ. unsafe), так как не имеют даже теоретической возможности для защиты от некорректных данных.

Также функции семейства s (без n, такие как sprintf, vsprintf) не имеют ограничителей по максимальному размеру записываемой строки и могут приводить к ошибке переполнения буфера (когда данные записываются за пределы отведённой области памяти).

Поведение при несоответствии строки форматирования и переданных аргументов

Стек при правильном вызове printf

В рамках соглашения о вызове cdecl, очистку стека осуществляет вызвавшая функция. При вызове printf аргументы (или указатели на них) помещаются в порядке следования записи (слева направо). По мере обработки строки форматирования функция printf читает аргументы со стека. Возможны следующие ситуации:

  • количество и тип аргументов совпадают с указанными в строке форматирования (нормальная работа функции)
  • в функцию передано больше аргументов, чем указано в строке форматирования (лишние аргументы)
  • в функцию передано меньше аргументов, чем требуется согласно строке форматирования (недостаток аргументов)
  • в функцию переданы аргументы неправильного размера
  • в функцию переданы аргументы правильного размера, но неправильного типа

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

Избыточное количество аргументов

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

Недостаточное количество аргументов

Если при вызове printf аргументов в стеке меньше, чем требуется для обработки строки форматирования, то недостающие аргументы читаются со стека, несмотря на то, что на стеке находятся произвольные данные (не имеющие отношения к работе printf). Если обработка данных прошла «успешно» (то есть не привела к прекращению работы программы, зависанию или записи в стек), после возврата в вызывающую функцию значение указателя стека возвращается в исходное, и работа программы продолжается.

При обработке «лишних» значений стека возможны следующие ситуации:

  • Успешное чтение «лишнего» параметра для вывода (число, указатель, символ и т. д.) — в результаты вывода помещается «почти случайное» значение, прочитанное со стека. Это не представляет собой опасности для работы программы, но может приводить к компрометации каких-либо данных (вывод значений стека, которые может использовать злоумышленник для анализа работы программы и получению доступа к внутренней/закрытой информации программы).
  • Ошибка при чтении значения со стека (например, в результате исчерпания доступных значений стека или доступ к «несуществующим» страницам памяти) — такая ошибка, вероятнее всего, приведёт к аварийному завершению работы программы.
  • Чтение указателя на параметр. Строки передаются с помощью указателя, при чтении «произвольной» информации со стека, прочитанное (почти случайное) значение используется как указатель на случайную область памяти. Поведение программы в этом случае не определено и зависит от содержимого этой области памяти.
  • Запись параметра по указателю (%n) — в данном случае поведение аналогично ситуации с чтением, но осложняется возможными сторонними эффектами записи в произвольную ячейку памяти.

Несоответствие типов аргументов

Формально любое несоответствие типа аргумента ожиданию вызывает неопределённое поведение программы. На практике можно выделить несколько особо интересных с точки зрения программистской практики случаев:

  • Аргумент имеет тот же тип, что и ожидается, но другой размер.
  • Аргумент имеет тот же размер, что и ожидается, но другой тип.

Иные случаи, как правило, приводят к очевидно некорректному поведению, и легко выявляются.

Несоответствие размера целочисленного аргумента или аргумента с плавающей точкой

Для целочисленного аргумента (при целочисленной же форматной спецификации) возможны следующие ситуации:

  • Передача параметров, размер которых больше ожидаемого (чтение меньшего из большего). При этом, в зависимости от принятого порядка байтов и направления роста стека, выведенное значение может как совпасть со значением аргумента, так и оказаться не имеющим к нему отношения.
  • Передача параметров, размер которых меньше ожидаемого (чтение большего из меньшего). В этом случае возможна ситуация, когда читаются области стека, выходящие за пределы переданных аргументов. Поведение функции в этом случае аналогично поведению в ситуации с недостатком параметров. В общем случае выводимое значение не соответствует ожидаемому.

Для вещественного аргумента (при вещественной же форматной спецификации) при любом несовпадении размеров выводимое значение, как правило, не совпадает с переданным.

Как правило, при ошибке размера какого-то одного аргумента правильная обработка всех последующих аргументов становится невозможной, так как в указатель на аргументы вносится ошибка. Однако этот эффект может быть нивелирован выравниванием значений на стеке.

Выравнивание значений на стеке

На многих платформах действуют правила выравнивания целочисленных и/или вещественных значений, требующие (или рекомендующие) их размещения по адресам, кратным их размеру. Эти правила распространяются и на передачу аргументов функций через стек. В этом случае ряд несовпадений типов ожидаемых и фактических параметров может оставаться незамеченным, создавая иллюзию правильности программы.

uint32_t a = 1;
uint64_t b = 2, c = 3;
printf("%"PRId64"%"PRId64"%"PRId64, b, a, c);
В данном примере фактическому параметру a типа uint32_t сопоставлена неверная форматная спецификация %"PRId64", предусмотренная для типа uint64_t. Тем не менее, на ряде платформ c 32-разрядным типом int, в зависимости от принятого порядка байт и направления роста стека, ошибка может оставаться незамеченной. Фактические параметры b и c будут выровнены по адресу, кратному их размеру (вдвое больше размера a). А «между» значениями a и b будет оставлено пустое (как правило, занулённое) пространство размером в 32 бита; при обработке спецификации %"PRId64" 32-разрядное значение a вместе с этим пустым пространством будут истолкованы как единое 64-разрядное значение.

Подобная ошибка может неожиданно проявиться при переносе программного кода на другую платформу, смене компилятора или режима компиляции.

Потенциальное несоответствие размеров

Определения языков Си и Си++ описывают лишь наиболее общие требования к размерам и представлению типов данных. Поэтому на многих платформах представление некоторых формально разных типов данных оказываются одинаковыми. Это приводит к тому, что некоторые несовпадения типов могут долгое время оставаться необнаруженными.

Например, на платформе Win32 общепринято, что размеры типов int и long int совпадают (32 разряда). Так, вызов printf("%ld", 1) или printf("%d", 1L) будет выполнен «правильно».

Подобная ошибка может неожиданно проявиться при переносе программного кода на другую платформу, смене компилятора или режима компиляции.

При написании программ на языке Си++ нужно внимательно относиться к выводу значений переменных, объявленных при помощи псевдонимов целочисленных типов, в частности size_t и ptrdiff_t; формальное определение стандартной библиотеки Си++ ссылается на первый стандарт языка Си (1990). Во втором стандарте языка Си (1999) для типов size_t и ptrdiff_t и для ряда иных типов определены спецификаторы размера для использования с подобными объектами. Многие реализации Си++ их также поддерживают.

size_t s = 1;
printf("%u", s);
Данный пример содержит ошибку, способную проявиться на платформах, где sizeof (unsigned int) не равно sizeof (size_t).
size_t s = 1;
printf("%zu", s);
Правильный способ вывести значение объекта типа size_t в языке Си.

Несоответствие типов при совпадении размеров

Если переданные аргументы совпадают по размеру, но имеют отличный тип, то часто работа программы будет «почти правильной» (не вызовет ошибок доступа к памяти), хотя выводимое значение, вероятнее всего, будет бессмысленным. Надо отметить, что смешение па́рных друг другу целочисленных типов (знакового и беззнакового) допустимо, неопределённого поведения не вызывает, и иногда сознательно используется в практике.

При использовании форматной спецификации %s значение аргумента целочисленного, вещественного типа или типа указателя иного, нежели char*, будет проинтерпретировано как адрес строки. Этот адрес, вообще говоря, может произвольно указывать на несуществующую или недоступную область памяти, что приведёт к ошибке доступа к памяти, либо на область памяти, не содержащую строки, что приведёт к выводу бессмыслицы, возможно, очень объёмному.

Уязвимость строки форматирования

Так как printf (и остальные функции семейства) могут выводить текст строки форматирования без изменений, если он не содержит управляющих последовательностей, то возможен вывод текста командой
printf(text_to_print);
В случае, если text_to_print получается из внешних источников (читается из файла, получается от пользователя или операционной системы), то наличие в получаемой строке знака процента может приводить к крайне нежелательным последствиям (вплоть до зависания программы).

Пример некорректного кода:
printf(" Current status: 99% stored.");
В этом примере содержится управляющая последовательность «% s», содержащая признак управляющей последовательности (%), флаг (пробел) и тип данных «строка» (s). Функция, приняв управляющую последовательность, попытается прочитать из стека указатель на строку. Так как функции не передавались дополнительные параметры, значение, которое будет прочитано со стека, не определено. Полученное значение будет интерпретировано как указатель на строку с завершающим нулём. Вывод такой «строки» может привести к выводу произвольного дампа памяти, ошибке доступа к памяти и разрушению стека. Такой тип уязвимости называют атакой на строку форматирования (англ. Format string attack).[21]

Переполнение буфера

Функция printf при выводе результата не ограничивается по максимальному количеству выводимых символов. Если в результате ошибки или недосмотра будет выведено больше символов, чем ожидалось, худшее, что может случиться — это «разрушение» картинки на экране. Созданная по аналогии с printf функция sprintf также не ограничивалась в максимальном размере результирующей строки. Однако, в отличие от «бесконечного» терминала, память, которую выделяет приложение для результирующей строки, всегда ограничена. И в случае выхода за ожидаемые рамки запись производится в области памяти, принадлежащие другим структурам данных (или, вообще, в недоступные участки памяти, что практически на всех платформах означает аварийное завершение программы). Запись в произвольные области памяти приводит к непредсказуемым эффектам (которые, возможно, проявятся много позже и не в форме ошибки программы, а в форме повреждения пользовательских данных). Отсутствие ограничения на максимальный размер строки является принципиальной ошибкой планирования при разработке функции. Именно из-за этого функции sprintf и vsprintf имеют статус небезопасных. Взамен им были разработаны функции snprintf, vsnprintf, принимающие дополнительный аргумент, ограничивающий максимальную результирующую строку. Появившаяся много позже функция swprintf (для работы с многобайтными кодировками) учитывает эту недоработку и принимает аргумент для ограничения результирующей строки. (Именно поэтому нет функции snwprintf).

Пример опасного вызова sprintf:

char buffer[65536];
char* name = get_user_name_from_keyboard();
sprintf(buffer, "User name:%s", name);

В вышеприведённом коде неявно предполагается, что пользователь не будет печатать 65 тысяч символов на клавиатуре, и буфера «должно хватить». Но пользователь может перенаправить ввод из другой программы или всё-таки ввести более 65 тысяч символов. В этом случае произойдёт повреждение областей памяти и поведение программы станет непредсказуемым.

Сложности в использовании

Отсутствие контроля типов

Функции семейства printf используют типы данных языка Си. Размеры этих типов и их соотношения могут меняться от платформы к платформе. Например, на 64-битных платформах в зависимости от выбранной модели (LP64, LLP64 или ILP64) размеры типов int и long могут различаться. Если программист задаст форматную строку «почти правильно», код будет работать на одной платформе и выдавать неправильный результат на другой (в ряде случаев, возможно, приводя к повреждению данных).

Например, код printf( "text address: 0x%X", "text line" ); работает правильно на 32-битной платформе (размер ptrdiff_t и размер int 32 бита) и на 64-битной модели IPL64 (где размеры ptrdiff_t и int 64 бита), но даст неверный результат на 64-битной платформе модели LP64 или LLP64, где размер ptrdiff_t 64 бита, а размер int 32 бита.[22]

В Oracle Java в аналоге функции printf применяются обёрнутые типы с динамической идентификацией,[6] в Embarcadero Delphi — промежуточная прослойка array of const,[23] в различных реализациях на C++[24] — перегрузка операций, в C++20 — вариативные шаблоны. К тому же форматы (%d, %f и т. д.) задают не тип аргумента, а лишь формат вывода, так что замена типа аргумента может вызвать аварийную ситуацию или нарушить высокоуровневую логику (например, «сломать» вёрстку таблицы) — но не испортить память.

Недостаточная стандартизация

Проблема усугубляется недостаточной стандартизацией форматных строк в разных компиляторах: например, ранние версии библиотек Microsoft не поддерживали "%lld" (приходилось указывать "%I64d"). Всё ещё есть разделение между Microsoft и GNU по типу size_t: %Iu у первого и %zu у второго. GNU C не требует в функции swprintf максимальную длину строки (приходится писать snwprintf).

Невозможность переставить аргументы местами

Функции семейства printf удобны для локализации программного обеспечения: например, проще перевести «You hit %s instead of %s.», чем обрывки строк «You hit », « instead of » и «.». Но и здесь таится проблема: невозможно переставить подставляемые строки местами, чтобы получилось: «Вы попали не в <2>, а в <1>.».

Расширения printf, применяемые в Oracle Java и Embarcadero Delphi, всё-таки позволяют переставить аргументы местами.

Утилита printf

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

Утилита имеет следующий формат вызова: printf format [argument …], где

  • format — строка формата, по синтаксису похожая на строку формата функции printf.
  • argument — список аргументов (0 или более), записанных в строковой форме.

Примеры реализации

#include <stdio.h>
#include <locale.h>

#define PI 3.141593

int main()
{
	setlocale(LC_ALL, "RUS");
	int number = 7;
	float pies = 12.75;
	int cost = 7800;
	printf("%d участников соревнований съели %f пирожков с вишнями.\n", number, pies);
	printf("Значение pi равно %f\n", PI);
	printf("До свидания! Ваше искусство слишком дорого обходится (%c%d)\n", '$', 2 * cost);

	return 0;
}
#include <stdio.h>

#define PAGES 959

int main()
{
	printf("*%d*\n", PAGES);
	printf("*%2d*\n", PAGES);
	printf("*%10d*\n", PAGES);
	printf("*%-10d*\n", PAGES);

	return 0;
}
/*
Результат:
*959*
*959*
*       959*
*959       *
*/
#include <stdio.h>

#define BLURB "Authentic imitation!"

int main()
{
	const double RENT = 3852.99;
	printf("*%8f*\n", RENT);
	printf("*%e*\n", RENT);
	printf("*%4.2f*\n", RENT);
	printf("*%3.1f*\n", RENT);
	printf("*%10.3f*\n", RENT);
	printf("*%10.3E*\n", RENT);
	printf("*%+4.2f*\n", RENT);

	printf("%x %X %#x\n", 31, 31, 31);
	printf("**%d**% d% d **\n", 42, 42, -42);
	printf("**%5d**%5.3d**%05d**%05.3d**\n", 6, 6, 6, 6);
	printf("\n");

	printf("[%2s]\n", BLURB);
	printf("[%24s]\n", BLURB);
	printf("[%24.5s]\n", BLURB);
	printf("[%-24.5s]\n", BLURB);

	return 0;
}

/* результат
*3852.990000*
*3.852990e+03*
*3852.99*
*3853.0*
*  3852.990*
* 3.853E+03*
*+3852.99*
1f 1F 0x1f
**42** 42-42 **
**    6**  006**00006**  006**

[Authentic imitation!]
[    Authentic imitation!]
[                   Authe]
[Authe                   ]
*/

Ссылки

  1. Краткое описание языка BCPL. Дата обращения: 16 декабря 2006. Архивировано 9 декабря 2006 года.
  2. Руководство по языку Би Архивировано 6 июля 2006 года.
  3. Описание функции sprintf в документации Perl. Дата обращения: 12 января 2007. Архивировано 14 января 2007 года.
  4. Описание форматирующего оператора для строковых типов в Питоне Архивировано 9 ноября 2006 года.
  5. Описание функции printf в составе PHP. Дата обращения: 23 октября 2006. Архивировано 6 ноября 2006 года.
  6. 1 2 Описание функции java.io.PrintStream.printf() в Java 1.5. Дата обращения: 12 января 2007. Архивировано 13 января 2007 года.
  7. Описание функции printf в документации Ruby. Дата обращения: 3 декабря 2006. Архивировано 5 декабря 2006 года.
  8. Описание функции string.format в документации Lua. Дата обращения: 14 января 2010. Архивировано 15 ноября 2013 года.
  9. Описание функции format в документации TCL. Дата обращения: 14 апреля 2008. Архивировано 4 июля 2007 года.
  10. Описание шаблона строки для printf в документации GNU Octave. Дата обращения: 3 декабря 2006. Архивировано 27 октября 2006 года.
  11. Документация Maple. Дата обращения: 12 декабря 2023. Архивировано 23 июля 2023 года.
  12. R. Fourer, D.M. Gay, and B.W. Kernighan. AMPL: A Modeling Language for Mathematical Programming, 2nd Ed.. Pacific Grove, CA: Brooks/Cole--Thomson Learning, 2003.
  13. GNU Emacs Lisp Reference Manual, Formatting Strings Архивная копия от 27 сентября 2007 на Wayback Machine
  14. Описание модуля Printf в документации OCaml. Дата обращения: 12 января 2007. Архивировано 13 января 2007 года.
  15. Описание модуля Printf в документации Haskell. Дата обращения: 23 июня 2015. Архивировано 23 июня 2015 года.
  16. std::println! - Rust. doc.rust-lang.org. Дата обращения: 24 июля 2016. Архивировано 18 августа 2016 года.
  17. Format. www.freepascal.org. Дата обращения: 7 декабря 2016. Архивировано 24 ноября 2016 года.
  18. fmt - The Go Programming Language. golang.org. Дата обращения: 25 марта 2020. Архивировано 4 апреля 2020 года.
  19. §7.19.6.1 ISO/IEC 9899:TC2
  20. § 7.11.1.1 ISO/IEC 9899:TC2, LC_NUMERIC определяет, в частности, форму представления разделителя дробной и целой части.
  21. Описание уязвимостей printf, Robert C. Seacord: Secure Coding in C and C++. Addison Wesley, September, 2005. ISBN 0-321-33572-4
  22. Описание проблем переноса приложений с 32 на 64 битную архитектуру. Дата обращения: 14 декабря 2006. Архивировано 8 марта 2007 года.
  23. System.SysUtils.Format Архивная копия от 11 января 2013 на Wayback Machine (англ.)
  24. Например, boost::format, документация Архивная копия от 26 марта 2013 на Wayback Machine (англ.)

Источники

  • printf, fprintf, snprintf, vfprintf, vprintf, vsnprintf, vsprintf в стандарте ISO/IEC 9899:TC2 (ISO C) [3]
  • printf, fprintf, sprintf, snprintf в стандарте Single Unix [4]
  • vprintf, vfprintf, vsprintf, vsnprintf в стандарте POSIX [5]
  • wprintf, swprintf, wprintf в стандарте POSIX [6]
  • vfwprintf, vswprintf, vwprintf в стандарте POSIX [7]
  • wsprintf в MSDN [8]
  • wvnsprintf в MSDN [9]
  • wnsprintf в MSDN [10]
  • wvsprintf в MSDN [11]
  • wnsprintf в MSDN [12]
  • asprintf, vasprintf в man-pages в Linux [13], в документации к libc [14]
  • Описание синтаксиса строки форматирования в руководстве libc [15].
  • Описание строки форматирования в документации к Microsoft Visual Studio 2005 [16]
  • Описание register_printf_function [17], [18]
  • Язык программирования C. Лекции и упражнения. Автор: Стивен Прата. ISBN 978-5-8459-1950-2 , 978-0-321-92842-9; 2015 г.

См. также

Kembali kehalaman sebelumnya