Страницы

Поиск по вопросам

Показаны сообщения с ярлыком printf. Показать все сообщения
Показаны сообщения с ярлыком printf. Показать все сообщения

среда, 5 февраля 2020 г.

ANSI-последовательности (ANSI escape sequences)

#c #printf


Как эта магия работает?Может ли этот магически код \033 делать что-то ещё? Это реализовано
на уровене ОС или Си так умеет делать?
    


Ответы

Ответ 1



В свое время это было достаточно популярным средством для вывода в цвете, в разных местах экрана и т.д. и называется это ANSI-последовательностями. Более того, создавались целые движущиеся (!) картины из символов с раскраской :) Однако, как я понимаю, в первую очередь из-за нереализованности этого в консоли Windows популярность данного средства очень упала. Приводить здесь полную таблицу ANSI-кодов - не уверен, что это хорошая идея, из-за отсутствия возможности нормального форматирования в ruSO. Но прочесть о них можно, например, в Википедии или, например, тут. Никакого отношения ни к C, ни тем более к printf данная тема не имеет.

понедельник, 6 января 2020 г.

Почему возникает ошибка format string is not a string literal?

#c #printf


Согласно стандарту функция printf имеет прототип: 
int printf(const char *restrict format, ...);

Собственно, сабж:

#include 

int main (void)
{
    const char *format = "hello world!";
    printf(format);

    return 0;
}


Протест Clang:

printf.c:7:12: error: format string is not a string literal
      (potentially insecure) [-Werror,-Wformat-security]
    printf(format);
           ^~~~~~
printf.c:7:12: note: treat the string as an argument to avoid this
    printf(format);
           ^
           "%s", 
1 error generated


Почему возникает такая ошибка? Ведь строка форматирования соответствует прототипу.
    


Ответы

Ответ 1



Собственно, clang ругается на совсем другое. Первой ошибкой он говорит, что хорошо бы строку форматирования явно задать литералом, а не переменной, так как переменную может кто то случайно поменять или подменить. А явное - это всегда лучше. Подобная строка форматирования сколько раз была причиной багов и уязвимостей, что clang решает предупредить. Второй строкой он говорит, что красивее эту конструкцию писать так printf("%s", format); Но иногда лучше сразу писать puts(format); Но на самом деле ошибка возникает из за -Werror, который заставляет warning трактовать как ошибки.

суббота, 4 января 2020 г.

Как продублировать символ с помощью printf

#c #printf #оформление


printf ("%c", 196);


Как мне сделать так, что символ был напечатан не 1 раз, а допустим 5 раз, чтобы избежать
таких строк

printf ("%c%c%c%c%c, 196, 196, 196, 196, 196);


Сторонние библиотеки юзать нельзя, тольк 
    


Ответы

Ответ 1



Так не устроит? for(int i = 0; i < 5; ++i) putchar(196); Ну, или for(int i = 0; i < 5; ++i) printf("%c",196); P.S. Если бы работать в C++, то там символ-заполнитель можно указать - char k = 'a'; cout << setfill(k) << setw(5) << k << endl; Но это так, досужие рассуждения, так как вопрос именно о C...

воскресенье, 1 декабря 2019 г.

Возможно ли в С вывести что-нибудь в консоль, не используя stdio.h?

#c #printf


Возможно ли в С вывести что-нибудь в консоль, не используя stdio.h, printf?
    


Ответы

Ответ 1



stdio.h - это буферизированная надстройка над базовыми функциями ввода-вывода read, write, объявленными в unistd.h. #include write(1, "Hallo!\n", sizeof "Hallo!\n" - 1); Первый аргумент - файловый дескриптор - номер в таблице открытых файлов исполняемой программы. При запуске по стандарту заранее открыты три дескриптора: 0 - стандартный ввод, 1 - стандартный вывод, 2 - стандартный вывод ошибок. В некоторых системах могут быть ещё другие. fwrite из того же stdio.h. Работает похожим образом, но через тот же буфер, как и printf: #include fwrite("Hallo!\n", sizeof "Hallo!\n" - 1, 1, stdout); Хотя она и вызывает write, но за счёт буферизации часто оказывается быстрее при записи большого количества данных в файл (при перенаправлении вывода). Безусловно fwrite быстрее printf, так как последняя ищет в строке форматные последовательности, начинающиеся со знака %. fputs и puts из stdio.h выводят строку, оканчивающуюся нулевым байтом ('\0'). Вторая добавляет ещё и символ конца строки и выводит только на стандартный вывод. #include fputs("Hallo!\n", stdout); puts("Hallo!"); // выводит то же самое, что и предыдущая функция По скорости должны быть как fwrite, но отличаются способом определения длины строки, поэтому с их помощью невозможно вывести сами нулевые байты. Мои измерения в ubuntu однако показывают, что puts почему-то намного медленнее fputs и почти как printf.

Ответ 2



В дополнение к POSIX write(2) и Win32 WriteConsoleW() можно упомянуть функции, которые также позволяют выводить в консоль: Библиотечные функции, объявленные в conio.h, весьма различались в зависимости от компилятора. Первоначально реализованные в Microsoft Visual C++ различные функции привязывались напрямую к нескольким первым функциям DOS, связанным с прерыванием 21h. Но библиотека, поставляемая с Turbo C++ и Borland C++, не использует DOS API, а вместо этого напрямую обращается к видеопамяти для вывода информации и использования сигналов прерывания BIOS. #include putch('A'); Также (зависит от железа, ОСи) можно выводить символы на экран с помощью прямой манипуляции памяти, которая отображена на видео буфер. Из книги "Computer Graphics Primer", написанной Mitchell Waite и опубликованной в 1979: IBM VGA BIOS (mode 13h) режим предоставляет похожий доступ к видеопамяти как к 320×200 массиву пикселов с 256 цветами. Обычно между программой и видеопамятью присутствует множество уровней абстракции. На Linux, если X не запущен(или в Ctrl+Alt+Fx виртуальной консоли), можно mmap /dev/fb0 устройство (Linux framebuffer всего на ступеньку выше работы с видеокартой напрямую—интерфейс предоставляется самим ядром): fbdev = open("/dev/fb0", O_RDWR); buffer = mmap(0, screensize, PROT_READ | PROT_WRITE, MAP_SHARED, fbdev, 0); Здесь buffer—это массив байтов размером screensize, запись в который позволяет рисовать в консоли, например, в режиме 1600x1200 32bpp (screensize = 1600*1200*32/CHAR_BIT), чтобы закрасить выбранный пиксель ярко жёлтым (красный+зелёный): buffer[i+0] = 0; // синий buffer[i+1] = 255; // зелёный buffer[i+2] = 255; // красный buffer[i+3] = 0; // прозрачность Вот пример кода, как квадрат нарисовать в консоли, используя framebuffer: How to display something on screen through linux framebuffer?

Ответ 3



Для винды: WriteConsole(). Для DOS - формирование кода вызова INT 21 и через передачу управления на него.

Ответ 4



#include /* для любой linux системы. У меня gcc (Ubuntu 4.8.4-2ubuntu1~14.04) 4.8.4 */ int main() { int i = system ("ls -al"); // показать файлы текущей директории в тч скрытые return 0; }

Ответ 5



Borland C++ 3.1: #include int main() { putch('7'); return 0; }

понедельник, 25 ноября 2019 г.

printf как средство печати переменных в С


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


Как напечатать содержимое переменной в С:

char msg = 'k';
printf("%c", msg);

Как напечать содержимое переменной в С++:

char msg = 'k';
cout << msg;



А теперь вопросы:


В C++ для вывода не нужно указывать модификатор, тогда почему в С нет аналога cout (я все понимаю, что язык гораздо старше C++, но все же)?
Мне часто пишут, что тип в printf нужно приводить:

char msg = 'k';
printf("%f", (float)msg);



А зачем? В аналоге С++, этого же не надо делать!
Почему я не могу написать так: printf("%f", msg);? В чем причина? Разве printf не приведет данные самостоятельно к тому модификатору, который мы указываем?
    


Ответы

Ответ 1



В C, printf(format, msg) в исходном коде вызывает одну и ту же функцию вне зависимост от типа, значения msg переменной. C — статически типизированный язык, это значит, чт во время исполнения printf функция не знает, что msg это char, более того printf даж не знает сколько её аргументов передали. Поэтому приходится руками в format строке задавать желаемое представление: значение какого типа из памяти, в которой msg лежит, загрузить и как это значение отформатировать, чтобы получившиеся байты затем в stdout записать. В C++, cout << msg для разных типов msg может скомпилировать вызов разных функци (перегрузки оператора <<). Вы легко можете определить свой оператор <<, к примеру, чтобы выводить векторы для отладки: #include #include template std::ostream& operator << (std::ostream& os, const std::vector& v) { for (auto&& x : v) os << x << ' '; return os; } int main() { std::vector v {1, 2, 3}; std::cout << v << '\n'; } Пример: $ g++ -std=c++11 main.cc -o main $ ./main 1 2 3 Упрощая, можно представить, что компилятор для cout << msg генерирует вызов print_char(msg) если msg это char, print_float(msg), если msg это float, print_vector(msg), если ms это vector итд. Здесь каждая функция знает какой тип она принимает и для каждого типа используется одно представление в виде байт по умолчанию, если iomanip не используются). К примеру, для int выводятся его десятичные цифры (а не hex или что-то другое) по умолчанию. В C11 появился _Generic, который позволяет разные функции в зависимости от тип аргумента (controlling-expression) вызывать, поэтому можно определить print_arg(arg), которое бы работало для разных типов arg (один аргумент). Почему я не могу написать так: printf("%f", msg);? В чем причина? Разве print не приведет данные самостоятельно к тому модификатору, который мы указываем? ... printf("%f", (float)msg); \\ 107.000000, а printf("%f", msg); \\ 0.000000 почему ответ разный? При вызове вариативных (variadic) функций таких как printf, объявленных с многоточие (...) в параметрах, которые могут принимать разное количество аргументов c неизвестным компилятору ожидаемыми типами (С компилятор не обязан понимать язык printf format-строк и значение format может быть вообще неизвестно во время компиляции), происходят преобразования по умолчанию: char неявно в int превращается (integer promotion) или в unsigned int, если значение char не представимо в int на данной платформе (экзотика). Аналогично, float в double превращается при вызове printf() — поэтому не требуется %lf для double в printf(), а используется просто %f. Для случая, c printf можно использовать простую модель: компилятор кладёт аргумент (int, double, etc) в область памяти, а printf читает их оттуда в соответствии с инструкциям в format строке — printf выступает в роли мини-компьютера: format задаёт программу и printf читает память (va_arg), в которой её аргументы лежат, форматирует их и записывает получившиеся байты в stdout. (double)msg объект может отличаться в памяти от (int)msg объекта. Поэтому print может видеть разные битовые паттерны в памяти и соответственно, результат printf("%f", (int)msg) и printf("%f", (double)msg) могут быть разными (одна и та же инструкция: %f применяется к разному содержимому в памяти). Пример для моей машины (как типы выглядят в памяти может зависеть от платформы (операционно системы + процессор) и опций компилятора). Байты в памяти я буду в виде hexdump показывать (к примеру: 6B16 == 10710). Для printf("%c", msg): 'k' в C имеет тип int — 6B 00 00 00 char msg — 6B в printf это передаётся как int — 6B 00 00 00 %c берёт этот аргумент 6B 00 00 00 и превращает его в unsigned char (6B) и соответствующий байт (6B) выводится в stdout. См. printf документацию для c формата. Для printf("%f", (float)msg): msg (6B) преобразуется в (float)msg — 00 00 D6 42 что передаётся в printf в виде double — 00 00 00 00 00 C0 5A 40 %f интерпретирует память 00 00 00 00 00 C0 5A 40 как число с плавающей точкой и выводи в фиксированном формате (6 знаков после запятой): 107.000000 (символ для точки от локали может зависеть). Соответствующие байты в ascii кодировке, которые в stdout пишутся: 31 30 37 2e 30 30 30 30 30 30 Для printf("%f", msg): msg (char — 6B) передаётся в printf в виде int — 6B 00 00 00 %f интерпретирует память 6B 00 00 00 XX XX XX XX как число с плавающей точкой 5.3e-322 (если XX == 00) и выводится 0.000000 Так как при неверном формате поведение не определено (undefined behavior — UB), т printf("%f", msg) может сделать всё что угодно, хоть ракеты запустить. На моей машине результат printf("%f", msg) зависит от предыдущего кода к примеру: char c = 'k'; printf("%f\n", (float)c); printf("%f\n", c); печатает: 107.000000 107.000000 # XX XX XX XX == 00 C0 5A 40 (остатки от предыдущего вызова) но: printf("%f\n", 5.3e-322); printf("%f\n", c); печатает: 0.000000 0.000000 # XX XX XX XX == 00 00 00 00 (остатки от предыдущего вызова) Убедитесь, что для форматов известных во время компиляции (к примеру "%f\n") ва компилятор генерирует предупреждения для неверных типов — UB следует избегать. printf("%i", (int)msg); \\\ 107 и printf("%i", msg); в обоих случая msg char передаётся как int (6B 00 00 00) и при одинаковых форматах, результат должен быть одинаковым (6B16 == 10710). Можно посмотреть содержимое переменных на вашей машине с помощью С кода: #include static void print_memory(unsigned char *memory, size_t n) { for (unsigned char *p = memory; p != memory + n; ++p) printf("%02X ", *p); puts(""); } int main(void) { char c = 'k'; print_memory((unsigned char *)&c, sizeof c); int i = c; print_memory((unsigned char *)&i, sizeof i); float f = c; print_memory((unsigned char *)&f, sizeof f); double d = f; print_memory((unsigned char *)&d, sizeof d); } Пример: $ gcc -std=c99 main.c -o main $ ./main 6B 6B 00 00 00 00 00 D6 42 00 00 00 00 00 C0 5A 40 Как double в памяти может выглядеть Пример из ответа, применённый к 107.0: число двойной точности в IEEE 754 формат представляется как d = ±знак · (1 + мантисса / 252) · 2порядок − 1023 знак, мантисса и порядок упакованы в двоичном представлении как: 00 00 00 00 00 C0 5A 40 // little-endian (8 byte as hex) 40 5A C0 00 00 00 00 00 // big-endian 0100000001011010110000000000000000000000000000000000000000000000 # 64-bit ^ |-самый левый бит знак= 0 (положительный) ^ ^ |---------| Затем 11 бит порядок= 0b10000000101 (==1029) ^ ^ |--------------------------------------------------| Оставшиеся 52 бита манитисса= 0b1010110000000000000000000000000000000000000000000000 = 3025855999639552 = 0xac00000000000 Всё вместе: d = +(1 + 3025855999639552 / 252) * 2(1029 - 1023) = 64 + 3025855999639552 / 70368744177664 = (4503599627370496 + 3025855999639552) / 70368744177664 = 7529455627010048 / 70368744177664 = 107.0 Это демонстрирует, почему 107.0 может быть представлено в памяти как 00 00 00 0 00 C0 5A 40. Пример урезанной printf функции #include // va_list, va_arg() // emulate some printf() functionality using writec() static void print(const char* format, ...) { va_list args; va_start(args, format); int infmt = 0, is_char = 0; union { int i; double f; } arg; char buffer[23] = {0}; // enough for %c, %d, %f for (const char *p = format; *p; ++p) { if (infmt) { // print arg infmt = 0; switch(*p) { case '%': // "%%": print % literally writec(*p); break; case 'c': // "%c": print char is_char = 1; // fall through case 'd': // "%d": print int case 'i': // "%i": print int arg.i = va_arg(args, int); // load int arg if (is_char) { is_char = 0; writec((unsigned char)arg.i); // format as char, write } else { itoa(arg.i, buffer, sizeof buffer); // format as int for (char *pb = buffer; *pb; ++pb) writec(*pb); // write } break; case 'f': arg.f = va_arg(args, double); // load double arg ftoa(arg.f, buffer, sizeof buffer); // format as floating point for (char *pb = buffer; *pb; ++pb) writec(*pb); // write break; default: arg.i = va_arg(args, int); // load int arg scpy(buffer, ""); for (char *pb = buffer; *pb; ++pb) writec(*pb); // write }; } else if (*p != '%') { // print literally writec(*p); } else { // *p == '%' infmt = 1; } } va_end(args); } switch используется, чтобы распознать описатели преобразований (%d) в format-строке va_arg() загружает аргументы нужного типа вспомогательные функции itoa() и ftoa() форматируют int и double соответственно writec() пишет один байт в stdout. Это определение print() достаточно для кода: int main(void) { char msg = 'k'; print("%c %i %i %f %x\n", msg, msg, (int)msg, (float)msg, msg); print("%c ", msg); print("%i ", msg); print("%i ", (int)msg); print("%f\n", (float)msg); print("%f\n", msg); // XXX UB } Пример: $ cc -std=c99 print-example.c -o print-example $ ./print-example k 107 107 107.000000 k 107 107 107.000000 107.000000 Чтобы скомпилировать, достаточно вспомогательные функции определить (определени перед print() нужно вставить): #include // POSIX write() static void writec(unsigned char c) { write(1, &c, 1); } static void scpy(char* dest, const char* src) { while (*dest++ = *src++); } static void ftoa(double d, char* buffer, int n) { if (d == 107) //XXX scpy(buffer, "107.000000"); else scpy(buffer, "XXX"); } /// format positive int as decimal ascii digits static char* utoa_rec(unsigned i, char* buffer, int *pn) { if (i >= 10) buffer = utoa_rec(i / 10, buffer, pn); if ((*pn)-- > 0) *buffer++ = '0' + (i % 10); return buffer; } static void itoa(int i, char* buffer, int n) { if (i < 0) { i = -i; //XXX ignore INT_MIN if (n-- > 0) *buffer++ = '-'; // sign } buffer = utoa_rec(i, buffer, &n); if (n > 0) *buffer++ = '\0'; } Определения функций приведены, чтобы можно было пример запустить, но фактически он просто заглушками являются (не для повторного использования), чтобы только продемонстрировать одну из простейших print(format, ...) реализаций. Вот пример полной реализации vfprintf() из glibc.

Ответ 2



почему в С нет аналога cout Функциональность cout в языке С++ критически завязана на механизм перегрузки функци на уровне библиотеки, т.е. фактически на существовании доступного библиотеке (и пользователю механизма перегрузки функций, а также сопутствующего механизма перегрузки операторов. Правильная версия функции вывода выбирается механизмом перегрузки на основе анализа типа указанного вами аргумента. В языке С нет механизмов перегрузки функций и перегрузки операторов на уровне пользователя или библиотеки. Поэтому нет и таких внешне "типонезависимых" операций ввода-вывода. В версии С11 стандарта языка С появился механизм generic-выражений, который може использоваться для эмуляции пользовательской/библиотечной перегрузки функций. Этот механизм, например, используется (может использоваться) в стандартном заголовочном файле для реализации "перегруженных" математических функций-макросов. Но реализаций "типонезависимых" функций ввода-вывода в стандартной библиотеке н делалось. Если есть желание - можете воспользоваться этим новым механизмом и попробовать реализовать их самостоятельно. Мне часто пишут что тип в printf нужно приводить. А зачем? Разве printf не приведет данные самостоятельно к тому модификатору, который мы указываем? printf - это так называемая variadic функция. Все аргументы этой функции, кроме первого, являются variadic аргументами. Они соответствуют ... в объявлении списка параметров функции int printf( const char* format, ... ); Такие аргументы передаются через особый механизм предачи параметров. Его особенность является то, что сама функция printf абсолютно ничего не знает о типах фактически передаваемых аргументов и по этой причине ничего самостоятельно привести к правильному типу не может. С чисто практической точки зрения вы можете считать, что все variadic аргументы записываютс в непрерывный бинарный поток. Внутри себя функция printf будет читать бинарные данны из этого потока при помощи механизма va_list/va_start/va_arg. Что именно записано этот поток функция printf сама по себе знать не может. Поэтому разбирать этот бинарны поток на части она будет в соответствии с тем форматом, который вы сами ей снаружи передали. И если вы ей "наврете" в этом формате, то она сама ничего не подозревая, будет разбирать этот бинарный поток неправильно. По этой причине все данные, которые вы кладете в этот бинарный поток, должны в точности соответствовать тем спецификаторам формата, которые вы указали в форматной строке. Реализации не обязаны реализовать передачу variadic аргументов именно таким образом, но особенности передачи variadic аргументов эта модель иллюстрирует достаточно точно.

Ответ 3



Для работы cout необходима перегрузка оператора <<, причём для каждого типа она своя, потому и не требуется модификаторов. В Си операторы не перегружаются. Мне кажется, слово "приведёт" не совсем точно, я бы сказал "интерпретирует": print выведет значение переменной так, как если бы она была того типа, что указан в строк формата. Переменная при этом не меняет значения. Сама printf не имеет возможности определить соответствие аргументов строке формата на этапе выполнения (а компилятор Си не учит программиста, как жить и где разложить себе грабли). Поэтому возможны всякие сюрпризы, если случайно допустить ошибку в строке формата и спутать типы или количество аргументов.

понедельник, 8 апреля 2019 г.

ANSI-последовательности (ANSI escape sequences)

Как эта магия работает?Может ли этот магически код \033 делать что-то ещё? Это реализовано на уровене ОС или Си так умеет делать?


Ответ

В свое время это было достаточно популярным средством для вывода в цвете, в разных местах экрана и т.д. и называется это ANSI-последовательностями. Более того, создавались целые движущиеся (!) картины из символов с раскраской :)
Однако, как я понимаю, в первую очередь из-за нереализованности этого в консоли Windows популярность данного средства очень упала.
Приводить здесь полную таблицу ANSI-кодов - не уверен, что это хорошая идея, из-за отсутствия возможности нормального форматирования в ruSO. Но прочесть о них можно, например, в Википедии или, например, тут
Никакого отношения ни к C, ни тем более к printf данная тема не имеет.

понедельник, 11 февраля 2019 г.

Почему возникает ошибка format string is not a string literal?

Согласно стандарту функция printf имеет прототип: int printf(const char *restrict format, ...);
Собственно, сабж:
#include
int main (void) { const char *format = "hello world!"; printf(format);
return 0; }
Протест Clang
printf.c:7:12: error: format string is not a string literal (potentially insecure) [-Werror,-Wformat-security] printf(format); ^~~~~~ printf.c:7:12: note: treat the string as an argument to avoid this printf(format); ^ "%s", 1 error generated
Почему возникает такая ошибка? Ведь строка форматирования соответствует прототипу.


Ответ

Собственно, clang ругается на совсем другое. Первой ошибкой он говорит, что хорошо бы строку форматирования явно задать литералом, а не переменной, так как переменную может кто то случайно поменять или подменить. А явное - это всегда лучше. Подобная строка форматирования сколько раз была причиной багов и уязвимостей, что clang решает предупредить.
Второй строкой он говорит, что красивее эту конструкцию писать так
printf("%s", format);
Но иногда лучше сразу писать
puts(format);
Но на самом деле ошибка возникает из за -Werror, который заставляет warning трактовать как ошибки.

понедельник, 28 января 2019 г.

Как продублировать символ с помощью printf

printf ("%c", 196);
Как мне сделать так, что символ был напечатан не 1 раз, а допустим 5 раз, чтобы избежать таких строк
printf ("%c%c%c%c%c, 196, 196, 196, 196, 196);
Сторонние библиотеки юзать нельзя, тольк


Ответ

Так не устроит?
for(int i = 0; i < 5; ++i) putchar(196);
Ну, или
for(int i = 0; i < 5; ++i) printf("%c",196);
P.S. Если бы работать в C++, то там символ-заполнитель можно указать -
char k = 'a'; cout << setfill(k) << setw(5) << k << endl;
Но это так, досужие рассуждения, так как вопрос именно о C...

пятница, 12 октября 2018 г.

С и переменное число аргументов

Здравствуйте. Такой вопрос появился. Есть ли в C удобный способ последовательно затолкать элементы массива в функции с переменным количеством аргументов. Поясню на примере: void print_arr(char *fmt, char **arr, int arrsz) { printf(fmt, ???); } Или обязательно писать обвёртку, которая будет с массивами работать? Можно ли как-то стандартыми средствами преобразовать arr в тип va_list и передать vprintf? Заранее спасибо за ваши ответы.


Ответ

Возможно появились компиляторы C, в которых va_list реализовано по-другому, но c давних пор это работало: char *args[] = {"one", "two", "three"}; vprintf("%s, %s, %s
", args); gcc выдаёт предупреждение, которое легко снимается преобразованием: vprintf("%s, %s, %s
", (va_list)args); результат тот который нужен: $ gcc valist.c $ ./a.out one, two, three Для gcc под amd64 найдено экспериментально следующее извращение: char *args[] = {"one", "two", "three"}; struct { int x; char **y, **z; } valist = {48, &args, &args}; vprintf("%s, %s, %s
", (void*)&valist); Если sizeof(va_list) == 4 то первый способ скорее всего работает, во втором случае sizeof(va_list) == 24

вторник, 2 октября 2018 г.

printf как средство печати переменных в С

Я не знаю как точно сформулировать то, что я хочу спросить, но выглядит это следующим образом:
Как напечатать содержимое переменной в С
char msg = 'k'; printf("%c", msg); Как напечать содержимое переменной в С++
char msg = 'k'; cout << msg;
А теперь вопросы:
В C++ для вывода не нужно указывать модификатор, тогда почему в С нет аналога cout (я все понимаю, что язык гораздо старше C++, но все же)? Мне часто пишут, что тип в printf нужно приводить:
char msg = 'k'; printf("%f", (float)msg);
А зачем? В аналоге С++, этого же не надо делать! Почему я не могу написать так: printf("%f", msg);? В чем причина? Разве printf не приведет данные самостоятельно к тому модификатору, который мы указываем?


Ответ

В C, printf(format, msg) в исходном коде вызывает одну и ту же функцию вне зависимости от типа, значения msg переменной. C — статически типизированный язык, это значит, что во время исполнения printf функция не знает, что msg это char, более того printf даже не знает сколько её аргументов передали. Поэтому приходится руками в format строке задавать желаемое представление: значение какого типа из памяти, в которой msg лежит, загрузить и как это значение отформатировать, чтобы получившиеся байты затем в stdout записать.
В C++, cout << msg для разных типов msg может скомпилировать вызов разных функций (перегрузки оператора <<). Вы легко можете определить свой оператор <<, к примеру, чтобы выводить векторы для отладки:
#include #include
template std::ostream& operator << (std::ostream& os, const std::vector& v) { for (auto&& x : v) os << x << ' '; return os; }
int main() { std::vector v {1, 2, 3}; std::cout << v << '
'; }
Пример:
$ g++ -std=c++11 main.cc -o main $ ./main 1 2 3
Упрощая, можно представить, что компилятор для cout << msg генерирует вызов print_char(msg), если msg это char, print_float(msg), если msg это float, print_vector(msg), если msg это vector итд. Здесь каждая функция знает какой тип она принимает и для каждого типа используется одно представление в виде байт по умолчанию, если iomanip не используются). К примеру, для int выводятся его десятичные цифры (а не hex или что-то другое) по умолчанию.
В C11 появился _Generic, который позволяет разные функции в зависимости от типа аргумента (controlling-expression) вызывать, поэтому можно определить print_arg(arg), которое бы работало для разных типов arg (один аргумент).
Почему я не могу написать так: printf("%f", msg);? В чем причина? Разве printf не приведет данные самостоятельно к тому модификатору, который мы указываем? ... printf("%f", (float)msg); \\ 107.000000, а printf("%f", msg); \\ 0.000000 почему ответ разный?
При вызове вариативных (variadic) функций таких как printf, объявленных с многоточием (...) в параметрах, которые могут принимать разное количество аргументов c неизвестными компилятору ожидаемыми типами (С компилятор не обязан понимать язык printf format-строки и значение format может быть вообще неизвестно во время компиляции), происходят преобразования по умолчанию: char неявно в int превращается (integer promotion) или в unsigned int, если значение char не представимо в int на данной платформе (экзотика). Аналогично, float в double превращается при вызове printf() — поэтому не требуется %lf для double в printf(), а используется просто %f
Для случая, c printf можно использовать простую модель: компилятор кладёт аргументы (int, double, etc) в область памяти, а printf читает их оттуда в соответствии с инструкциями в format строке — printf выступает в роли мини-компьютера: format задаёт программу и printf читает память (va_arg), в которой её аргументы лежат, форматирует их и записывает получившиеся байты в stdout
(double)msg объект может отличаться в памяти от (int)msg объекта. Поэтому printf может видеть разные битовые паттерны в памяти и соответственно, результат printf("%f", (int)msg) и printf("%f", (double)msg) могут быть разными (одна и та же инструкция: %f применяется к разному содержимому в памяти).
Пример для моей машины (как типы выглядят в памяти может зависеть от платформы (операционной системы + процессор) и опций компилятора). Байты в памяти я буду в виде hexdump показывать (к примеру: 6B16 == 10710).
Для printf("%c", msg)
'k' в C имеет тип int — 6B 00 00 00 char msg — 6B в printf это передаётся как int — 6B 00 00 00 %c берёт этот аргумент 6B 00 00 00 и превращает его в unsigned char (6B) и соответствующий байт (6B) выводится в stdout. См. printf документацию для c формата
Для printf("%f", (float)msg)
msg (6B) преобразуется в (float)msg — 00 00 D6 42 что передаётся в printf в виде double — 00 00 00 00 00 C0 5A 40 %f интерпретирует память 00 00 00 00 00 C0 5A 40 как число с плавающей точкой и выводит в фиксированном формате (6 знаков после запятой): 107.000000 (символ для точки от локали может зависеть). Соответствующие байты в ascii кодировке, которые в stdout пишутся: 31 30 37 2e 30 30 30 30 30 30
Для printf("%f", msg)
msg (char — 6B) передаётся в printf в виде int — 6B 00 00 00 %f интерпретирует память 6B 00 00 00 XX XX XX XX как число с плавающей точкой 5.3e-322 (если XX == 00) и выводится 0.000000
Так как при неверном формате поведение не определено (undefined behavior — UB), то printf("%f", msg) может сделать всё что угодно, хоть ракеты запустить. На моей машине результат printf("%f", msg) зависит от предыдущего кода к примеру:
char c = 'k'; printf("%f
", (float)c); printf("%f
", c);
печатает:
107.000000 107.000000 # XX XX XX XX == 00 C0 5A 40 (остатки от предыдущего вызова)
но:
printf("%f
", 5.3e-322); printf("%f
", c);
печатает:
0.000000 0.000000 # XX XX XX XX == 00 00 00 00 (остатки от предыдущего вызова)
Убедитесь, что для форматов известных во время компиляции (к примеру "%f
") ваш компилятор генерирует предупреждения для неверных типов — UB следует избегать.
printf("%i", (int)msg); \\\ 107 и printf("%i", msg);
в обоих случая msg char передаётся как int (6B 00 00 00) и при одинаковых форматах, результат должен быть одинаковым (6B16 == 10710).
Можно посмотреть содержимое переменных на вашей машине с помощью С кода:
#include
static void print_memory(unsigned char *memory, size_t n) { for (unsigned char *p = memory; p != memory + n; ++p) printf("%02X ", *p); puts(""); }
int main(void) { char c = 'k'; print_memory((unsigned char *)&c, sizeof c); int i = c; print_memory((unsigned char *)&i, sizeof i); float f = c; print_memory((unsigned char *)&f, sizeof f); double d = f; print_memory((unsigned char *)&d, sizeof d); }
Пример:
$ gcc -std=c99 main.c -o main $ ./main 6B 6B 00 00 00 00 00 D6 42 00 00 00 00 00 C0 5A 40
Как double в памяти может выглядеть
Пример из ответа, применённый к 107.0: число двойной точности в IEEE 754 формате представляется как d = ±знак · (1 + мантисса / 252) · 2порядок − 1023
знак, мантисса и порядок упакованы в двоичном представлении как:
00 00 00 00 00 C0 5A 40 // little-endian (8 byte as hex) 40 5A C0 00 00 00 00 00 // big-endian 0100000001011010110000000000000000000000000000000000000000000000 # 64-bit ^ |-самый левый бит знак= 0 (положительный)
^ ^ |---------| Затем 11 бит порядок= 0b10000000101 (==1029) ^ ^ |--------------------------------------------------| Оставшиеся 52 бита манитисса= 0b1010110000000000000000000000000000000000000000000000 = 3025855999639552 = 0xac00000000000
Всё вместе:
d = +(1 + 3025855999639552 / 252) * 2(1029 - 1023) = 64 + 3025855999639552 / 70368744177664 = (4503599627370496 + 3025855999639552) / 70368744177664 = 7529455627010048 / 70368744177664 = 107.0
Это демонстрирует, почему 107.0 может быть представлено в памяти как 00 00 00 00 00 C0 5A 40
Пример урезанной printf функции
#include // va_list, va_arg()
// emulate some printf() functionality using writec() static void print(const char* format, ...) { va_list args; va_start(args, format); int infmt = 0, is_char = 0; union { int i; double f; } arg; char buffer[23] = {0}; // enough for %c, %d, %f
for (const char *p = format; *p; ++p) { if (infmt) { // print arg infmt = 0; switch(*p) { case '%': // "%%": print % literally writec(*p); break; case 'c': // "%c": print char is_char = 1; // fall through case 'd': // "%d": print int case 'i': // "%i": print int arg.i = va_arg(args, int); // load int arg if (is_char) { is_char = 0; writec((unsigned char)arg.i); // format as char, write } else { itoa(arg.i, buffer, sizeof buffer); // format as int for (char *pb = buffer; *pb; ++pb) writec(*pb); // write } break; case 'f': arg.f = va_arg(args, double); // load double arg ftoa(arg.f, buffer, sizeof buffer); // format as floating point for (char *pb = buffer; *pb; ++pb) writec(*pb); // write break; default: arg.i = va_arg(args, int); // load int arg scpy(buffer, ""); for (char *pb = buffer; *pb; ++pb) writec(*pb); // write }; } else if (*p != '%') { // print literally writec(*p); } else { // *p == '%' infmt = 1; } } va_end(args); }
switch используется, чтобы распознать описатели преобразований (%d) в format-строке va_arg() загружает аргументы нужного типа вспомогательные функции itoa() и ftoa() форматируют int и double соответственно writec() пишет один байт в stdout
Это определение print() достаточно для кода:
int main(void) { char msg = 'k'; print("%c %i %i %f %x
", msg, msg, (int)msg, (float)msg, msg); print("%c ", msg); print("%i ", msg); print("%i ", (int)msg); print("%f
", (float)msg); print("%f
", msg); // XXX UB }
Пример:
$ cc -std=c99 print-example.c -o print-example $ ./print-example k 107 107 107.000000 k 107 107 107.000000 107.000000
Чтобы скомпилировать, достаточно вспомогательные функции определить (определения перед print() нужно вставить):
#include // POSIX write()
static void writec(unsigned char c) { write(1, &c, 1); }
static void scpy(char* dest, const char* src) { while (*dest++ = *src++); }
static void ftoa(double d, char* buffer, int n) { if (d == 107) //XXX scpy(buffer, "107.000000"); else scpy(buffer, "XXX"); }
/// format positive int as decimal ascii digits static char* utoa_rec(unsigned i, char* buffer, int *pn) { if (i >= 10) buffer = utoa_rec(i / 10, buffer, pn); if ((*pn)-- > 0) *buffer++ = '0' + (i % 10); return buffer; }
static void itoa(int i, char* buffer, int n) { if (i < 0) { i = -i; //XXX ignore INT_MIN if (n-- > 0) *buffer++ = '-'; // sign } buffer = utoa_rec(i, buffer, &n); if (n > 0) *buffer++ = '\0'; }
Определения функций приведены, чтобы можно было пример запустить, но фактически они просто заглушками являются (не для повторного использования), чтобы только продемонстрировать одну из простейших print(format, ...) реализаций.
Вот пример полной реализации vfprintf() из glibc

понедельник, 1 октября 2018 г.

printf как средство печати переменных в С

Я не знаю как точно сформулировать то, что я хочу спросить, но выглядит это следующим образом:
Как напечатать содержимое переменной в С
char msg = 'k'; printf("%c", msg); Как напечать содержимое переменной в С++
char msg = 'k'; cout << msg;
А теперь вопросы:
В C++ для вывода не нужно указывать модификатор, тогда почему в С нет аналога cout (я все понимаю, что язык гораздо старше C++, но все же)? Мне часто пишут, что тип в printf нужно приводить:
char msg = 'k'; printf("%f", (float)msg);
А зачем? В аналоге С++, этого же не надо делать! Почему я не могу написать так: printf("%f", msg);? В чем причина? Разве printf не приведет данные самостоятельно к тому модификатору, который мы указываем?


Ответ

В C, printf(format, msg) в исходном коде вызывает одну и ту же функцию вне зависимости от типа, значения msg переменной. C — статически типизированный язык, это значит, что во время исполнения printf функция не знает, что msg это char, более того printf даже не знает сколько её аргументов передали. Поэтому приходится руками в format строке задавать желаемое представление: значение какого типа из памяти, в которой msg лежит, загрузить и как это значение отформатировать, чтобы получившиеся байты затем в stdout записать.
В C++, cout << msg для разных типов msg может скомпилировать вызов разных функций (перегрузки оператора <<). Вы легко можете определить свой оператор <<, к примеру, чтобы выводить векторы для отладки:
#include #include
template std::ostream& operator << (std::ostream& os, const std::vector& v) { for (auto&& x : v) os << x << ' '; return os; }
int main() { std::vector v {1, 2, 3}; std::cout << v << '
'; }
Пример:
$ g++ -std=c++11 main.cc -o main $ ./main 1 2 3
Упрощая, можно представить, что компилятор для cout << msg генерирует вызов print_char(msg), если msg это char, print_float(msg), если msg это float, print_vector(msg), если msg это vector итд. Здесь каждая функция знает какой тип она принимает и для каждого типа используется одно представление в виде байт по умолчанию, если iomanip не используются). К примеру, для int выводятся его десятичные цифры (а не hex или что-то другое) по умолчанию.
В C11 появился _Generic, который позволяет разные функции в зависимости от типа аргумента (controlling-expression) вызывать, поэтому можно определить print_arg(arg), которое бы работало для разных типов arg (один аргумент).
Почему я не могу написать так: printf("%f", msg);? В чем причина? Разве printf не приведет данные самостоятельно к тому модификатору, который мы указываем? ... printf("%f", (float)msg); \\ 107.000000, а printf("%f", msg); \\ 0.000000 почему ответ разный?
При вызове вариативных (variadic) функций таких как printf, объявленных с многоточием (...) в параметрах, которые могут принимать разное количество аргументов c неизвестными компилятору ожидаемыми типами (С компилятор не обязан понимать язык printf format-строки и значение format может быть вообще неизвестно во время компиляции), происходят преобразования по умолчанию: char неявно в int превращается (integer promotion) или в unsigned int, если значение char не представимо в int на данной платформе (экзотика). Аналогично, float в double превращается при вызове printf() — поэтому не требуется %lf для double в printf(), а используется просто %f
Для случая, c printf можно использовать простую модель: компилятор кладёт аргументы (int, double, etc) в область памяти, а printf читает их оттуда в соответствии с инструкциями в format строке — printf выступает в роли мини-компьютера: format задаёт программу и printf читает память (va_arg), в которой её аргументы лежат, форматирует их и записывает получившиеся байты в stdout
(double)msg объект может отличаться в памяти от (int)msg объекта. Поэтому printf может видеть разные битовые паттерны в памяти и соответственно, результат printf("%f", (int)msg) и printf("%f", (double)msg) могут быть разными (одна и та же инструкция: %f применяется к разному содержимому в памяти).
Пример для моей машины (как типы выглядят в памяти может зависеть от платформы (операционной системы + процессор) и опций компилятора). Байты в памяти я буду в виде hexdump показывать (к примеру: 6B16 == 10710).
Для printf("%c", msg)
'k' в C имеет тип int — 6B 00 00 00 char msg — 6B в printf это передаётся как int — 6B 00 00 00 %c берёт этот аргумент 6B 00 00 00 и превращает его в unsigned char (6B) и соответствующий байт (6B) выводится в stdout. См. printf документацию для c формата
Для printf("%f", (float)msg)
msg (6B) преобразуется в (float)msg — 00 00 D6 42 что передаётся в printf в виде double — 00 00 00 00 00 C0 5A 40 %f интерпретирует память 00 00 00 00 00 C0 5A 40 как число с плавающей точкой и выводит в фиксированном формате (6 знаков после запятой): 107.000000 (символ для точки от локали может зависеть). Соответствующие байты в ascii кодировке, которые в stdout пишутся: 31 30 37 2e 30 30 30 30 30 30
Для printf("%f", msg)
msg (char — 6B) передаётся в printf в виде int — 6B 00 00 00 %f интерпретирует память 6B 00 00 00 XX XX XX XX как число с плавающей точкой 5.3e-322 (если XX == 00) и выводится 0.000000
Так как при неверном формате поведение не определено (undefined behavior — UB), то printf("%f", msg) может сделать всё что угодно, хоть ракеты запустить. На моей машине результат printf("%f", msg) зависит от предыдущего кода к примеру:
char c = 'k'; printf("%f
", (float)c); printf("%f
", c);
печатает:
107.000000 107.000000 # XX XX XX XX == 00 C0 5A 40 (остатки от предыдущего вызова)
но:
printf("%f
", 5.3e-322); printf("%f
", c);
печатает:
0.000000 0.000000 # XX XX XX XX == 00 00 00 00 (остатки от предыдущего вызова)
Убедитесь, что для форматов известных во время компиляции (к примеру "%f
") ваш компилятор генерирует предупреждения для неверных типов — UB следует избегать.
printf("%i", (int)msg); \\\ 107 и printf("%i", msg);
в обоих случая msg char передаётся как int (6B 00 00 00) и при одинаковых форматах, результат должен быть одинаковым (6B16 == 10710).
Можно посмотреть содержимое переменных на вашей машине с помощью С кода:
#include
static void print_memory(unsigned char *memory, size_t n) { for (unsigned char *p = memory; p != memory + n; ++p) printf("%02X ", *p); puts(""); }
int main(void) { char c = 'k'; print_memory((unsigned char *)&c, sizeof c); int i = c; print_memory((unsigned char *)&i, sizeof i); float f = c; print_memory((unsigned char *)&f, sizeof f); double d = f; print_memory((unsigned char *)&d, sizeof d); }
Пример:
$ gcc -std=c99 main.c -o main $ ./main 6B 6B 00 00 00 00 00 D6 42 00 00 00 00 00 C0 5A 40
Как double в памяти может выглядеть
Пример из ответа, применённый к 107.0: число двойной точности в IEEE 754 формате представляется как d = ±знак · (1 + мантисса / 252) · 2порядок − 1023
знак, мантисса и порядок упакованы в двоичном представлении как:
00 00 00 00 00 C0 5A 40 // little-endian (8 byte as hex) 40 5A C0 00 00 00 00 00 // big-endian 0100000001011010110000000000000000000000000000000000000000000000 # 64-bit ^ |-самый левый бит знак= 0 (положительный)
^ ^ |---------| Затем 11 бит порядок= 0b10000000101 (==1029) ^ ^ |--------------------------------------------------| Оставшиеся 52 бита манитисса= 0b1010110000000000000000000000000000000000000000000000 = 3025855999639552 = 0xac00000000000
Всё вместе:
d = +(1 + 3025855999639552 / 252) * 2(1029 - 1023) = 64 + 3025855999639552 / 70368744177664 = (4503599627370496 + 3025855999639552) / 70368744177664 = 7529455627010048 / 70368744177664 = 107.0
Это демонстрирует, почему 107.0 может быть представлено в памяти как 00 00 00 00 00 C0 5A 40
Пример урезанной printf функции
#include // va_list, va_arg()
// emulate some printf() functionality using writec() static void print(const char* format, ...) { va_list args; va_start(args, format); int infmt = 0, is_char = 0; union { int i; double f; } arg; char buffer[23] = {0}; // enough for %c, %d, %f
for (const char *p = format; *p; ++p) { if (infmt) { // print arg infmt = 0; switch(*p) { case '%': // "%%": print % literally writec(*p); break; case 'c': // "%c": print char is_char = 1; // fall through case 'd': // "%d": print int case 'i': // "%i": print int arg.i = va_arg(args, int); // load int arg if (is_char) { is_char = 0; writec((unsigned char)arg.i); // format as char, write } else { itoa(arg.i, buffer, sizeof buffer); // format as int for (char *pb = buffer; *pb; ++pb) writec(*pb); // write } break; case 'f': arg.f = va_arg(args, double); // load double arg ftoa(arg.f, buffer, sizeof buffer); // format as floating point for (char *pb = buffer; *pb; ++pb) writec(*pb); // write break; default: arg.i = va_arg(args, int); // load int arg scpy(buffer, ""); for (char *pb = buffer; *pb; ++pb) writec(*pb); // write }; } else if (*p != '%') { // print literally writec(*p); } else { // *p == '%' infmt = 1; } } va_end(args); }
switch используется, чтобы распознать описатели преобразований (%d) в format-строке va_arg() загружает аргументы нужного типа вспомогательные функции itoa() и ftoa() форматируют int и double соответственно writec() пишет один байт в stdout
Это определение print() достаточно для кода:
int main(void) { char msg = 'k'; print("%c %i %i %f %x
", msg, msg, (int)msg, (float)msg, msg); print("%c ", msg); print("%i ", msg); print("%i ", (int)msg); print("%f
", (float)msg); print("%f
", msg); // XXX UB }
Пример:
$ cc -std=c99 print-example.c -o print-example $ ./print-example k 107 107 107.000000 k 107 107 107.000000 107.000000
Чтобы скомпилировать, достаточно вспомогательные функции определить (определения перед print() нужно вставить):
#include // POSIX write()
static void writec(unsigned char c) { write(1, &c, 1); }
static void scpy(char* dest, const char* src) { while (*dest++ = *src++); }
static void ftoa(double d, char* buffer, int n) { if (d == 107) //XXX scpy(buffer, "107.000000"); else scpy(buffer, "XXX"); }
/// format positive int as decimal ascii digits static char* utoa_rec(unsigned i, char* buffer, int *pn) { if (i >= 10) buffer = utoa_rec(i / 10, buffer, pn); if ((*pn)-- > 0) *buffer++ = '0' + (i % 10); return buffer; }
static void itoa(int i, char* buffer, int n) { if (i < 0) { i = -i; //XXX ignore INT_MIN if (n-- > 0) *buffer++ = '-'; // sign } buffer = utoa_rec(i, buffer, &n); if (n > 0) *buffer++ = '\0'; }
Определения функций приведены, чтобы можно было пример запустить, но фактически они просто заглушками являются (не для повторного использования), чтобы только продемонстрировать одну из простейших print(format, ...) реализаций.
Вот пример полной реализации vfprintf() из glibc