Глава 33 Вещественные числа



Почему так несовершенны все людские поделки? Даже компьютер и язык Паскаль! Эх, был бы числовой тип, пригодный на все случаи жизни, но…

Пять целочисленных типов не покрывают всех потребностей в вычислениях. Во-первых, диапазон их значений не так уж велик. Скажем, население Земли – около шести миллиардов – не поместится в переменной типа LongInt. А что сказать о комарином «населении»? Это, во-первых. А во-вторых, такими числами нельзя выразить дробные значения.

Выручают вещественные числа. Откуда такое чудное название? – Этими числами можно выразить количество сыпучих и жидких веществ. Если так, то целые числа следовало назвать штучными. У вещественных чисел есть и другое название – действительные. Которое из двух предпочтете? – дело вкуса.

Изображение вещественных чисел

Вещественные числа отнесены к простым типам, но устроены сложнее целых. Рассмотрим способы изображения таких чисел. Представить их можно двояко: либо в привычной для нас форме с фиксированной точкой (в Паскале точку используют вместо запятой), либо в так называемом научном (логарифмическом) формате. Увидев где-либо такое число, не пугайтесь, – здесь применен научный формат изображения вещественного числа.


    3.33333343267441E-0002


Мы видим десятичную дробь, на хвосте которой болтается буква «E» и число -0002. Вот разгадка этой записи: дробь, что расположена до буквы «E», называется мантиссой, а число за этой буквой — порядком. Порядок показывает, на сколько позиций надо передвинуть десятичную точку в мантиссе для получения числа в привычном виде. Здесь порядок отрицательный, поэтому точка сдвигается на две позиции влево, а значит перед нами число 0.0333333343267441. Для положительного порядка точку двигают вправо, стало быть, число


    3.33333343267441E+0003


в форме с фиксированной точкой запишется так: 3333.33343267441.

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


 9.1093829140E-0031  – масса электрона, кг

 1.9889200000E+0030  – масса солнца, кг


Вывод вещественных чисел

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


{ Программа для исследования форматов вывода вещественных чисел }

begin

    Writeln( 10/3);      { без спецификаторов }

    Writeln( 10/3 : 12);  { указывается только ширина поля }

    Writeln( 10/3 : 15:0); { только целая часть }

    Writeln( 10/3 : 15:2); { два знака после точки }

    Writeln( 10/3 : 15:3); { три знака после точки }

end.


Результат её работы таков.


3.33333333333333E+0000

3.333E+0000

    3

    3.33

    3.333


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

Типы вещественных чисел

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

Табл. 3 – Вещественные типы

Тип данных Точность Диапазон возможных значений Количество значащих цифр (точность) Размер в байтах
От До
Real Стандартная 2.9 x 10–39 1.7 x 1038 11-12 6
Single Одинарная 1.5 x 10–45 3.4 x 1038 7-8 4
Double Двойная 5.0 x 10–324 1.7 x 10308 15-16 8
Extended Повышенная 3.6 x 10–4951 1.1 x 104932 19-20 10

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

Теперь исследуем точность представления чисел разными типами данных.


{ Программа для исследования точности вещественных типов }

var F0 : Real; F1 : single; F2 : double; F3 : extended;

begin

    F0:= 1/3; F1:= 1/3; F2:= 1/3; F3:= 1/3;

    Writeln('Single = ', F1:23:18);

    Writeln('Real = ', F0:23:18);

    Writeln('Double = ', F2:23:18);

    Writeln('Extended= ', F3:23:18);

end.


Десятичное представление дроби 1/3 нам известно, – это бесконечная последовательность троек, а результат вычислений по этой программе перед вами (для Borland Pascal, в других компиляторах результаты могут немного отличаться):


Single = 0.333333343267440796

Real = 0.333333333333484916

Double = 0.333333333333333315

Extended= 0.333333333333333333


Как и следовало ожидать, тип Extended дает самую высокую точность, – после десятичной точки следуют одни тройки. Другие типы менее точны. Если так, зачем они нужны? Обратимся к истории.

Первые версии Паскаля ещё не застали персональных компьютеров. Тогда в языке существовал только один тип вещественных чисел – Real. Его считают стандартным типом Паскаля, и для обработки таких чисел годится любой процессор (но вычисления будут медленными).

Но вот появились компьютеры с математическими сопроцессорами, многократно ускоряющими счет. Эти сопроцессоры оперируют с форматами, отличными от Real. Для совместимости с новой техникой в язык были введены ещё три типа чисел, указанные в табл. 3. Тип Extended даёт наивысшую точность и самый широкий диапазон представления чисел. И это понятно, ведь его размер больше, чем у других, и составляет 10 байтов. Но почему он выигрывает и в скорости? А потому, что для сопроцессора тип Extended – родной, применяйте его для вычислений. А что же Single и Double? Поскольку они занимают меньше места в памяти, то лучше подходят для хранения больших объёмов данных.

Сравнение вещественных чисел

Вещественные числа часто сравнивают между собой. Однако проверка их на точное равенство таит неприятный сюрприз, – такие сравнения ненадежны! Все потому, что вещественные числа – приближенные; они могут быть очень близки, и все же чуточку не совпадать друг с другом. Точное совпадение – это удача, а не закономерность. Правильней сравнивать числа с некоторой точностью, как в показанном ниже примере.


var A, B : Extended;

...

if A = B then …     { это ненадежное сравнение }

if Abs(A-B) < 0.001 then … { надежное сравнение с точностью 0.001}


Во втором сравнении переменные A и B считаются одинаковыми, если отличаются менее чем на одну тысячную. Как видите, знаком равенства тут и не пахнет. К слову, функция Abs возвращает абсолютное значение аргумента, – ведь здесь надо получить положительное значение разности. Выражение Abs(A-B) в математике пишется так: |A-B|.

Типы данных пользователя

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

Предположим, ваша программа содержит много переменных типа Integer. Но, по ходу работы над проектом, вы решили заменить этот тип чисел на какой-то иной, например на Longint или даже Extended. Такую замену можно сделать редактором текста, тщательно порывшись в программе и найдя все места, где объявлены переменные. Но можно сделать лучше – объявить собственный тип данных.

Для объявления пользовательских типов в Паскале служит секция описания типов, которая начинается с ключевого слова TYPE – «тип». Внутри секции можно объявить один или несколько типов данных пользователя. Так, например, вы можете объявить свой тип на базе встроенного типа Integer.


Type TValue = Integer;


Здесь объявлен тип данных по имени TValue (Value – «значение»), он равнозначен типу Integer. Как видите, объявление типа схоже с объявлением константы. Только справа от знака равенства следует не значение, а описание типа.

Имя пользовательского типа придумывают по общим правилам для имен. В свое время мы учредили префиксы для констант и аргументов процедур, – префиксы делают программу понятней. Для констант мы договорились применять префикс «C», для аргументов процедур и функций – префикс «a». Для типов возьмем префикс «T» (от слова Type). Повторяю: префиксы – это всего лишь добровольное соглашение программистов, а не требование языка.

Теперь, когда тип TValue объявлен, его можно использовать для объявления переменных, например:


Type TValue = Integer;

Var A, B, C : TValue;

    ...

    Readln(A, B);

    C:= A+B;


Если со временем потребуется изменить типы переменных A, B и C на тип Longint, то мы исправим лишь объявление пользовательского типа.


Type TValue = Longint;


И после компиляции все переменные типа TValue примут тип Longint.

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

Совместимость и преобразование типов

Поздравляю! Теперь вам знакомы все простые типы данных – солдаты нашей армии. Сражаясь в едином строю, они будут передавать при необходимости данные друг другу. Такая передача данных должна подчиняться определенным правилам. Они просты, но заставляют программиста всякий раз задуматься: то ли я делаю? А это придает программам надежность.

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

Целое и целое

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


Var B: Byte; I: Integer; W: Word; L: Longint;

    ...

    { Эти операторы не вызовут нарушения границ диапазонов }

    I:= B;     W:=B;     L:= W;     L:=I;

    { Эти операторы могут повлечь нарушения диапазонов }

    B:=I;     I:=W;     W:=L;


Когда число N не помещается в переменной, то в неё попадает лишь младшая часть числа N (т.е. остаток от деления N mod 256 или N mod 65536).

Целое и символ

Взаимно превращать эти типы данных мы научились при шифровании символа; напомню об этом. Функция Ord возвращает числовой код любого порядкового типа, в том числе и символа. А функция Char делает обратный фокус, превращая число в символ, вот примеры:


    Writeln ( Ord(’D’) );     { 68 }

    Writeln ( Char(68) );     { D }


Как видите, число в символ преобразуется через имя типа Char. Это общий прием, волшебная палочка для обращений типов данных. Например, булевых.

Целое и булево

Превратить булево в целое можно все той же функцией Ord.


    Writeln ( Ord(False) );     { 0 }

    Writeln ( Ord(True) );     { 1 }


А для обратного превращения воспользоваться именем типа Boolean.


    Writeln ( Boolean(0) );     { False }

    Writeln ( Boolean(1) );     { True }


Целое и перечисление

Вернемся к перечислению месяцев, вымышленному нами в предыдущей главе, где переменная была объявлена так:


var m : (Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec);


Превратить значение такой переменной в число можно функцией Ord. А наоборот? Пока нам этого не удавалось. Но после объявления пользовательского типа данных задача решается очень просто.


Type TMonth = (Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec);

Var m : TMonth;

    ...

    m:= 3;     { это ошибка }

    m:= TMonth(3); { это равнозначно m:= Apr (счет идет от нуля) }


Здесь объявлен пользовательский перечислимый тип TMonth (Month – «месяц»), далее вы вольны применять его и для объявления переменных, и для преобразования типов. Вот где проявляется сила пользовательского типа!

Вещественное и вещественное

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

Целое и вещественное

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


    Writeln ( Trunc(3.75) ); { 3 }

    Writeln ( Round(3.75) ); { 4 }

    Writeln ( Round(3.25) ); { 3 }


Напоследок рассмотрите рис. 76, где показана общая картина совместимости и преобразования простых типов данных.

Строгий контроль типов в Паскале задуман для пущей надежности программам. В старых языках программирования (Си, Фортран) такого контроля либо не было, либо он был очень слаб. Программист перебрасывал данные как ему вздумается, не слишком заботясь о последствиях. Подобные вольности порождали массу ошибок. Теперь же Паскаль предлагает программисту следующее: пожалуйста, переноси данные, куда угодно, но при этом укажи явным образом, что ты делаешь.

Размеры переменных и типов данных

Иногда требуется знать не только содержимое переменных, но и объём занимаемой ими памяти. Разумеется, вы можете узнать это из таблиц, приведенных в справке или руководстве по языку. Размеры сложных типов данных тоже поддаются расчету. И все же лучший способ определить размер переменной некоторого типа – вызов псевдофункции SizeOf. В качестве параметра она принимает либо имя переменной, либо имя типа, а возвращает целое число – объём занимаемой памяти в байтах. Вот примеры.


Type TMonth = (Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec);

Var m : TMonth;

    ...

    Writeln ( SizeOf(m) );         { 1 }

    Writeln ( SizeOf(TMonth) );     { 1 }

    Writeln ( SizeOf(Integer) );     { 2 }

    Writeln ( SizeOf(Extended) );     { 10 }



Рис.76 – Совместимость и преобразования типов

Я обозвал SizeOf псевдофункцией за то, что никаких вычислений она не делает, – результат вычисляется при компиляции программы. Ведь компилятор сам «знает» объём памяти, занимаемой любым типом данных, и подставляет в программу уже готовое число.

Псевдофункция SizeOf удобна, и вдобавок улучшает переносимость программ. Например, в разных режимах компиляции Free Pascal и в разных компиляторах размер типа Integer может отличаться (2 или 4 байта). Применяя функцию SizeOf, вам не придется задумываться об этом и менять вручную одни числа на другие, – компилятор сделает это за вас.

Итоги

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

• Тип Extended предпочтительней использовать для вычислений, тип Single – для хранения больших объёмов данных в памяти и на диске.

• Вещественные числа печатаются либо в форме с плавающей точкой, либо в форме с фиксированной точкой.

• Вещественные числа, в отличие от целых, – приближенные. Сравнивать их между собой можно лишь с некоторой точностью.

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

• Целым переменным нельзя присвоить вещественные значения непосредственно, для этого используют либо функцию отсечения дробной части Trunc, либо функцию округления Round.

• Порядковые типы данных допускают взаимное преобразование посредством псевдофункций, имена которых совпадают с именами типов данных.

• Пользовательские типы данных объявляют внутри секции TYPE, такие типы данных делают программу гибче и надежней.

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

А слабо?

А) Напишите две функции, округляющие вещественное число:

• до большего значения (например: 3.1 –> 4; 3.9 –> 4);

• до меньшего значения (например: 3.1 –> 3; 3.9 –> 3).

Б) Ваша процедура принимает строковую переменную, вычисляет среднее арифметическое кодов её символов и печатает его с двумя цифрами после точки.

В) Напечатайте с тремя знаками после точки 20 случайных вещественных чисел в диапазоне от 0 до 10. Подсказка: для формирования дробных чисел можно делить случайное число на другое число, например, Random(10000)/1000.

Г) Напечатайте с тремя знаками после точки 20 случайных чисел в диапазоне от 0 до 10 так, чтобы числа следовали по возрастанию. Подсказка: сравнивайте очередное число с предыдущим.

Д) Программа для подсчета стоимости покупок. Для каждой покупки пользователь вводит два действительных числа: вес покупки и цену за 1 кг в рублях. Признак завершения ввода данных – нулевой вес. Программа должна напечатать общую стоимость с точностью до копейки (два знака после точки) с округлением в большую сторону. Проверьте результат на калькуляторе.

Е) Квадратный корень. Квадрат – это равносторонний прямоугольник, его площадь вычисляется по формуле S=D•D, где D – сторона квадрата. А когда площадь S известна, и надо определить сторону D? Тогда из S извлекают квадратный корень (обозначается символом V). Так, если S=9, то D=V9=3.

Для извлечения корня в Паскале есть функция SQRT. Напишите собственную функцию MySQRT, прибегнув к методу последовательных приближений. В грубом, нулевом приближении примем D0=1. Последующее, более точные значения D будем вычислять по формуле

Di+1 = (Di + S/Di)/2

Так, при S=9 получим D1=(1+9/1)/2= 5, D2=(5+9/5)/2= 3.4 и так далее, пока абсолютная разность между двумя последовательными значениями D станет пренебрежимо мала. Функция MySQRT должна принять число и вычислить его корень с точностью 0.0001. Внутри функции напечатайте промежуточные значения D. Подсказка: для Di и Di+1 вам потребуются лишь две локальные переменные.

Ж) В тесто кладут четырех главных ингредиента: муку, сахар, яичный порошок и молоко. Все это смешивается в пропорции, заданной рецептом. Например, рецепт 100:5:7:500 означает, что на 100 граммов муки кладут 5 граммов сахара, 7 граммов яичного порошка и 500 граммов молока. У пекаря есть некоторое количество всех ингредиентов, и он хочет замесить из них максимально возможное количество теста, соблюдая рецепт. Ваша программа должна ввести:

• Рецепт – это 4 целых числа.

• Исходное количество ингредиентов – это 4 действительных числа.

Программа должна напечатать:

• Общее количество полученного теста с точностью два знака после точки.

• Остатки ингредиентов – 4 числа с точностью два знака после точки.

Загрузка...