Int main(void)
{
float a, b; b = 2.0e20 + 1.0;
a = b - 2 . 0e20 ;
printf("%f \n", a);
return 0;
}
Вывод выглядит следующим образом:
0.000000 <- старая версия компилятора дсс в операционной системе Linux
-13584010575872.000000 <-Turbo С 1.5
400817546854 4.000000 <-XCode 4.5, Visual Studio 2012, текущая версия компилятора gсс.
Причина получения таких странных результатов состоит в том, что компьютер не следит за тем, чтобы под числа с плавающей запятой было отведено столько десятичных позиций, сколько нужно для правильного выполнения операции. Число 2.0е20 представлено цифрой 2, за которой следует 20 нулей, и за счет прибавления 1 вы пытаетесь изменить 21-ю цифру. Чтобы эта операция выполнилась корректно, программа должна иметь возможность хранить число, состоящее из 21 цифры. Число типа float — это обычно шесть или семь цифр, масштабированных при помощи показателя степени до большего или меньшего числа, так что такая попытка сложения обречена на неудачу. С другой стороны, если вместо 2.0е20 вы укажете 2.0е4, то получите правильный ответ, поскольку вы пытаетесь изменить пятую цифру, а числа типа float обладают достаточной для этой операции точностью.
Представление значений с плавающей запятой
В предыдущей программе было видно, что вывод одной и той же программы отличался в зависимости от используемой компьютерной системы. Причина этого отличия в том, что существует много разных способов реализации представления чисел с плавающей запятой в рамках описанных общих подходов. Для обеспечения большего единообразия в Институте инженеров по электротехнике и радиоэлектронике (IEEE) разработан стандарт для представления чисел с плавающей запятой и вычислений с плавающей запятой, который теперь применяется во многих аппаратных блоках обработки чисел с плавающей запятой.
Какие размеры типов используются в вашей системе? Чтобы выяснить это, попробуйте выполнить программу, показанную в листинге 3.8.
Листинг 3.8. Программа typesize.c
/* typesize.c -- prints out type sizes */
#include <stdio.h>
Int main(void)
{
/* c99 provides a %zd specifier for sizes */
printf("Type int has a size of %zd bytes.\n", sizeof(int));
printf("Type char has a size of %zd bytes.\n", sizeof(char));
printf("Type long has a size of %zd bytes.\n", sizeof(long));
printf("Type long long has a size of %zd bytes.\n",
sizeof(long long));
printf("Type double has a size of %zd bytes.\n",
sizeof(double));
printf("Type long double has a size of %zd bytes.\n",
sizeof(long double));
return 0;
}
В языке С имеется встроенная операция sizeof, которая возвращает размер типа в байтах. Для такого применения sizeof в стандартах С99 и Cl 1 предоставляется спецификатор %zd. Компиляторы, несовместимые с этими стандартами, могут потребовать вместо него спецификатор %и или %1и. Ниже показан пример вывода программы
typesize.c:
Тип int имеет размер 4 байт(ов).
Тип char имеет размер 1 байт(ов) .
Тип long имеет размер 8 байт(ов) .
Тип long long имеет размер 8 байт(ов) .
Тип double имеет размер 8 байт(ов).
Тип long double имеет размер 16 байт(ов).
Эта программа находит размеры только шести типов, но вы легко можете ее модифицировать, чтобы она определяла размер любого другого интересующего типа.
Использование типов данных
При разработке программы обращайте внимание на то, какие переменные необходимы, и какие типы они должны иметь. Скорее всего, для чисел вы выберете int или, возможно, float, а для символов — тип char. Объявляйте переменные в начале функции, в которой они используются. Выбирайте для переменных имена, отражающие их предназначение. При инициализации обеспечьте соответствие типов констант и типов переменных. Например:
int apples =3; /* правильно */
int oranges = 3.0; /* плохая форма */
В отношении несовпадения типов язык С более либерален, чем, скажем, Pascal. Компиляторы С разрешают инициализацию, сделанную во втором операторе, но могут выдать сообщение, особенно если установлен высокий уровень предупреждений. Поэтому лучше не вырабатывать в себе плохие привычки.
Когда вы инициализируете переменную одного числового типа значением другого числового типа, компилятор С преобразует такое значение в тип переменной. Это означает возможность потери данных. Например, взгляните на следующие примеры инициализации:
int cost = 12.99; /* инициализация переменной типа int значением double */
float pi = 3.1415926536; /* инициализация переменной типа float значением double */
В первом объявлении переменной cost присваивается значение 12; при преобразовании значений с плавающей запятой в целочисленные компилятор С вместо округления просто отбрасывает дробную часть числа (выполняет усечение). Во втором объявлении происходит некоторая потеря точности, поскольку для типа float точность гарантируется только в пределах шести цифр. Когда вы делаете такую инициализацию, компиляторы могут (но не обязаны) выдавать предупреждающее сообщение.
Управляющие последовательности
Давайте рассмотрим еще один пример, связанный с выводом, в котором используются специальные управляющие последовательности для символов языка С. В частности, программа, представленная в листинге 3.10, демонстрирует работу символов возврата на одну позицию влево (\Ь), табуляции (\t) и возврата каретки (\г). Их концепции существуют со времен, когда компьютеры применяли для вывода телетайпы, и они не всегда успешно транслируются в современных графических интерфейсах. Например, код в листинге 3.10 не работает описанным здесь образом в некоторых реализациях для компьютеров Macintosh.
Листинг 3.10. Программа escape.с
/* escape.c -- uses escape characters */
#include <stdio.h>
