Пилим printf() своими руками

Weston Normcore
3 min readJun 21, 2020

--

Часть первая. Как перестать бояться va_arg

Меня часто спрашивают, с чего начать работу над проектом printf(). Многих отпугивает обилие ключей, флагов и прочих опций, которые надо реализовать, а главное — смущает сама идея работы с переменным количеством аргументов. Это краткое руководство призвано помочь сомневающимся и успокоить волнующихся. Забегая вперед — все получится, сложности в этом проекте заключены в основном в реализации бонусной части, но до нее пока далеко. Итак, начнем!

Что принимает printf()?

Прототип нашей функции выглядит следующим образом:
int ft_printf(const char *s, …);
Сразу обращает на себя внимание загадочное многоточие. С таким мы раньше не встречались. Многоточие говорит компилятору, что функция может принимать различное количество аргументов (от нуля до сколько-хватит-памяти). Чуть позже мы узнаем, как с этим работает программист. А пока давайте посмотрим на первый аргумент (const char *s). Он называется строкой форматирования и содержит в себе описание того, что должна выводить функция. Рассмотрим простой пример строки форматирования:
Hello %s! The deadline is in %u days.\n
Помимо привычного текста на английском языке, здесь есть два спецификатора формата: %s и %u. Спецификаторы всегда начинаются со знака процента и завершаются одним из поддерживаемых символов преобразования. Не считая бонусной части, нужно реализовать поддержку символов cspdiuxX%. Между знаком процента и символом преобразования (в английских текстах — conversion) могут идти различные флаги и модификаторы ширины и точности. Работу с ними мы рассмотрим отдельно, а пока постараемся сосредоточиться на том, как связаны спецификаторы формата и передаваемые в функцию аргументы.
Итак, в нашей строке есть спецификатор %s (string), и это значит, что в функцию, помимо строки форматирования, нужно передать аргумент типа char *. Кроме того, есть спецификатор %u (unsigned), и требуется также аргумент типа unsigned int. Как же передаются и принимаются параметры printf()?

Изучаем stdarg.h

Все необходимое для работы с переменным числом аргументов доступно в stdarg.h. Объявлен специальный тип va_list и макросы для работы с аргументами: va_start, va_arg, va_copy и va_end. На данном этапе вполне нормально рассматривать эти конструкции как «черные ящики», которые помогут нам достать нужный аргумент из числа переданных в функцию. Давайте представим, что в нашу функцию передали два аргумента, соответствующих строке форматирования (то есть char * и unsigned int), и попробуем что-то с ними сделать.

int ft_printf(const char *s, …)
{
va_list ap;
char *name;
unsigned int days;
va_start(ap, s);

name = va_arg(ap, char*);
days = va_arg(ap, unsigned int);
ft_putstr(name);
ft_putnbr(days);
va_end(ap);
return (0);
}

Примечания и замечания к коду:

  • Сначала нужно объявить переменную типа va_list. Затем, до обработки первого аргумента, вызвать макрос va_start, передав в него нашу переменную, а также аргумент s (последний, идущий перед списком переменной длины).
  • Затем в порядке, соответствующем строке форматирования, мы вызываем макрос va_arg столько раз, сколько у нас ожидается аргументов. Обратите внимание — передается список ap, а дальше — тип аргумента (char *, unsigned int и т.д.). Важное отступление: чтобы считать аргумент типа char, нужно передать в va_arg тип int. Особенностей реализации мы здесь касаться не будем, просто запомните, что char считывается через int.
  • После завершения считывания аргументов необходимо вызвать макрос va_end.
  • В модуле stdarg также доступна функция va_copy, которая позволяет сохранить состояние списка аргументов и вернуться к нему. В нашем случае в этом нет нужды, желающие могут изучить работу va_copy самостоятельно или задать мне вопрос чуть позже.
  • Предполагается, что вы умеете выводить на печать строки (реализована функция ft_putstr) и целые числа (функция ft_putnbr). Если с этим есть проблемы, отложите пока printf и добейтесь послушания от функции write(). Без нее написать printf не получится.

Работаем самостоятельно

Задания для закрепления материала:

  1. Напишите функцию, которая анализирует строку форматирования и возвращает количество аргументов, которые надо будет считать дополнительно. Учтите, что два идущих подряд знака процента (%%) означают, что надо напечатать сам знак процента, и аргумент в этом случае не считывается.
  2. Напишите функцию, которая печатает строку форматирования, а вместо каждого аргумента подставляет его номер в списке. Считывать сами аргументы пока не надо, но убедитесь, что ваша функция корректно работает с разными строками (не забудьте опять про двойной знак процента).
  3. Напишите функцию, которая будет работать аналогично printf(), но поддерживает только преобразования %c и %s (одиночные символы и строки). Флаги и модификаторы пока не обрабатываем, считаем, что аргументы задаются знаком процента, за которым сразу идет символ преобразования.

Когда вы успешно справитесь с этими заданиями, можно будет переходить к следующей части. План предположительно таков: обрабатываем %%, %c и %s, потом разбираемся с беззнаковыми целыми %u, потом учимся работать с шестнадцатеричным форматом %x и %X, на базе %x осваиваем работу с адресами %p, потом перестаем бояться отрицательных чисел и работаем с %d и %i (эти два преобразования идентичны в случае printf).

С наилучшими пожеланиями,
Александр Шуст
aka gwynton

Unlisted

--

--