Язык c — stdarg.h и обертка над printf


Содержание

Как сделать обёртку над printf?

Для лога надо. Написал на подобии как в hge

Тоесть код в обёртке неправильно цисло жуёт.

Кто чем поможет :) .

Дык, этого, почему sprintf(buf, str, ap); ? sprintf не работает с ва_аргами — он как printf. А для того, чтобы выводить в буфер и с ва_аргами, есть функция vsprintf (дедуктивно угадываемое название, да. :)

>>inline int BMP_log(const char* str . )
насторожило отсутсвеи запятой после str :)

Спасибо! Добавил букву v и заработало :)

Посмотрел свой первый пост и действительно в примере ж от hge есть буква v, и как я её не заметил!

Описание функций языка Си

All | _ | A | B | C | D | E | F | G | H | I | J | K | L | M | N | O | P | Q | R | S | T | U | V | W | X | Y | Z

printf – вывод форматированной строки в стандартный поток вывода.

#include
int printf (const char *format, . );

format – указатель на строку c описанием формата.

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

Функция printf выводит в стандартный поток вывода строку отформатированную в соответствии с правилами, указанными в строке, на которую указывает аргумент format.

Правила задаются набором трех типов директив:
1. Обычные символы (кроме ‘%’ и ‘\’), которые выводятся без изменения;
2. Спецификаторы формата;
3. Специальные сиволы.

Каждый спецификатор формата начинается с символа ‘%’ и имеет следующий формат:

Спецификатор формата может иметь 0 или более [флагов], которые могут принемать значенияуказанные в таблице 1.

Таблица 1.

Флаг Назначение флага
— (дефис) Результат преобразования выравнивается по левому краю (по умолчанию — по правому краю)
+ Перед положительными числами выводится знак ‘+’, а перед отрицательыыми — знак ‘-‘ (по умолчанию выводится только знак ‘-‘ перед отрицательыми числами)
‘ ‘ (пробел) Если не указан модификатор ‘+’, то перед положительными числами, на месте знака числа, будет выводиться пробел.
# Использовать альтернативную форму представления выводимого числа. При выводе чисел в шестнадцатеричном формате (преобразователь ‘х’ или ‘Х’) перед числом будет указываться 0х или 0Х соответственно. При выводе чисел в восьмеричном формате (преобразователь ‘о’)перед числом будет указываться 0. При выводе чисел с плавующей точкой (преобразователи e, E, f, g и G) всегда будет содержаться десятичная точка (по умолчанию десятичная точка выводится только при ненулевой дробной части). При использовании преобразователей g и G хвостовые нули не будут удаляться (по умолчанию удаляются).
Если не указан флаг ‘-‘, то слева от выводимого числа будут выведены символы ‘0’ для подгона числа к указанной ширене. Если для преобразователей d, i, o, x или X указана точность, то флаг 0 игнорируется.

Спецификатор [флаги] можно не указывать.

Спецификатор [ширина] задаёт минимальный размер выводимого числа в символах. Если количество символов в выводимом числе меньше указанной минимальной ширины, то недостоющее количество символов заполняется нулями или пробелами слева или справа в зависимости от указанных флагов. Ширина указывается либо целым числом, либо символом * с последующим указанием имени переменной типа int, содержащей значение ширины, перед аргументом к которому он относится. Если аргумент имеет отрицательное значение, то он эквивалентен соответствующему положительному значению с флагом «-«.

Спецификатор [ширина] можно не указывать.

Действия спецификатора [точность] зависит от типа выводимого числа:

— Для типов d, i, o, u, x, X определяет минимальное число выводимых цифр. Если количество выводимых цифр в числе меньше, чем указано в спецификаторе [точность], то выводимое число будет дополнено нулями слева. Например, если при выводе числа 126 указать точность 4, то на экран будет выведено число 0126

— Для типов a, A, e, E, f, F определяет количество выводимых цифр после запятой. Если в выводимом числе количество значимых цифр после запятой меньше указанной точности, то недостающие символы выводятся нулями справа от числа. Если больше, то лишние цифры не выводятся. Например, если при выводе числа 126.345 указать точность 2, будет выведено на экран число 126.34, а если указать точность 5, то на экран будет выведено число 126.34500.

— Для типов g и G определяет максимальное выводимое число цифр. Например, если при выводе числа 126.345 указать точность 4, будет выведено на экран число 126.3. Если при выводе числа 1242679.23 указать точность 3, будет выведено на экран число 1.24е+06.

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

Спецификатор [точность] можно не указывать.

Спецификатор [модификаторы] определяет размер выводимых данных (char, short, long, longlong). Спецификаторы используются для вывода чисел типа: char, short int, long int, long long int, long double или для явного преобразования выводимых данных. Например, если имеется переменная типа int, а необходимо вывести ее как short int. Доступные модификаторы приведены в таблице 2.

Таблица 2.

Модификатор Назначение модификатора
h Для вывода числа типа short int или unsigned short int. Или для явного преобразования при выводе целочисленного числа к типу short int или unsigned short int. Используется совместно с типами преобразования:d, i, o, u, x и X, n.
hh Для вывода числа типа char или unsigned char. Или для явного преобразования при выводе целочисленного числа к типу char или unsigned char. Используется совместно с типами преобразования:d, i, o, u, x и X, n.
l Для вывода числа типа long int или unsigned long int. Или для явного преобразования при выводе целочисленного числа к типу long int или unsigned long int. Используется совместно с типами преобразования:d, i, o, u, x и X, n.
ll Для вывода числа типа long long int или unsigned long long int. Или для явного преобразования при выводе целочисленного числа к типу long long int или unsigned long long int. Используется совместно с типами преобразования:d, i, o, u, x и X, n.
L Для вывода числа типа long double. Или для явного преобразования при выводе числа c плавающей точкой к типу long double. Используется совместно с типами преобразования:e, E, f, g и G.

Спецификатор [тип преобразования] определяет как надо итерпритировать и выводить число, например как знаковое целочисленное в десятичном виде, или беззнаковое целочисленное в шестнадцатеричном, или как число с плавающей точкой и так далее. Перечень доступных преобразований и их описание указано в таблице 3.

Таблица 3.

Тип преобразования Назначение преобразования
d,i Вывод целого числа со знаком в десятичной систем счисления. По умолчанию выводится число размером sizeof( int ), с правым выравниванием, указанием знака только для отрицательных чисел.
u Вывод целого числа без знака в десятичной систем счисления. По умолчанию выводится число размером sizeof( int ), с правым выравниванием.
o Вывод целого числа без знака в восьмеричной систем счисления. По умолчанию выводится число размером sizeof( int ), с правым выравниванием.
x, X Вывод целого числа без знака в шестнадцетеричной систем счисления. Причем для преобразования x используются символы abcdef, а для X — символы ABCDEF. По умолчанию выводится число размером sizeof( int ), с правым выравниванием.
f, F Вывод числа с плавающей точкой в виде [-]dddd.ddd. По умолчанию выводится число с точностью 6, если число по модулю меньше единицы, то пред десятично точкой выводится ноль, знак указывается только для отрицательных чисел, с правым выравниванием. Размер по умолчанию sizeof( double ).
e, E Вывод числа с плавающей точкой в экспоненциальной форме записи, в виде [-]dddd.ddde±dd, причем для модификатора e используется символ e, а для модификатора E — символ E. По умолчанию выводится число с точностью 6, если число по модулю меньше еденицы, то пред десятично точкой выводится ноль, знак указывается только для отрицательных чисел, с правым выравниванием. После символа «e» (или «E») всегда выводится две цифры (они равны 0, если аргумент равен 0).
g, G Вывод числа с плавающей точкой в форме зависищей от величины цисла. Например число 345.26 будет выведено как 345.26, а число 1344527.434 как 1.34453e+06. По умолчанию выводится 6 значащих цифр числа с округлением, если число по модулю меньше еденицы, то пред десятично точкой выводится ноль, знак указывается только для отрицательных чисел, с правым выравниванием.
a, A Вывод числа с плавающей точкой в шестнадцатеричном фомрате, в экспоненциальной форме записи. Например число 137.434 будет выведено как 0x1.12de353f7ced9p+7. Экспонента обозначается символом p. Для модификатора a используется символ p, а для модификатора A — символ P.По умолчанию знак указывается только для отрицательных чисел, выравнивание — правое.
с Вывод символа, соответстветсвующего числу указанному в аргументе функции. По умолчанию число приводится к типу unsigned char.
s Вывод строки, на которую ссылается указатель в аргументе функции printf. Строка выводится пока не будет встречен символ конец строки (/0). По умолчанию строка должна обозначаться как char*. Если указан модификатор l, то строка интерпитируется как wchar_t*. Для функции wprintf строка по умолчанию обрабатывается как wchar_t*.
S Аналогичен преобразованию s с модификатором l (ls).
p Вывод указателя. Результат ввода зависит от архитектуры и используемого компилятрора. Например, на 16 битной платформе MS-DOS вывод будет иметь вид типа FFAB:1402, а на 32-битной платформе с плоской адресацией — 00FC0120.
n Запись по адресу, указанному в аргументе функции, количества выведенных символов функцией printf до встречи преобразователя %n. При обработке преобразователя %n никакого вывода символов не производится.

Для форматирования вывода в функции printf предусмотрен набор специальных симовлов. Перечень специальных символов приведен в таблице 4.

Ввод-вывод в Си

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

Ввод и вывод информации осуществляется через функции стандартной библиотеки. Прототипы рассматриваемых функций находятся в файле stdio.h . Эта библиотека содержит функции

  • printf() — для вывода информации
  • scanf() — для ввода информации.

Вывод информации

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

Общая форма записи функции printf() :

СтрокаФорматов состоит из следующих элементов:


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

Объекты могут отсутствовать.

Управляющие символы не выводятся на экран, а управляют расположением выводимых символов. Отличительной чертой управляющего символа является наличие обратного слэша ‘\’ перед ним.

Основные управляющие символы:

  • ‘\n’ — перевод строки;
  • ‘\t’ — горизонтальная табуляция;
  • ‘\v’ — вертикальная табуляция;
  • ‘\b’ — возврат на символ;
  • ‘\r’ — возврат на начало строки;
  • ‘\a’ — звуковой сигнал.

Форматы нужны для того, чтобы указывать вид, в котором информация будет выведена на экран. Отличительной чертой формата является наличие символа процент ‘%’ перед ним:

  • %d — целое число типа int со знаком в десятичной системе счисления;
  • %u — целое число типа unsigned int ;
  • %x — целое число типа int со знаком в шестнадцатеричной системе счисления;
  • %o — целое число типа int со знаком в восьмеричной системе счисления;
  • %hd — целое число типа short со знаком в десятичной системе счисления;
  • %hu — целое число типа unsigned short ;
  • %hx — целое число типа short со знаком в шестнадцатеричной системе счисления;
  • %ld — целое число типа long int со знаком в десятичной системе счисления;
  • %lu — целое число типа unsigned long int ;
  • %lx — целое число типа long int со знаком в шестнадцатеричной системе счисления;
  • %f — вещественный формат (числа с плавающей точкой типа float );
  • %lf — вещественный формат двойной точности (числа с плавающей точкой типа double );
  • %e — вещественный формат в экспоненциальной форме (числа с плавающей точкой типа float в экспоненциальной форме);
  • %c — символьный формат;
  • %s — строковый формат.

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

Результат работы программы

Тот же самый код может быть представлен с использованием одного вызова printf :

Форматный вывод на Си для микроконтроллеров.

Форматированный ввод-вывод применяется очень широко, в первую очередь это, конечно, взаимодействие с пользователем, а так-же отладочный вывод, логи, работа с различными текстовыми протоколами и многое другое. В этой статье рассматриваются особенности применения форматированного вывода (ввод оставим на потом) для микроконтроллеров.
Первая программа написанная для ПК традиционно называется «Hello, world» и естественно пишет в стандартный вывод эту знаменитую фразу:

Первая программа для микроконтроллера обычно зовётся «Blinky» и она просто мигает светодиодом. Дело в том, что заставить работать традиционный «Hello, world» на микроконтроллере не так уж и просто для начинающего. Во первых, нет стандартного устройства вывода и его функциональность ещё нужно реализовать. Во вторых, не всегда бывает очевидно как подружить стандартные функции вывода с целевым устройством. Ситуация усугубляется тем, что в каждом компиляторе (тулчейне) это делается каким-то своим способом.

Форматный вывод.

Что-же, в общем, за зверь такой форматный вывод? Упрощенно говоря, это — вывод значений различных типов в виде текстовых полей.

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

Требования и особенности ввода-вывода для МК.

1. Гибкость. В отличии от старших братьев, для МК нет и не может быть стандартного устройства ввода-вывода. В каждой системе будет что-то своё, с уникальными требованиями и особенностями. В одной системе будет вывод в USART, во второй — на ЖК дисплей, в третей — в файл на SD карточке, в четвёртой — всё сразу. Значит система форматированного ввода-вывода должна быть достаточно гибкой, настраиваемой и независимой от аппаратных особенностей целевой платформы.

2. Требования к ресурсам. В МК ресурсов бывает мало и очень мало. В идеале, в прошивку МК должна попасть только та функциональность, которая реально используется. Скорость и размер кода имеют значение, под час решающее. Если мы не используем вывод типов с плавающей точкой, то и в полученной прошивке не должно быть кода, который его осуществляет.

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

4. Доступность. Библиотека ввода-вывода должна быть в наличии для целевой платформы.

5. Функциональность часто ставится на последнее место в угоду скорости и компактности.

Цукерберг рекомендует:  GuzzleHttp и CURLOPT_PROXY

Стандартная библиотека Си.

Стандартным и единственно доступным средством форматированного вывода в Си является семейство функций: printf, sprintf, snprintf и fprintf.
Рассматривать будем функцию printf, как наиболее типичного и часто используемого представителя функций форматного вывода, при необходимости переходя к другим. Итак фунция printf имеет следующий синтаксис:

Это обычная функция с переменным числом параметров. Здесь первый аргумент fmt является форматной строкой, которая содержит, как обычный текст, так и специальные управляющие последовательности. Троеточие (. ) обозначает список дополнительных параметров, их может быть от нуля и до сколько поместится в стеке. Да, да все параметры в функцию printf передаются преимущественно в стеке (за исключением архитектуры ARM, где первые 4 параметра передаются в регистрах). Запихивает их в стек вызывающая функция, она-же инициализирует кадр стека перед вызовом и очищает после. Сама printf узнать сколько и каких параметров ей было передано может узнать только из форматной строки. При передачи параметров размером меньше (signed char, unsigned char, char и на некоторых платформах signed/unsigned short), чем int, они будут расширенны соответствующим расширением(знаковым или без-знаковым) до int-a. Это сделано для того, чтобы стек был всегда выровнен по границе машинного слова, а так-же уменьшает количество возможных ошибок при нестыковке размера фактического параметра и ожидаемого из форматной строки. Так-же параметры типа float при передаче в функции с переменным числом аргументов, приводятся к типу double.
Форматная строка содержит как символы непосредственно выводимые в поток, так и специальные управляющие последовательности, которые начинаются со знака «%». Управляющие последовательности имеют следующий формат:

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

Флаги определяют дополнительные параметры форматирования (их может быть несколько):

Ширина — десятичное число, задаёт минимальное количество символов выводимых для соответствующего параметра. Если выводимое значение содержит меньше символов, чем указано, то оно будет дополнено пробелами (или нулями если есть флаг «0») до нужной ширины слева или справа, если указан флаг «-«. Например:

Если вместо десятичного числа указать «*», то значение ширины будет считанно из дополнительного целочисленного параметра. Это позволяет задавать значение ширины поля вывода из переменной:

Здесь первый раз i передаётся в качестве ширины поля, второй — значения.
Точность или длинна — десятичное число после знака точки «.». В случае вывода целых чисел этот элемент означает минимальное количество записанных знаков, если выводимое число короче указанной длинны, то оно дополняется ведущими нулями, если число длиннее, то оно не урезается.

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

Для форматов «g» и «G» это — общее количество выведенных значимых цифр.

Для строк «s» этот элемент называется длинна и означает максимальное количество выведенных символов, обычно строки выводятся пока не встретится нулевой завершающий символ.

Также как и в элемента «ширина», в место точности можно поставить звёздочку и передать её значение в виде дополнительного целочисленного параметра:

Дополнительный модификатор служит для указания размерности типа:
h — применяется со спецификаторами i, d, o, u, x и X для знаковых и без-знаковых коротких целых short и unsigned short).
l — совместно со спецификаторами i, d, o, u, x и X означают длинные целые long и unsigned long).
l — совместно со спецификаторами s и c «широкие» многобайтные строку и символ соответственно.
L — обозначает тип long double, применяется со спецификаторами e, E, f, g и G.
В компиляторах поддерживающих тип long long, таких как GCC и IAR, часто есть для него нестандартный модификатор ll.
В стандарте С99 добавлены модификаторы «t» и «Z» для типов ptrdiff_t и size_t соответственно.

Работа над ошибками

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

На платформах, где int имеет 32 бита этот код работает правильно, а где int — 16 бит — будут выведены только 2 младших или старших (в зависимости от порядка следования байт) байта.
К счастью некоторые компиляторы, например GCC, знают printf «в лицо» и выдают предупреждения в случае несовпадения фактических параметров с форматной строкой. Однако это работает только если форматная строка задана литералом. А если она хранится в переменной (например extern указатель инициализируемый в другом модуле), то компилятор бессилен и проследить соответствеи параметров может быто очень не просто.

Особенности реализаций


AVR-GCC он-же WinAVR

В AVR-GCC, а точнее в avr-libc самая удачная, на мой взгляд, реализация стандартной библиотеки ввода-вывода Си. В ней имеется возможность выбирать необходимый функционал функций семейства printf. Они прекрасно работают со строками как RAM так и во Flash. Все функции семейства, включая snprintf и fprintf разделяют общий код и очень хорошо оптимизированы как по скорости, таки по объёму кода.
Для поддержки находящихся во Flash памяти строк введен новый спецификатор форматной строки %S — S — заглавная, строчная s по-прежнему означает строку в RAM. Но во Flash памяти может быть и сама форматная строка, для этого есть специальные модификации функций с суффиксом «_P» printf_P, fprintf_P, sprintf_P и т. д., которые ожидают форматную строку во Flash.
Для того чтобы printf заработала, нужно написать функцию вывода символа и определить файловый дескриптор стандартного вывода.

Помимо stdout есть стандартные дескрипторы stdin и stderr для стандартного ввода и стандартного вывода ошибок соответственно. Используя функцию fprintf можно явно указывать нужный файловый дескриптор и при необходимости можно определить сколько угодно устройств вывода:

В avr-libc имеется три уровня функциональности библиотеки ввода-вывода:
1. нормальный — поддерживается вся упомянутая выше функциональность, кроме вывода чисел с плавающей точкой. Этот режим включен по умолчанию и каких либо опций для него указывать не надо. Поддержка чисел с плавающей точкой занимает много места, а нужна сравнительно редко. Приведенный выше пример, скомпилированный с этим уровнем функциональности, занимает порядка 1946 байт Flash памяти.
2. минимальный — поддерживаются только самые базовые вещи: целые, строки, символы. Флаги, ширина поля и точность, если они есть в форматной строке, разбираются корректно, но игнорируются, поддерживается только флаг «#». Пример, скомпилированный с этим уровнем функциональности, занимает порядка 1568 байт Flash памяти. Его вполне можно было-бы применить на контроллере с 2 Кб flash памяти. Включается указанием в командной строке компоновщика («Linker options» в AVRStudio, а не компилятора, как расплывчато указано в документации avr-libc) следующих опций:

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

Скомпилированный пример занимает при этом 3488 байт.
Функции семейства printf из avr-libc не используют буферизацию, не считая буферов для конвертации чисел, и выводят символы в устройство по мере их обработки. Поэтому, если буферизация нужна, то ее можно реализовать самостоятельно в функции вывода символа, там можно реализовать и кольцевой буфер и все, что угодно. Также в этих функциях не используется динамическая память, что в нашем случае очень хорошо, зато активно используется стек. Попробуем определить максимальное использование стека в них. Для этого в приложенном архиве заходим в каталог AvrGccPrintf, компилируем проект посредством AVRStudio и запускаем симуляцию с помощью runSimul.cmd.

После открывает образовавшийся файл trace.log, находим значение указателя стека (SP) после входа в main (после пролога), находим минимальное значение SP (стек растёт вниз) и вычитаем из первого второе. У меня получилось 0x455 — 0x429 = 0x2c = 44 байта использует сама функция fprintf, плюс еще 8 байт в стеке занимают ее параметры, итого 52 байта. Еще 14 байт занимает один файловый дескриптор и ещё 6 байт три стандартных указателя на файловые дескрипторы (stdout, stdin, stderr). Итого 72 байта RAM только на вызов fprintf, без учета всего остального.
Также из файла trace.log можно узнать общее время выполнения функций и где процессор проводит его больше всего.

Подробнее о стандартной библиотеке ввода-вывода avr-libc можно здесь:
www.nongnu.org/avr-libc/user-manual/group__avr__stdio.html

IAR for AVR

Здесь есть несколько версий Си-шных библиотек, с отличающейся функциональностью:
— CLIB — относительно маленькая, но ограниченная библиотека. Нет поддержки файловых дескрипторов, локалей и многобайтных символов. Считается устаревшей.
— Normal DLIB — более новая. Так-же нет поддержки файловых дескрипторов, локалей и многобайтных символов, но есть некоторые плюшки из стандарта С99.
— Full DLIB — полная библиотека Си. Поддерживает всё согласно стандарту С99, но при этом очень объёмная. Рассматривать этот вариант не буду, так, как размер функции printf отсюда превышает доступные 4 Кб кода для бесплатной версии IAR KickStart for AVR.
В IAR имеется возможность выбирать возможности для printf. Для этого заходим с меню Project->Options далее в диалоге General Options->Library Configuration. В списке «Printf formatter» можно выбрать необходимый уровень функционала. Для CLIB это Large, Small, Tiny, для DLIB добавляется еще Full. По возможностям эти уровни примерно соответствуют аналогичным в avr-gcc, поэтому расписывать их не буду.

Для того чтобы printf заработала, надо определить функцию вывода символа:

Разберём полный пример. Он также предназначен для запуска на симуляторе.

Тут выясняются две неприятные особенности. Во-первых функции printf и printf_P видимо не разделяют общий код и printf_P всегда использует максимальный форматтер, независимо от того, что выбрали в настройках для printf, занимая порядка 3500 байт кода. Поэтому приведенный пример не помещается в четыре бесплатных килобайта. Для проверки одну из функций надо закомментировать.
Во-вторых, ни printf, ни printf_P не умеют читать строки из flash памяти — для них нет спецификатора.
Размеры printf для различных уровней функциональности примерно соответствуют аналогичным у avr-gcc, где чуть меньше, где чуть больше — непринципиально. А вот использование стека в разы выше, минимальный размер стека данных, при котором printf заработала, составил 200 байт для DLIB и около 150 для CLIB. Так, что на контроллерах с 2 кб flash, 128 RAM использовать эти функции не получится.
Демо-проект находится в каталоге AvrIarPrintf. Для запуска симуляции, а точнее преобразования генерируемого IAR-ом hex-а в пригодный для потребления simulavr-ом elf, на машине должен быть установлен WinAVR (и прописан в переменной окружения PATH, естественно).

Mspgcc

В стандартной библиотеке mspgcc для форматного вывода реализованы только функции printf и sprintf, плюс еще нестандартная функция uprintf, которая принимает указатель на функцию вывода символа в качестве первого аргумента. Файловых дескрипторов там нет и в помине, выбирать уровень функциональности форматтеров тоже нельзя. При этом printf «весит» порядка двух килобайт так, что запустить её, скажем, на MSP43 Launchpad-е не получится.
Для работы printf нужно, вполне ожидаемо, определить функцию int putchar(int c):

IAR for MSP430, IAR for ARM и может еще какой IAR

Вообще в реализациях стандартной библиотеки от IAR Systems всё довольно однообразно для различных платформ, что не может не радовать. Как правило есть минимум две версии стандартной библиотеки — одна урезанная без поддержки файловых дескрипторов, вторая — полная, соответственно, с их поддержкой. Зовутся они как правило «Normal» и «Full» соответственно. Так-же в каждой из них можно выбирать необходимый функционал разбора форматной строки, поддерживаемый функциями семейства printf. Варианты уже уже описаны для IARfor AVR: Large, Small, Tiny и Full.
Если выбрать вариант библиотеки без файловых дескрипторов, то для работы функций вывода нужно определить лишь функцию int putchar(int outchar).
Если используем вариант с дескрипторами, то определить нужно функции __write, __lseek, __close и remove.
Минимальная их реализация, например, для STM32 может выглядеть так:

ARM + NewLib

Большинство сборок GCC под ARM используют NewLib в качестве стандартной библиотеки Си. Это достаточно взрослый проект и хорошо соответствует Си-шным стандартам, но при этом она относительно «тяжела» и требовательна к ресурсам. Для настройки библиотеки под свои нужды используется механизм системных вызовов. О нём уже немного писалось тут ispolzuem-libc-newlib-dlya-stm32, поэтому подробно на них останавливаться не буду, а упомяну об особенностях.
Первое это требования к памяти. Все stdio функции из NewLib используют хитрую буферизацию и динамически распределяют память для своих внутренних нужд. А значит им нужна куча и не маленькая, а в целых 3 Кбайт. Плюс примерно 1500 байт на статические структуры и плюс около 500 байт стека. Итого только чтоб напечатать «Hello, world!» нужно порядка 5 Кб оперативки. Что как-бы чуть больше, чем много для STM32-Discovery, на которой я запускал тестовый пример, с её 8 килобайтами. Также при использовании таких функций как printf по зависимостям тянется практически вся stdio плюс функции поддержки плавающей точки. В итоге тестовая прошивка занимает чуть меньше 30 Кб памяти программ. Если отказаться от использования чисел с плавающей точкой, то вместо printf можно использовать её облегченный аналог iprintf. В этом случае объём тестовой прошивки будет около 12 Кб.
Если какая либо из функций stdio не сможет выделить необходимую её память из кучи, то она тихонечко свалится в HardFault без объяснений причин.
Еще один момент это буферизация. Она может несколько запутать. Вызываем:

И… Ничего не происходит. Хотя системные вызовы правильно определены, куча настроена, места в ней хватает.
Наш вывод был спокойно положен в буфер и ждет там либо пока буфер не заполнится, либо не будет принудительно сброшен. Сбросить этот буфер можно с помощью функции fflush, или послав на вывод символ новой строки.

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

При этом потребление памяти кучи уменьшится более чем на 1.5 Кб.

Xprintf

Это реализация функций похожих на printf от товарища Chan-а. Качаем отсюда:
elm-chan.org/fsw/strf/xprintf.html
Библиотека содержит следующие функции для форматированного вывода — аналоги функций стандартной библиотеки Си:

Все функции написаны целиком на Си и их можно использовать практически на любом микроконтроллере. Однако они не учитывают особенностей некоторых МК, например, нет никакой поддержки строк во Flash памяти для семейства AVR, что не добавляет удобства использования. Для использования xprintf необходимо инийиализировать указатель xfunc_out, присвоив ему адрес функции вывода символа. Рассмотрим пример. Компилятор avr-gcc, проект AvrStudio, рассчитан на запуск в симуляторе simulavr.

Здесь для вывода строк из Flash памяти их приходится предварительно скопировать в оперативку.
Из достоинств этой библиотеки можно выделить компактность кода (

1600 байт для приведённого примера) и лёгкость использования на различных платформах и модифицировать. На этом достоинства заканчиваются. Из недостатков стоит отметить
относительно медленную работу, примерно в полтора-два медленнее чем стандартная printf из avr-libc, и несоответствие стандартам — не поддерживаются некоторые флаги (‘пробел’, ‘ #’, ‘+’ ), спецификаторы (i, p n), не считая флагов для чисел с плавающей точкой и т.д.
Потребление стека порядка 38 байт не считая аргументов.

Цукерберг рекомендует:  Роботы будущего, которые уже среди нас

Описание примеров.

AvrGccPrintf
AvrIarPrintf
AvrXprintf
Msp430GccPrintf
Msp430IarPrintf
Stm32Format
IarArm

Работает на STM32 Discovery. Собирается с помощью IAR for ARM.

Итоги.

Преимущества использования стандартной библиотеки Си для форматного вывода:
— Стандартность. Этим всё сказано.
— Доступность. В каком-то виде есть практически в любом Си компиляторе.
— Разделение формата вывода и выводимых данных. Форматную строку легко вынести в отдельный файл/модуль, например для дальнейшей локализации.
— Хорошая функциональность.
Недостатки:
— Полностью стандартная реализация в большинстве случаев слишком «тяжела» для микроконтроллеров.
— Использование лишь одной функции, например printf, тянет за собой значительную часть библиотеки вывода, даже если реально используются только ограниченные возможности.
— Неаккуратное использование функций с форматной строкой может привести к трудно обнаруживаемым ошибкам кодирования.
— В каждом компиляторе используется какой-то свой способ для определения низкоуровневых функций вывода.

  • Си,
  • stdio,
  • printf,

  • stdout,
  • STM32,
  • AVR,
  • MSP430,
  • simulavr,
  • конкурс2
  • +7
  • 14 октября 2011, 09:21
  • neiver
  • 1

Комментарии ( 39 )

Пример, скомпилированный с этим уровнем функциональности, занимает порядка 1568 байт Flash памяти. Его вполне можно было-бы применить на контроллере с 2 Кб flash

  • reptile
  • 14 октября 2011, 11:36
  • neiver
  • 14 октября 2011, 12:33
  • reptile
  • 14 октября 2011, 12:54
  • neiver
  • 14 октября 2011, 13:00
  • Vga
  • 14 октября 2011, 13:08

100 байт свободно :)
Сам форматный вывод в примерно 1000 с небольшим байт помещается (и для AVR и для MSP430).
Об этом следующая статья будет. С шаблонами и выносом мозга.

  • neiver
  • 14 октября 2011, 13:16
  • Vga
  • 14 октября 2011, 13:24

Отличная статья – видно, что проделана большая работа.
Позволю себе небольшое дополнение: если говорить о выводе отладочной информации то такая информация как правило нужна только на этапе разработки. В релизном коде часто отладку отключают. Для того чтобы упростить процесс включения/отключения отладки можно применить директивы условной компиляции. Например, такая реализация:

Соответственно в коде для вывода отладки вместо printf используем _DBG, например

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

stdarg.h — stdarg.h

stdarg.h является заголовком в стандартной библиотеке C на языке программирования C , что позволяет функции принимать неограниченное количество аргументов . Он предоставляет средства для пошагового списка функциональных аргументов неизвестного числа и типа. С ++ предоставляет эту функциональность в заголовке cstdarg .

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

содержание

Декларирование VARIADIC функции

VARIADIC функции являются функциями , которые могут принимать переменное число аргументов и объявляются с многоточием вместо последнего параметра. Пример такой функции printf . Типичная декларация

VARIADIC функция должна иметь по крайней мере один параметр с именем, поэтому, например,

не допускается в С (В C ++, такое заявление разрешено) В C, запятая должна предшествовать многоточие; в C ++, это не является обязательным.

Определение VARIADIC функций

Тот же синтаксис используется в определении:

Многоточие не может появиться в определениях функций в старом стиле.

stdarg.h типы

Стандартная библиотека C
Общие темы
  • Типы данных
  • классификация символов
  • Струны
  • Математика
  • Файл ввода / вывода
  • Дата / время
  • локализация
  • Выделение памяти
  • Контроль над процессом
  • сигналы
  • Альтернативные маркеры
Разные заголовки
название Описание Совместимость
va_list тип аргументов перебора C89

stdarg.h макросы

название Описание совместимость
va_start Начать перебор аргументы с микросхемой va_list C89
va_arg Получить аргумент C89
va_end освободить va_list C89
va_copy Скопируйте содержимое одного va_list к другому C99

Доступ аргументы

Чтобы получить доступ к неназванные аргументы, нужно объявить переменную типа va_list в VARIADIC функции. Макрос va_start затем вызывается с двумя аргументами: первый является переменной , объявленной типа va_list , второе имя последнего имени параметра функции. После этого, каждый вызов va_arg макроса дает следующий аргумент. Первый аргумент va_arg является va_list и второй тип следующего аргумента , переданного функции. Наконец, va_end макрос должен быть вызван на va_list до возврата из функции. (Это не требуется для чтения во всех аргументах.)

C99 обеспечивает дополнительный макрос va_copy , который может дублировать состояние va_list . Вызов макроса va_copy(va2, va1) копии va1 в va2 .

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

  • Использование услуг printf или scanf -как формат строки со встроенными указателями , которые указывают типы аргументов.
  • Значение сторожевого в конце аргументов переменного числа.
  • Подсчет аргумент, указывающее количество аргументов переменного числа.


Передача неназванных аргументов других вызовов

Поскольку размер безымянного списка аргументов , как правило , неизвестно (соглашения о вызове , используемые большинством компиляторов , не позволяют определить размер безымянного блока аргумента , на который указывает va_list внутри функции приемной), там также не надежный, универсальный способ перешлите неназванные аргументы в другую VARIADIC функцию. Даже там , где определение размера списка аргументов можно косвенными методами (например, путем анализа формата строки fprintf() ), нет переносимого способа передать динамически определенное количество аргументов во внутренний VARIADIC вызов, так как количество и размер аргументов , передаваемых в такие вызовы должны обычно известны во время компиляции. В какой — то степени, это ограничение может быть ослаблено, используя VARIADIC макросы вместо функций переменным числом. Кроме того, большинство стандартных процедур библиотеки предоставляют v -prefixed альтернативных вариантов , которые принимают на ссылку в список безымянного аргумента (т.е. инициализированный va_list переменного) , а не самого безымянный список аргументов. Например, vfprintf() альтернативный вариант fprintf() ожидая va_list вместо фактического безымянного списка аргументов. Таким образом , определенный пользователем VARIADIC функция может инициализировать va_list переменную с помощью va_start и передать его в соответствующую стандартную функцию библиотеки, в сущности передавая безымянный список аргументов по ссылке , а не делать это по значению. Потому что нет никакого надежного способа передать неназванные списки аргументов по значению в C, обеспечивая VARIADIC API функций без предоставления эквивалентных функций , принимающих va_list вместо считаются плохой практикой программирования.

безопасность Тип

Некоторые реализации C обеспечивают расширения C , которые позволяют компилятору проверить для правильного использования строк формата и часовых. Если исключить эти расширения, компилятор обычно не может проверить неназванные аргументы , переданные имеют тип функция ожидает, или преобразовать их в требуемый тип. Поэтому следует соблюдать осторожность , чтобы гарантировать правильность в этом отношении, так как неопределенное поведение приводит , если типы не совпадают. Например, если ожидаемый тип int * , то нулевой указатель должен быть передан как (int *)NULL . Запись только NULL приведет к аргументу типа либо int или void * , ни один из которых является правильным. Другим аспектом является аргументом по умолчанию в акции , применяемые к безымянным аргументам. float Автоматически будет повышен до double . Кроме того, аргументы типов более узких , чем int будут назначены int или unsigned int . Функция приема неназванные аргументы должны ожидать , расширенный тип.

GCC имеет расширение , которое проверяет передаваемые аргументы:

format(archetype, string-index, first-to-check) Атрибут формата указывает , что функция принимает printf , scanf , strftime или strfmon аргументы типа , которые должны быть типа сверен строки формата. Например, заявление:

пример

Эта программа дает результат:

Для вызова других функций вара ARGS из вашей функции (например, Sprintf), необходимо использовать версию вар ARG функции (vsprintf в данном примере):

varargs.h

Устаревшие версии POSIX определили унаследованный заголовок varargs.h , который датируется до стандартизации C и обеспечивает функциональность , аналогичную stdarg.h . Этот заголовок является частью ни ISO C , ни POSIX. Файл, как определен во второй версии спецификации Single UNIX , просто содержит все функциональные возможности C89 stdarg.h , с тем исключением , что: он не может быть использован в стандартных определениях нового типа C; Вы можете не иметь данный аргумент (стандарт C требует , по меньшей мере , один аргумент); и как она работает отличается в стандартном C, можно было бы написать:

С varargs.h , функция будет выглядеть так :

и называется точно так же.

varargs.h требует старый стиль определения функций из-за способа работает реализация. С другой стороны , это не представляется возможным смешивать определение функций в старом стиле , с stdarg.h .

Язык c — stdarg.h и обертка над printf

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

Для определения параметров неопределенной длины в таких функциях используется многоточие:

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

Например, определим функцию, которая вычисляет сумму чисел, количество чисел нефиксировано:

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

Первый и обязательный параметр функции sum — n — указывает на количество необязательных параметров. В цикле устанавливаем указатель ptr на адрес параметра n и последовательно перемещаем его. С помощью операции разыменования *(++ptr) после перемещения указателя на один элемент вперед получаем значение и выполняем сложение с переменной result.

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

И для упрощения работы с переменным количеством параметров неопределенных типов в языке Си в стандартом заголовочном файле stdarg.h определены специальные макрокоманды:

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

Макрос va_start имеет следующее определение:

Первый параметр макроса — param связывает объект va_list с первым необязательным параметром. Для его определения в качестве второго параметра в макрос передается последний обязательный параметр функции. Таким образом, используя последний обязательный параметр, мы можем нацелить объект va_list на адрес первого необязательного параметра. То есть фактически va_list выступает в данной роли как указатель.

Макрос va_arg имеет следующее определение:

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

Макрос позволяет выйти из функции с переменным списком параметров. Она имеет следующее определение:

В качестве параметра она принимает указатель va_start, который ранее был задействован в макросах va_start и va_arg.

Перепишем предыдущий пример с использованием этих макрокоманд:

В функции sum() вначале определяется указатель va_list factor; .

Далее связываем этот указатель с первым необязательным параметром: va_start(factor, n); .

В цикле пробегаемся по всем необязательным параметрам и их значение прибавляем к переменной result: result += va_arg(factor, int);

В конце завершаем обработку параметров: va_end(factor); .

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

Но стоит отметить, что используемые нами функции ввода-вывода printf() и scanf() то же имеют неопределенное число параметров, но их типы также неопределены:

Для идентификации типов аргументов параметр format использует спецификаторы %d, %c и так далее. Например, определим собстенную функцию, которая будет выводит текст на экран, принимая параметры разных типов:

Для упрощения примера здесь взяты только два спецификатора: d (для типа int) и f (для типа double). В самой функции display с помощью указателя char *c пробегаемся по всем символам переданной строки format, пока этот указатель не станет указывать на нулевой символ ( *c!=’\0′ ). Если символ не равен знаку %, то выводим этот символ. Иначе смотрим, какой символ идет после знака % — d или f. В зависимости от этого получаем либо объект int, либо объект double.

Форум русскоязычного сообщества Ubuntu

Следите за новостями русскоязычного сообщества Ubuntu в Twitter-ленте @ubuntu_ru_loco

Автор Тема: Русские и английские символы через printf() в Си (Прочитано 1579 раз)


0 Пользователей и 1 Гость просматривают эту тему.

Страница сгенерирована за 0.067 секунд. Запросов: 24.

© 2012 Ubuntu-ru — Русскоязычное сообщество Ubuntu Linux.
© 2012 Canonical Ltd. Ubuntu и Canonical являются зарегистрированными торговыми знаками Canonical Ltd.

stdarg и printf () в C

Заголовочный файл используется, чтобы заставить функции принимать неопределенное количество аргументов, верно?

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

Я нашел следующие строки в файле stdio.h gcc:

Я не могу понять большую часть того, что в нем, но, похоже, оно включает в себя

Итак, если printf() использует для принятия переменного числа аргументов и stdio.h имеет printf() , программе на C, использующей printf() не обязательно включать не так ли?

Я попробовал программу, которая имела printf() и пользовательскую функцию, принимающую переменное число аргументов.

Я попробовал программу:

Он работает нормально, если включен, но в противном случае генерирует следующую ошибку:

Или это то, что printf() не использует для принятия переменного числа аргументов? Если так, как это сделано?

8 ответов

Вам нужен ? Нет , ты не . является частью грамматики языка — это «встроенный». Однако, как только вы захотите сделать что-то осмысленное и переносимое с таким списком аргументов, вам понадобятся имена, определенные там: va_list , va_start и так далее.

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

Итак, если printf () использует для принятия переменного числа аргументов и stdio.h имеет printf (), то программа на C, использующая printf (), не обязательно должна включать это?

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

Заголовочный файл stdarg используется для того, чтобы функции могли принимать неопределенное количество аргументов, верно?

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

Это языковая функция и не требует дополнительных объявлений / определений.

Я нашел следующие строки в файле stdio.h gcc:

Я предполагаю, что этот материал требуется только для объявления таких вещей, как vprintf() без внутреннего включения :

Чтобы завершить это:

  • Заголовок, который объявляет функцию с переменным числом аргументов, не должен включать внутри.
  • Реализация функции с переменным числом аргументов должна включать в себя и использовать va_list API для доступа к дополнительным аргументам.

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

Стандарт C требует, чтобы stdio.h «вел себя как-бы», он не включает stdarg.h . Другими словами, макросы va_start , va_arg , va_end и va_copy и тип va_list должны быть недоступны путем включения stdio.h . Другими словами, эта программа обязана не компилировать:

(Это отличие от C ++. В C ++ все стандартные заголовки библиотек допускаются, но не обязательно, включают друг друга.)

Это правда, что реализация printf (вероятно) использует механизм stdarg.h для доступа к своим аргументам, но это просто означает, что некоторые файлы в исходном коде для библиотеки C (« printf.c », возможно) должны включать stdarg.h а также stdio.h ; это не влияет на ваш код.

Также верно, что stdio.h объявляет функции, которые принимают аргументы типа va_list . Если вы посмотрите на эти объявления, то увидите, что на самом деле они используют имя typedef, которое начинается либо с двух подчеркиваний, либо с подчеркивания и заглавной буквы: например, с того же stdio.h вы смотрите,

Все имена, которые начинаются с двух подчеркиваний, или подчеркивания и заглавной буквы, зарезервированы для реализации — stdio.h может объявлять столько имен, сколько ему нужно. И наоборот, вам, программисту приложения, не разрешается объявлять любые такие имена или использовать те, которые объявляет реализация (за исключением задокументированного подмножества, такого как _POSIX_C_SOURCE и __GNUC__ ). Компилятор позволит вам сделать это, но эффекты не определены.

Теперь я собираюсь поговорить о том, что вы цитировали из stdio.h . Здесь это снова:

Чтобы понять, что это делает, вам нужно знать три вещи:

Недавние «проблемы» POSIX.1 , официальной спецификации того, что значит быть операционной системой «Unix», добавляют va_list к набору вещей, которые должен определять stdio.h . (В частности, в Выпуске 6 va_list определяется stdio.h как расширение «XSI», а в Выпуске 7 это обязательно.) Этот код определяет va_list , но только если программа запросила функции Выпуск 6 + XSI или Выпуск 7; это то, что #if defined __USE_XOPEN || defined __USE_XOPEN2K8 #if defined __USE_XOPEN || defined __USE_XOPEN2K8 означает. Обратите внимание, что он использует _G_va_list для определения va_list , так же как и в других местах, он использовал _G_va_list для объявления vprintf . _G_va_list уже доступен как-то.

Вы не можете написать один и тот же typedef дважды в одной и той же единице перевода. Если stdio.h определил va_list без какого-либо уведомления stdarg.h не делать это снова,

не будет компилироваться.

GCC поставляется с копией stdarg.h , но не с копией stdio.h . stdio.h вами stdio.h взят из GNU libc , который является отдельным проектом под зонтиком GNU и поддерживается отдельной (но частично совпадающей) группой людей. Важно отметить, что заголовки GNU libc не могут предполагать, что они компилируются GCC.

Итак, код, который вы цитировали, определяет va_list . Если __GNUC__ определен, что означает, что компилятор является или GCC, или кверк-совместимым клоном, он предполагает, что он может обмениваться данными с stdarg.h используя макрос с именем _VA_LIST_DEFINED , который определяется тогда и только тогда, когда определен va_list — но является макросом Вы можете проверить это с #if . stdio.h может определить сам va_list , а затем определить _VA_LIST_DEFINED , и тогда stdarg.h не сделает этого, и

скомпилирует нормально. (Если вы посмотрите на stdarg.h GCC, который, вероятно, stdarg.h в вашей системе в /usr/lib/gcc/something/something/include , вы увидите зеркальное отображение этого кода вместе с очень длинным списком других макросов это также означает «не определяйте va_list , я уже сделал это» для других библиотек C, с которыми GCC может или мог бы когда-то использоваться.)

Но если __GNUC__ не определен, то stdio.h предполагает, что он не знает, как связаться с stdarg.h . Но он знает, что безопасно включать stdarg.h дважды в один и тот же файл, потому что стандарт C требует, чтобы это работало. Поэтому, чтобы определить va_list , он просто идет вперед и включает stdarg.h , и, таким образом, va_* макросы va_* которые stdio.h не должен определять.

Это то, что люди в HTML5 назвали бы «преднамеренным нарушением» стандарта C: это нарочно неправильно, потому что неправильность таким образом с меньшей вероятностью нарушит реальный код, чем любая доступная альтернатива. Особенно,


в подавляющем большинстве случаев более вероятно появится в реальном коде, чем

поэтому гораздо важнее заставить первый работать, чем второй, хотя оба должны работать.

Наконец, вы, возможно, все еще задаетесь вопросом, откуда _G_va_list этот чертов _G_va_list . Он не определен нигде в самом stdio.h , поэтому он должен быть либо встроенным в компилятор, либо определяться одним из заголовков, которые включает stdio.h . Вот как вы узнаете все, что включает системный заголовок:

Я использовал -std=c11 чтобы убедиться, что я не компилировал ни в режимах POSIX Issue 6 + XSI, ни Issue 7, и все же мы видим stdarg.h в этом списке в любом случае — не включенный напрямую stdio.h , но libio.h , который не является стандартным заголовком. Давайте посмотрим там:

Таким образом, libio.h включает stdarg.h в специальном режиме (здесь есть еще один случай, когда для взаимодействия между системными заголовками используются макросы реализации), и ожидает, что он определит __gnuc_va_list , но использует его для определения _IO_va_list , а не _G_va_list . _G_va_list определяется _G_config.h .

. с точки зрения __gnuc_va_list . Это имя определяется stdarg.h :

И, наконец, __builtin_va_list является недокументированным внутренним свойством GCC, что означает «любой тип, подходящий для va_list с текущим ABI».

(Да, реализация stdio в GNU libc намного сложнее, чем оправдание. Объяснение заключается в том, что в прежние времена люди пытались сделать его объект FILE напрямую используемым как filebuf C ++. Это не сработало десятилетиями — на самом деле, я не уверен, сработало ли оно когда-либо : оно было заброшено до EGCS , которая существует еще в моей истории, но есть много, много пережитков попытки до сих пор держаться , либо для двоичной обратной совместимости или потому что никто не удосужился их почистить.)

(Да, если я читаю это правильно, stdio.h GNU libc не будет работать правильно с компилятором C, чей stdarg.h не определяет __gnuc_va_list . Это абстрактно неправильно, но безвредно; любой, кто хочет новый GCC-совместимый компилятор для работы с GNU libc будет иметь гораздо больше поводов для беспокойства.)

Функции с переменным числом параметров

Функции с переменным числом параметров

К ак уже обсуждалось ранее, по умолчанию параметры передаются функции через стек. Поэтому, технически, нет ограничения на количество передаваемых параметров – “запихать” можно сколько угодно. Проблема в том, как потом функция будет разбирать переданные параметры. Функции с переменным числом параметров объявляются как обычные функции, но вместо недостающих аргументов ставится многоточие. Пусть мы хотим сделать функцию, которая складывает переданные ей числа, чисел может быть произвольное количество. Необходимо каким-то образом передать функции число параметров. Во-первых, можно явно передать число параметров обязательным аргументом. Во-вторых, последний аргумент может иметь некоторое «терминальное» значение, наткнувшись на которое функция закончит выполнение.
Общий принцип работы следующий: внутри функции берём указатель на аргумент, далее двигаемся к следующему аргументу, увеличивая значение указателя.

OLD SCHOOL

Д елаем всё вручную. Функция, которая складывает переданные ей аргументы

Первый параметр – число аргументов. Это обязательный параметр. Второй аргумент – это первое переданное число, это тоже обязательный параметр. Получаем указатель на первое число

Далее считываем все числа и складываем их. В этой функции мы также при сложении проверяем на переполнение типа unsigned.

Можно сделать первый аргумент необязательным и «перешагнуть» аргумент unsigned char num, но тогда возникнет большая проблема: аргументы располагаются друг за другом, но не факт, что непрерывно. Например, в нашем случае первый аргумент будет сдвинут не на один байт, а на 4 относительно num. Это сделано для повышения производительности. На другой платформе или с другим компилятором, или с другими настройками компилятора могут быть другие результаты.

Поэтому лучше число параметров, если это аргумент, сделать типом int или unsigned int.

Можно сделать по-другому: в качестве «терминального» элемента передавать ноль и считать, что если мы встретили ноль, то больше аргументов нет. Пример

Но теперь уже передавать нули в качестве аргументов нельзя. Здесь также есть один обязательный аргумент – первое переданное число. Если его не передавать, то мы не сможем найти адрес, по которому размещаются переменные в стеке. Некоторые компиляторы (Borland Turbo C) позволяют получить указатель на …, но такое поведение не является стандартным и его нужно избегать.

VA_ARG

М ожно воспользоваться макросом va_arg библиотеки stdarg.h. Он делает практически то же самое, что и мы: получает указатель на первый аргумент а затем двигается по стеку. Пример, та же функция, только с va_arg

Первый аргумент – число параметров – также лучше делать типа int, иначе получим проблему со сдвигом, кратным 4.

Макросы и типы для работы с переменным числом параметров

Название Описание
va_list Тип, который используется для извлечения дополнительных параметров функции с переменным числом параметров
void va_start(va_list ap, paramN) Макрос инициализирует ap для извлечения дополнительных аргументов, которые идут после переменной paramN. Параметр не должен быть объявлена как register, не может иметь типа массива или указателя на функцию.
void va_end(va_list ap) Макрос необходим для нормального завершения работы функции, работает в паре с макросом va_start.
void va_copy(va_list dest, va_list src) Макрос копирует src в dest. Поддерживается начиная со стандарта C++11

Неправильное использование

Ф ункции printf и scanf типичные примеры функций с переменным числом параметров. Они имеют один обязательный параметр типа const char* — строку формата и остальные необязательные. Пусть мы вызываем эти функции и передаём им неверное количество аргументов: Если аргументов меньше, то функция пойдёт дальше по стеку и покажет какое-то значение, которое лежит «ниже» последнего аргумента, например

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

Так как очистку стека производит вызывающая функция, то стек не будет повреждён. Получается, что если изменить схему вызова и сделать так, чтобы вызываемый объект сам чистил стек после себя, то в случае неправильного количества аргументов стек будет повреждён. То есть, буде функция объявлена как __stdcall, в целях безопасности она не может иметь переменного числа аргументов.
Однако, если добавить спецификатор __stdcall к нашей функции summ она будет компилироваться. Это связано с тем, что компилятор автоматически заменит __stdcall на __cdecl.

Давайте убедимся в этом. Использование . в объявлении функции не является обязательным. То есть, если вы передадите функции больше параметров, то IDE покажет замечание, но код останется вполне рабочим. Например

Теперь объявим явно функцию как stdcall. Так как мы не использовали символа . то не произойдёт автоподмены stdcall на cdecl. Функция отработает, но после завершения стек будет повреждён.

Программа завершится с ошибкой вроде The value of ESP was not properly saved across a function call.

Форматированный вывод. Функция printf

Пожалуйста, приостановите работу AdBlock на этом сайте.

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

Общий синтаксис функции printf следующий:

Рис.1. Общий синтаксис функции printf.

У функции printf есть один обязательный параметр – строка, заключенная в двойные кавычки. Эту строку еще называют формат-строкой .

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

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

Формат-строка.

Любой символ в формат-строке относится к одной следующих групп:

  • символы, которые выводятся на экран без изменений
  • escape-последовательности
  • спецификаторы формата

Еscape-последовательности

С этой группой символов мы уже встречались в первом уроке. Символ \n . Он, как вы наверное помните, переносит выводимый текст на новую строку. Есть и другие эскейп-последовательности (иногда можно встретить название управляющие последовательности). Любая такая последовательность начинается с символа обратный слеш \ .

Часто используемые escape-последовательности:

\n — новая строка
\t — горизонтальная табуляция. Сдвигает выводимые данные вправо на ближайшую позицию табуляции. Обычно используется для выравнивания текста внутри строки.
\’ — вывод символа ‘
— вывод символа »
\\ — вывод символа \
\? — вывода символа ?

Как видите, последние четыре последовательности нужны лишь для того, чтобы вывести на экран символы «»», «’», «\» и «?». Дело в том, что если эти символы просто записать в формат-строку, то они не отобразятся на экране, а в некоторых случаях программа и вовсе не скомпилируется.

Следующая программа иллюстрирует работу escape-последовательностей.

Хотя escape-последовательности состоят из нескольких символов, но в потоке вывода они воспринимаются как цельный символ, который имеет своё собственное значение.

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

Спецификаторы формата.

Спецификаторы формата всегда начинаются с символа % , и предназначены для вывода на экран значений переменных и выражений.

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

Основные спецификаторы формата:

%d, %i — целые числа
%f, %g — вещественные числа
%c — символы

Есть и другие спецификаторы формата. Мы познакомимся с ними тогда, когда они нам понадобятся.

Сами спецификаторы формата на экран не выводятся. Вместо них выводятся данные, которые передаются в функцию printf после строки форматирования.

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

Посмотрим на примерах.

Рис.2 Вывод Листинг 2.

Рис.3 Вывод Листинг 3.

Рис.4 Вывод Листинг 4.

Рис.5 Вывод Листинг 5.

На следующей картинке показан принцип работы функции printf .

Рис.6 Принцип работы функции printf.

По сути, формат строка задаёт некоторый трафарет(шаблон), в который подставляются данные для вывода, в том порядке, в котором они указаны.

Два основных правила, которые нужно соблюдать при работе с функцией printf :

  • количество спецификаторов формата должно совпадать с количеством данных для вывода
  • спецификаторы формата должны точно соответствовать типам выводимых данных

Пара примеров неправильного использования функции printf .

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

Модификаторы формата.

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

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

Рис.7 Модификатор формата

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

В примере на картинке под вещественное число мы выделяем 8 символов и хотим видеть 3 знака после запятой.

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

У первого числа может спереди еще стоять знак минус, например %-8.3f . Этот минус говорит о том, что необходимо выровнять число по левому краю используемой области.

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

Результат работы данной программы представлен на рисунке ниже.

Рис.8 Вывод Листинг 8.

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

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

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

Практика

Решите предложенные задачи. Для удобства работы сразу переходите в полноэкранный режим

Исследовательские задачи для хакеров:

  1. Теперь вы можете увидеть, как выглядит мусор, который хранится в переменной после её объявления. Для этого объявите переменную любого типа и выведете её значение на экран использую функцию printf .
  2. Найдите в стандарте языка, в справочной системе компилятора полный список escape-последовательностей. Попробуйте использовать их в вашей программе. Разберитесь как они работают.
  3. Выясните, что произойдет, если в формат-строку функции printf вставить \c , где c — любой символ, не входящий в список escape-последовательностей. [K&R]
  4. Разберитесь с тем, как работает модификатор точности для целых чисел.
  5. Попробуйте вывести символы «»», «’», «\» и «?» без использования escape-последовательностей.
  6. Изучите как работает спецификатор %e

Дополнительные материалы

  1. Ранее я говорил, что тип double называется типом двойной точности, и что он в некотором смысле лучше, чем тип float . Запустите следующую программу, чтобы убедиться в этом.

Причина подобного результата в том, что количество памяти выделенное для хранения любой переменной ограничено Из-за этого вещественные числа хранятся в памяти компьютера приближенно. Для типа float памяти выделяется обычно меньше, чем для типа double . Поэтому возникают такие забавные моменты. Отдельно обратите внимание на результат деления целых чисел, записанный в переменную z . Об этом мы поговорим в следующем уроке.

Цукерберг рекомендует:  SSH - швейцарский нож linux-админа
Понравилась статья? Поделиться с друзьями:
Все языки программирования для начинающих