Я не знаю как точно сформулировать то, что я хочу спросить, но выглядит это следующим образом:
Как напечатать содержимое переменной в С?
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 <iostream>
#include <vector>
template<class T>
std::ostream& operator << (std::ostream& os, const std::vector<T>& v) {
for (auto&& x : v)
os << x << ' ';
return os;
}
int main()
{
std::vector<int> 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)
, если 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
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
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 <stdio.h>
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
Пример из ответа, применённый к 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
.
#include <stdarg.h> // 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, "<unknown, load as int>");
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 <unknown, load as int>
k 107 107 107.000000
107.000000
Чтобы скомпилировать, достаточно вспомогательные функции определить (определения перед print()
нужно вставить):
#include <unistd.h> // 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.
почему в С нет аналога cout
Функциональность cout
в языке С++ критически завязана на механизм перегрузки функций на уровне библиотеки, т.е. фактически на существовании доступного библиотеке (и пользователю) механизма перегрузки функций, а также сопутствующего механизма перегрузки операторов. Правильная версия функции вывода выбирается механизмом перегрузки на основе анализа типа указанного вами аргумента.
В языке С нет механизмов перегрузки функций и перегрузки операторов на уровне пользователя или библиотеки. Поэтому нет и таких внешне "типонезависимых" операций ввода-вывода.
В версии С11 стандарта языка С появился механизм generic-выражений, который может использоваться для эмуляции пользовательской/библиотечной перегрузки функций. Этот механизм, например, используется (может использоваться) в стандартном заголовочном файле <tgmath.h>
для реализации "перегруженных" математических функций-макросов.
Но реализаций "типонезависимых" функций ввода-вывода в стандартной библиотеке не делалось. Если есть желание - можете воспользоваться этим новым механизмом и попробовать реализовать их самостоятельно.
Мне часто пишут что тип в printf нужно приводить. А зачем? Разве printf не приведет данные самостоятельно к тому модификатору, который мы указываем?
printf
- это так называемая variadic функция. Все аргументы этой функции, кроме первого, являются variadic аргументами. Они соответствуют ...
в объявлении списка параметров функции
int printf( const char* format, ... );
Такие аргументы передаются через особый механизм предачи параметров. Его особенностью является то, что сама функция printf
абсолютно ничего не знает о типах фактически передаваемых аргументов и по этой причине ничего самостоятельно привести к правильному типу не может.
С чисто практической точки зрения вы можете считать, что все variadic аргументы записываются в непрерывный бинарный поток. Внутри себя функция printf
будет читать бинарные данные из этого потока при помощи механизма va_list/va_start/va_arg
. Что именно записано в этот поток функция printf
сама по себе знать не может. Поэтому разбирать этот бинарный поток на части она будет в соответствии с тем форматом, который вы сами ей снаружи передали. И если вы ей "наврете" в этом формате, то она сама ничего не подозревая, будет разбирать этот бинарный поток неправильно. По этой причине все данные, которые вы кладете в этот бинарный поток, должны в точности соответствовать тем спецификаторам формата, которые вы указали в форматной строке.
Реализации не обязаны реализовать передачу variadic аргументов именно таким образом, но особенности передачи variadic аргументов эта модель иллюстрирует достаточно точно.
1) Для работы cout
необходима перегрузка оператора <<
, причём для каждого типа она своя, потому и не требуется модификаторов. В Си операторы не перегружаются.
2) Мне кажется, слово "приведёт" не совсем точно, я бы сказал "интерпретирует": printf
выведет значение переменной так, как если бы она была того типа, что указан в строке формата. Переменная при этом не меняет значения. Сама printf
не имеет возможности определить соответствие аргументов строке формата на этапе выполнения (а компилятор Си не учит программиста, как жить и где разложить себе грабли). Поэтому возможны всякие сюрпризы, если случайно допустить ошибку в строке формата и спутать типы или количество аргументов.
Айфон мало держит заряд, разбираемся с проблемой вместе с AppLab
Перевод документов на английский язык: Важность и ключевые аспекты
Какие существуют виды рекламных бордов и как выбрать подходящий?
Практикуюсь в написании сканнера ip и mac адресов в локальной сети на c++Подскажите пожалуйста с чего мне начать?
Используется MySQL - 57 - х64