Третья строка демонстрирует, что происходит при попытке преобразования в символ значения, которое больше 255. В нашей системе тип short int занимает 2 байта, а тип char — 1 байт. Когда функция printf() выводит 336 с использованием спецификатора %с, она просматривает только один байт из двух, задействованных для хранения 336. Такое усечение (рис. 4.8) равнозначно делению целого числа на 256 с сохранением только остатка. В этом случае остаток равен 80, что представляет собой ASCII-значение символа Р. Формально можно сказать, что число интерпретируется как результат деления по модулю 256, что означает использование остатка от деления числа на 256.

Рис. 4.8. Интерпретация числа 336 как символа
В заключение мы попытались вывести в своей системе целое число (65618), превышающее максимально допустимое значение типа short int (32767). И снова компьютер применил деление по модулю. Число 65618 в силу своего размера хранится в нашей системе как 4-байтовое значение int. Когда оно выводится с применением спецификатора %hd, функция printf() использует только последние 2 байга, которые равносильны остатку от деления на 65536. В этом случае остаток равен 82. Остаток, находящийся между 32767 и 65536, с учетом способа хранения отрицательных чисел выводился бы как отрицательное число. В системах с другими размерами целых чисел общее поведение было бы таким же, но с другими числовыми значениями.
140 Глава 4
Когда вы начнете смешивать целочисленные типы и типы с плавающей запятой, результаты станут еще более причудливыми. Для примера рассмотрим программу, приведенную в листинге 4.12.
Листинг 4.12. Программа floatcnv.c

В нашей системе код из листинга 4.12 сгенерировал следующий вывод:
3.0е+00 3.0е+00 3.1е+46 1.7е+266
2000000000 1234567890
0 1074266112 0 1074266112
Первая строка вывода показывает, что применение спецификатора %е не вызывает преобразование целого числа в число с плавающей запятой. Давайте посмотрим, что происходит при попытке вывода переменной n3 (типа long) с использованием спецификатора %е. Во-первых, спецификатор %е заставляет функцию printf() ожидать значение типа double, которое в нашей системе является 8-байтовым. Когда функция printf() исследует переменную n3, представленную в нашей системе 4-байтовым значением, она просматривает также смежные 4 байта памяти. Таким образом, функция анализирует 8-байтовый блок, в котором содержится действительное значение n3. Во-вторых, она интерпретирует биты этого блока как число с плавающей запятой. Например, некоторые биты, будут трактоваться в качестве показателя степени. Поэтому, даже если бы значение n3 содержало правильное количество битов, для спецификаторов %е и %ld они бы интерпретировались по-разному. Конечный результат оказывается бессмысленным.
Первая строка вывода также иллюстрирует то, что упоминалось ранее — при передаче в виде аргумента функции printf() значение float преобразуется в тип double. В данной системе тип float занимает 4 байта, но переменная nl была расширена до 8 байтов, чтобы функция printf() смогла корректно отобразить ее значение.
Вторая строка вывода показывает, что функция printf() может правильно выводить значения n3 и n4, если указан корректный спецификатор.
Третья строка вывода демонстрирует, что даже правильный спецификатор может приводить к ложным результатам, если оператор printf() содержит несоответствия где-то в другом месте. Как и можно было ожидать, попытка вывода значения с плавающей запятой с применением спецификатора %ld оказывается неудачной, однако в данном случае неудачу терпит и попытка вывода значения типа long с использованием спецификатора % &ld! Проблема кроется в способе передачи информации функции. Точные детали отказа при выводе зависят от реализации, но во врезке “Передача аргументов” обсуждается поведение в типичной системе.
Символьные строки и форматированный ввод-вывод 141
Передача аргументов
Механизм передачи аргументов зависит от реализации. Вот как передача аргументов происходит в нашей системе. Вызов функции выглядит следующим образом:
printf ("%ld %ld %ld %ld\n", nl, n2, n3, n4);
Этот вызов сообщает компьютеру о том, что ему передаются значения переменных nl, n2, n3 и n4. Ниже описан один из распространенных способов обработки этой ситуации. Программа помещает значения в область памяти, которая называется стеком Когда компьютер помещает эти значения в стек, он руководствуется типами переменных, а не спецификаторами преобразования. Следовательно, для nl он выделяет в стеке 8 байтов (float преобразуется в double). Подобным же образом для переменной n2 отводится еще 8 байтов, после чего по 4 байта выделяется для переменных n3 и n4. Затем управление передается функции printf(). Эта функция читает значения из стека, но делает это согласно спецификаторам преобразования. Спецификатор %ld указывает, что функция printf() должна прочитать 4 байта, поэтому она считывает первые 4 байта в стеке в качестве своего первого значения. Прочитанные 4 байта представляют собой первую половину nl, которая интерпретируется как целочисленное значение long. Следующий спецификатор %ld обеспечивает чтение еще 4 байтов; это вторая половина nl, и она интерпретируется как второе целочисленное значение long (рис. 4.9). Аналогично третий и четвертый спецификаторы %ld приводят к чтению первой и второй половины n2 с последующей их интерпретацией в качестве еще двух целочисленных значений long, так что, хотя для переменных n3 и n4 указаны корректные спецификаторы, функция printf() читает не те байты, которые нужны.
Читать дальше