Программирование ARM Секреты printf Fri, March 29 2024  

Поделиться

Нашли опечатку?

Пожалуйста, сообщите об этом - просто выделите ошибочное слово или фразу и нажмите Shift Enter.

Секреты printf Печать
Добавил(а) microsin   

Перевод статьи "Secrets of printf" [1], статья посвящена практическому применению оператора вывода printf.

Оператор printf - это просто функция языка C, которая выполняет форматированную печать неких значений, параметров функции. "Форматированную" - означает, что перед выводом параметров на печать параметры функции преобразуются в текст по особым правилам (формату), задаваемым специальной строкой, так называемой строкой форматирования. Точно такая же функция printf есть и на языке PERL. В этой заметке сделана попытка объяснить, как работает printf, и как правильно разработать соответствующую строку форматирования для любого случая.

[1. Основные понятия]

В старые времена программисты должны были писать свои собственные подпрограммы для ввода и вывода чисел. По сути это было не очень сложным делом. Сначала надо было выделить массив текстовых символов для хранения результата, затем просто разделить число на 10, запомнить остаток, добавить 0x30 для получения ASCII кода цифры, и сохранить цифру в конец массива. Далее эту процедуру нужно было повторить, чтобы найти все десятичные цифры числа. Затем нужно было вывести массив на печать. Все просто, не правда ли?

Однако несмотря на то, что просто (для гения типа Эйнштейна), однако все еще требует применения некоторых усилий. И что мы будем делать в более сложных случаях при проверке ошибок, или при выводе отрицательных чисел? Чтобы решить эти проблемы, программисты создали специальные библиотеки с предварительно написанными функциями вывода. И это было неплохо. В конечном счете самые популярные из этих функций были канонизированы для того, чтобы составить "стандартные" библиотеки.

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

Так родился printf.

[2. Простая печать]

В самом простом случае функция printf получает один аргумент: строка символов, которая должна быть напечатана. Как ясно из названия, эта строка состоит из символов, каждый из которых будет выведен именно так, как он появляется в строке. Так, оператор printf("xyz"); должен просто вывести сначала x, затем y, и наконец z. Это не является по-настоящему "форматированной" печатью (formatted printf), однако это базовая операция, которую может произвести printf.

2.1. "Натуральные" специальные символы

Чтобы идентифицировать начало строки, мы применили двойные (") кавычки в её начале. Чтобы идентифицировать конец строки, мы поместили двойные кавычки также и в конец строки. Но как быть, если нам нужно напечатать также и двойные кавычки? Мы не можем просто поместить двойные кавычки в печатаемую строку, потому что тогда этот символ будет ошибочно задавать маркер конца строки. Таким образом, двойные кавычки стали специальным символом. Для них уже не работает правило печатаю-то-что-вижу. Как все-таки напечатать двойные кавычки?

Различные языки программирования применяют разные способы для решения этой проблемы. Некоторые требуют, чтобы специальный символ был введен дважды. Язык C использует обратный слеш (делительная черта, \) в качестве управляющего символа (escape character), для изменения значения следующего за ним символа. Таким образом, для печати двойных кавычек нужно указать обратный слеш и за ним двойную кавычку (\"). Чтобы напечатать сам обратный слеш, то его нужно ввести дважды. Таким образом, первый обратный слеш означает "следующий символ имеет альтернативное значение", и второй обратный слеш теперь означает "напечатать обратный слеш".

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

\ escape, управляющая последовательность для следующего символа
\\ печатается обратный слеш
" обозначает начало или конец строки
\" печатаются двойные кавычки
' начало или конец символьной константы
\' печатается одинарная кавычка
% начало спецификации формата
\% печатается символ процента
%% также печатается символ процента

2.2. Альтернативные специальные символы

Однако еще у нас есть символы, которые печатаются без обратного слеша как обычно, но когда слева к ним добавляется обратный слеш, они становятся также спецсимволами. Например, это символ новой строки (new line, или line feed LF, код ASCII 0x0A). Чтобы напечатать букву n, нам нужно просто указать в строке n. Чтобы перевести печать на новую строку, мы должны напечатать \n, что вовлекает альтернативное значение для n (новая строка). В следующей таблице приведен список таких альтернативных спецсимволов.

\a звуковой сигнал, предупреждение (звоночек, bell)
\b backspace, вернуться на 1 символ назад (затереть символ)
\f form feed, переход на новый лист
\n newline, новая строка (linefeed, перевод строки, код ASCII 0x0D, переход печати на новую строку)
\r carriage return, возврат каретки (CR, код ASCII 0x0D, позиция печати возвращается на начало строки)
\t tab, табуляция по горизонтали
\v vertical tab, вертикальная табуляция

[3. Как задавать формат вывода числа (Format Specifications)]

Настоящая сила printf раскрывается при выводе на печать переменных. К примеру, мы можем указать спецификатор формата %d. Если такой спецификатор присутствует в строке, то мы должны предоставить число в качестве второго параметра функции printf. Функция printf сделана так, что может принимать неограниченное количество параметров, если это необходимо (printf относится к функциям с переменным количеством параметров). Пример ниже показывает, как это происходит.

int age;
age = 25;
printf ( "I am %d years old\n", age );

В этом простом примере функция printf имеет 2 аргумента. Первый аргумент - строка "I am %d years old\n" (в ней содержится спецификатор формата %d). Вторым аргументом является целое число age.

3.1. Список аргументов

Когда printf обрабатывает свои аргументы (список аргументов, отделенных друг от друга запятыми), он начинает печатать символы, которые находит в левом аргументе, символ за символом. Когда в этом процессе попадается символ процента (%), то printf знает, что это спецификатор формата - специальный набор символов, который задает, как надо вывести число. Следующее по порядку с списке аргументов число выводится так, как указано в спецификаторе формата. Затем процесс обработки символов (вывод их на печать) первого аргумента продолжается. Можно указать в строке 1 аргумента функции printf несколько спецификаторов формата. В этом случае 1 спецификатор будет выводить первый дополнительный аргумент, 2 спецификатор второй дополнительный аргумент и так далее, до конца строки. Вот еще один пример (указано 2 спецификатора формата и два дополнительных аргумента функции printf):

int x = 5, y = 10;
printf ( "x is %d and y is %d\n", x, y );

3.2 Символ процента (Percent, %)

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

%c выводит на печать одиночный символ (character)
%d выводит на печать десятичное число (представление числа с основанием 10)
%e выводит на печать числа с плавающей запятой (floating-point) в экспоненциальном формате
%f выводит на печать числа с плавающей запятой (floating-point)
%g выводит на печать числа с плавающей запятой (floating-point) в общем формате
%i выводит на печать десятичного целого числа (представление числа с основанием 10)
%o выводит на печать числа в восьмеричном формате (представление числа с основанием 8)
%s выводит на печать строки символов
%u выводит на печать целое десятичное число без знака (представление числа с основанием 10)
%x выводит на печать целого шестнадцатеричного числа (представление числа с основанием 16)
%% выводит на печать символ процента (можно использовать для этого также \%)

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

printf-format-specifier-d

Имейте в виду, что в случае использования %d размер получаемой строки заранее не известен. Функция printf сгенерирует строку такого размера, какой нужен.

3.3. Опция ширины формата (Width Option)

Как уже упоминалось, простой печати чисел недостаточно. Есть другие желаемые опции. Возможно, самая важная из них - опция ширины формата. Если указать спецификатор формата %5d, то будет гарантировано, что вывод числа всегда займет 5 символьных позиций (если нужно, то больше, но никак не меньше). Эта возможность очень полезна при печати таблиц, потому что и большие, и маленькие числа займут в строке одинаковое место. Не так давно вся печать была моноширинной (monospaced, все символы по точкам в ширину были одинаковы), т. е. и символ w, и символ i занимали одинаковое место в строке по ширине. Это остается общим правилом в текстовых редакторах, используемых программистами.

Чтобы напечатать десятичное число определенной (как минимум заданной, не меньше) ширины, скажем шириной в 5 пробелов, спецификатор формата должен быть %5d. В таблице приведены простые примеры использования опции ширины (пробелы для наглядности показаны нижней квадратной скобкой).

printf-width-option

Имейте в виду, что результат вывода будет дополнен слева пробелами до необходимой указанной ширины (для примеров в таблице это 5 символов). Слишком большие числа, которые требуют для печати большее количество символов, будут выведены полностью.

Чтобы добиться нормального использования, поле опции ширины должно быть указано таким, чтобы удовлетворять максимальному размеру ожидаемого выводимого числа. Например, если Ваши числа могут состоять из 1, 2 или максимум 3 цифр, то формат %3d подойдет. Опция ширины будет работать неправильно, если потребуется напечатать число, которое слишком большое, чтобы уместиться в заданную ширину поля. Функция printf примет решение вывести такие числа полностью, даже если они займут место больше, чем задано в спецификаторе ширины формата. Так сделано потому, что лучше вывести правильный ответ, пусть даже некрасиво, чем напечатать урезанный (неправильный) результат, и потом гадать, где же произошла ошибка.

3.4. Заполнение лишнего места

Когда печатается маленькое по размеру число наподобие 27 в поле формата %5d, встает вопрос - чем и как заполнить 3 другие (пустые) места печати. Цифры 27 можно напечатать по-разному: вывести в первых двух позициях, в последних двух позициях, или где-то посередине. Также пустые места могут быть заполнены не просто пробелами, а звездочками (***27, или 27***, или **27*), или знаками доллара ($$$27), или символами равенства (===27), или начальными нулями (наподобие 00027).

Эти дополнительные символы часто называют символами "проверочной защиты" (check protection), потому что они предназначены помешать плохим парням изменить печатаемую сумму в долларах. Относительно просто поменять заполнение пробелами на что-то другое. Гораздо сложнее подменить символ звездочки, знак доллара и ли символ равенства.

Функция printf предоставляет заполнение пространства пробелами (слева или справа), и заполнение нулями (только слева). Если Вам нужна check protection, или центрирование, то нужно использовать какие-то другие дополнительные методы. Но даже без check protection или центрирования printf
все равно имеет впечатляющую коллекцию опций форматирования.

3.5. Опция выравнивания (Justify Option)

Вывод на печать чисел функцией printf может быть выровнена влево (left-justified, напечатана в поле слева) или вправо (right-justified, напечатано в поле справа). Наиболее естественной выглядит печать чисел выровненными вправо, с дополнением пробелами слева. Так работает спецификатор формата %5d, он означает: напечатать число по основанию 10 в поле шириной 5 символов, и цифры числа выровнять по правому краю, слева дополнив нужным количеством пробелов.

Чтобы сделать число выровненным слева, к спецификатору формата нужно добавить знак минуса (-). Чтобы напечатать число в поле шириной в 5 символов, с выравниванием по левому краю спецификатор формата будет %-5d. В таблице приведены некоторые примеры использования левого выравнивания.

printf-justify-option

Так же, как и раньше, для коротких чисел результат будет дополнен справа пробелами. Слишком большие числа будут выведены без дополнения пробелами и не урезанные.

3.6. Заполнение лидирующими нулями (Zero-Fill Option)

Чтобы печать даты выглядела красиво и качественно, обычно одиночные цифры даты и месяца дополняют слева нулем. Это и есть "лидирующий ноль". Мы можем написать May 5, 2003, или как принято в США 05/05/2003. Можно написать также дату в виде 2003.05.05. Обратите внимание, что лидирующий ноль не изменяет значение дат, а просто добавляет наглядности. Таким способом отформатированная дата хорошо выглядит в списке.

Когда используется zero-filled (заполнение лидирующими нулями), нули всегда добавляются спереди, и результат получается выровненным как по левому, так и по правому краю. В этом случае знак минуса не дает эффекта. Чтобы вывести число в 5 позиций с дополнением нулями слева применяйте спецификатор формата %05d. В таблице показаны примеры использования и полученные результаты.

printf-zero-fill-option

Короткие числа будут дополнены лидирующими нулями. Числа большого размера будут напечатаны как есть, без изменения.

3.7. Забава со знаками "плюс"

Отрицательные числа всегда будут выведены со знаком минуса (-). Положительные числа и нули обычно не печатаются со знаком, однако Вы можете это задать принудительно. Символ плюса (+) в спецификаторе формата делают такой запрос. Чтобы напечатать число со знаком в поле шириной 5 символов, спецификатор формата должен быть %+5d. В таблице показаны примеры использования и полученные результаты.

printf-fun-with-plus-signs

Имейте в виду, что 0 трактуется как положительное число. Короткие числа будут дополнены нужным количеством указанных заполнителей. Слишком большие числа будут выведены без дополнения и не урезанные.

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

3.8. Невидимый знак "плюс"

Знак + немного причудлив, он может быть невидимым. В этом случае вместо печати + на положительных числах (и при печати 0), мы напечатаем пробел, где этот знак должен был бы находиться. Это может оказаться полезным при печати выровненных влево чисел, если Вы хотите, чтобы знак минуса значительно выделялся. В примерах ниже показаны два альтернативных варианта.

printf-invisible-plus-sign

Помните о том, что спецификатор формата %-5d даст нам другой результат, который мы уже рассматривали ранее (он показан здесь снова для наглядности):

printf-justify-option

Имейте в виду, что знак + исчезает, но все еще занимает место слева от числа. Имейте в виду также, что мы можем скомбинировать некоторые опции в одном и том же спецификаторе формата. В этом случае мы имеем скомбинированные опции +, -, 5, или пробел, -, 5, или просто -, 5.

3.9. +, пробел и 0

Здесь приведен другой пример одновременного комбинирования некоторых опций в одном спецификаторе формата. Использование спецификаторов формата % 05d или %0 5d дадут нам следующие результаты:

printf-plus-space-and-zero1

Использование спецификаторов формата %+05d или %0+5d дадут нам следующие результаты:

printf-plus-space-and-zero2

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

3.10. Общие замечания по формату вывода

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

printf-format-specifications-summary

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

[4. Вывод на печать строк]

Опция %s позволяет нам печатать строку внутри строки. Ниже дан пример.

char * grade;
if ( year == 11 ) grade = "junior"; printf ( "%s is a %s\n", "Fred", grade );

Флаг левого выравнивания может быть применен к строкам, однако конечно же дополнение слева нулями (zero fill), знак +, и невидимый + являются бессмысленными.

printf-strings

[5. Вывод чисел с плавающей точкой (Floating Point)]

Числа с плавающей точкой наподобие 3.1415 содержат внутри себя точку. Обычные целые числа типа 27 не имеют такой точки.

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

В обычной коммерции используются прайсы, где цены часто фигурируют как целые доллары или доллары и центы (precision составляет 0 или 2 цифры). Для цены на бензин цены упоминаются как доллары, центы, и десятая доля от цента (precision составляет 3 цифры). Ниже приведены примеры, как может быть выведено на печать число e=2.718281828.

printf-floating-point1

Обратите внимание, что если в спецификаторе формата указаны точка и число, то это число (precision) указывает, сколько чисел должно появиться после десятичной точки.

Имейте в виду, что если не указаны точка и precision для %f, то по умолчанию будет приведен формат %.6f (6 цифр после десятичной точки).

Имейте также в виду, что если указана precision 0, то десятичная точка также исчезает. Если Вы хотите её вернуть, то нужно это сделать принудительно в виде простого текста (после спецификатора формата %f).

Мы можем указать оба и ширину (width), и точность (precision) одновременно в одном спецификаторе формата. Имейте в виду, что 5.2 означает общую длину 5, с 2 цифрами после десятичной точки. Самая распространенная ошибка, когда думают, что это означает 5 цифр до точки и 2 цифры после точки, но это неправильно. Будьте внимательны.

printf-floating-point2

Можно также комбинировать precision с флагами, с которыми мы уже познакомились, чтобы указать левое выравнивание, дополнение слева нулями, применение знака +, и т. д.

printf-floating-point3

[6. Как лучше всего разрабатывать формат]

Если Вы придумываете спецификатор формата, то первый шаг, который нужно сделать - решить, что именно Вы печатаете. Если это целое число (unsigned char, short, int, long), число с плавающей точкой (float, double), строка (char []) или одиночный символ (char), то Вы должны выбрать соответствующий спецификатор для базового типа формата.

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

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

[7. Советы для тестирования]

Тест printf включает проверку появления подходящих проблем. Сам по себе алгоритм работы printf непрост для полного понимания - как будет работать вывод в разных ситуациях. Поэтому изучение тестового вывода printf даст более точную картину - что работает не так.

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

7.1. Простые случаи

Можно просто увидеть, есть ли у коротких чисел лидирующие нули. Если так, то в спецификаторе формата здесь должен быть 0. Также просто увидеть, есть ли у положительных чисел знак +. Если этот так, то + должен быть и в спецификации форматирования.

7.2. Перед, между, позади

Следующее, что нужно проверить - что печатается до выводимого числа, в промежутке, и после. К примеру, В спецификации форматирования типа x%5dz, где x стоит перед числом и z за числом. Части x и z не входят в спецификатор формата, но входят как часть в печатаемый результат. Все остальное относится к тому, что печатается "между".

Для того, чтобы определить, что же печатается за числом, посмотрите на вывод отрицательного числа чрезмерно большого размера. Любые пробелы до выведенного числа и после него покажут на пробелы до и после спецификатора формата. Например, если -2035065302 печатается как __-2035065302_ (здесь для наглядности пробелы заменены на подчеркивания), то можете быть уверенными, что строка печати была наподобие __%..._, с двумя пробелами перед и одним пробелом после спецификатора формата. Это произошло потому, что чрезмерно большое число заняло все позиции, которые были отведены в спецификаторе формата.

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

7.3. Невидимый знак +

Сравните между собой вывод чрезмерно большого отрицательного и такого же положительного числа. Если положительное число имеет дополнительный пробел слева, то в этом месте формат задает невидимый знак +. Если слева от числа нет дополнительного пробела, но невидимый знак не задан.

7.4. Левое выравнивание

Вычтите друг из друга то, что перед, и то что позади. Посмотрите, что осталось слева. Посмотрите на вывод маленького отрицательного числа. Где Вы видите дополнительные печатаемые пробелы? Если они находятся спереди числа, то применено правое выравнивание числа. Если они находятся позади, то число выровнено при печати влево. Если пробелы есть и спереди, и сзади, значит Вы что-то делаете неправильно.

[8. Вывод printf на печать без символа новой строки]

Библиотека стандартного вывода (stdio.h) обычно реализована так, что в stdout ничего не будет выведено, пока в потоке символов не будет обнаружен символ новой строки '\n'. Это создает проблемы при выходе из программы, если в выведенном сообщении не было в конце символа новой строки [3], а также при отображении строки прогресса или процентов завершения текущей операции. 

Для решения этой проблемы можно использовать вызов fflush(stdout) [4]. Пример:

...
wsize += UPGRADE_DATA_BLOCK_SIZE;
float percent = (float)wsize * 100 / size;
printf ("\r%.0f%%", floor(percent));
fflush(stdout);
...

[Заключение]

Функция printf является мощным инструментом (в умелых руках) для вывода чисел и чего-нибудь еще, хранимого в переменных. Из-за того, что инструмент мощный и имеет много возможностей, он несколько сложен в освоении. Если попытаться использовать printf наобум, без изучения документации, то его сложность часто делает невозможным понимание принципа вывода. Однако при незначительном изучении сложность может быть развернута в простые возможности, включающие width (ширина поля вывода), precision (точность), signage (управление выводом знака), justification (выравнивание) и fill (заполнение пустых мест поля вывода). Если распознать и понять эти возможности, то printf становится удобным и надежным помощником при выводе значений на печать.

[Ссылки]

1. Secrets of “printf”, Professor Don Colton, Brigham Young University Hawaii.
2. IAR EWB ARM: форматированный вывод printf библиотеки DLIB.
3. printf не выводит текст при выходе из программы.
4. c stdout print without new line? site:stackoverflow.com.

 

Комментарии  

 
0 #3 Сергей 08.12.2016 19:34
Можно ли прописать в "printf" "table"? Чтобы получилась таблица.

microsin: спецификатора форматирования "table" не существует. Печатать таблицу подобным способом получится только если написать специальную функцию, где будет использоваться несколько вызовов printf.
Цитировать
 
 
+8 #2 JohnySC 03.05.2016 20:10
Спасибо, как всегда исчерпывающе и сразу в закладки :-)
Цитировать
 
 
+2 #1 fuf 01.11.2015 15:04
Как добавить 0 в конце строки до заданной длины?

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

Добавить комментарий


Защитный код
Обновить

Top of Page