ProDelphi - Тип данных. Особенности вещественных чисел в Delphi 6 delphi порядковые типы данных


Данные в компьютере можно рассматривать как ячейки памяти, имеющие свои имена (идентификаторы). Все данные в программе на языке Delphi должны быть описаны до их первого использования. И компилятор следит, чтобы в программе они использовались в соответствии с этим описанием, что позволяет избежать ошибок.

Любая величина в Delphi может быть постоянной или переменной. Её имя (идентификатор) может состоять из комбинации латинских букв, цифр и знака подчёркивания, и начинаться не с цифры. При этом регистр символов значения не имеет.

Место описания данных в программе - вне логических блоков begin / end . В модуле перед ключевым словом implementation есть блок описания:

var
Form1: TForm1;

Именно здесь, начиная со следующей строки, удобно объявлять глобальные переменные и константы. Как видим, одна (Form1) уже есть!

Команда объявления переменных в языке Delphi :

var имя_переменной : тип_переменной ;

Слово var - ключевое. Именем может быть любой идентификатор, если он не был описан ранее и не является одним из ключевых или зарезервированных слов языка Delphi . Если нужно описать несколько переменных одного типа, то их перечисляют, отделяя запятой:

var A, B, C : Integer;

Если несколько описаний следуют друг за другом, то ключевое слово var повторно можно не указывать:

var A, B : Integer;
C, D : String;

Постоянную величину иначе называют константой . Конечно, в программе можно использовать числа и строки непосредственно: 3.1415 или "Это значение числа пи" , но иногда удобнее присвоить их идентификатору. Описание констант аналогично описанию переменных, но используется ключевое слово const , за именем идентификатора следует тип, затем знак равенства и его значение. Причём тип константы допускается не указывать:

const pi= 3.1415 ;
ZnakPi : String = "Это значение числа пи" ;

К слову, константа Pi встроенная в Delphi, то есть для того чтобы использовать в Delphi число 3,1415... в расчётах, нужно просто присвоить встроенную константу Pi переменной типа Real или просто использовать непосредственно в выражениях.

Теперь пришло время узнать о типах данных, используемых в Delphi . Прежде всего это строки и числа.

Строкой называется последовательность символов, заключённая в одиночные кавычки:
"это текстовая строка" Если текст должен содержать сам символ кавычки, то его надо повторить дважды:
"это "" - символ одиночной кавычки" Строка может быть и пустой, не содержащей символов. Тогда она состоит из двух идущих друг за другом без пробела кавычек. Естественно, строка может состоять и только из одних пробелов.
Самый популярный строковый тип - String . Строка типа String может содержать переменное количество символов объёмом до 2 Гбайт. Если нужно ограничить размер строки фиксированным значением, то после ключевого слова String в квадратных скобках указывается число, определяющее количество символов в строке: String . Более полно работа со строками Delphi описывается далее.
Одиночный символ имеет тип Char и записывается в виде знака в одиночных кавычках: "a" . Есть символы, которые на экране отобразить невозможно, например, символ конца строки (равен #13), символ переноса строки (равен #10). Такие символы записываются в виде их числового кода (в кодировке ANSI ), перед которым стоит знак # . Например, #0 .
Наконец, существуют так называемые нуль-терминированные строки. Отсчёт символов в таких строках начинается с нуля, а заканчивается символом с кодом 0 (#0 ). Такие строки имеют тип PChar .

Числа бывают целые и дробные .
В следующей таблице перечислены стандартные типы целых чисел и соответствующие им дипазоны допустимых значений.

Дробные числа имеют дробную часть, отделяемую десятичной точкой. Допускается использование символа e (или E ), за которым следует число, указывающее, что левую часть нужно умножить на 10 в соответствующей степени: 5e25 - пять умножить на десять в двадцать пятой степени.
Ниже приведены стандартные типы дробных чисел и соответствующие им диапазоны допустимых значений. Для большинства типов указан диапазон положительных значений, однако допустимым является аналогичный диапазон отрицательных значений, а также число 0 .

Следующим типом данных является логический Boolean , состоящий всего из двух значений: True (Истина) и False (Ложь). При этом True > False .

Теперь, используя компоненты, их свойства и события, вводя собственные переменные, можно конструировать программы, содержащие вычисления. Осталось узнать, как вычисленное значение вывести на экран.
Про консольные программы я здесь не говорю ! А в нормальных оконных Windows-приложениях это значение нужно поместить в какой-нибудь компонент, имеющий свойства Text или Caption . Это, например, такие компоненты как Label и Edit , да и сама Форма имеет свойство Caption , куда тоже можно выводить информацию. Однако, в Delphi информацию перед выводом, как правило, необходимо преобразовывать. Так как присвоение возможно только между переменными одного типа, то такая программа (не пытайтесь её исполнять):

var A, B, C: Integer ;
begin
A:= 5 ;
B:= 10 ;
C:= A+B ;
Label1.Caption:= C ;
end ;

Вызовет ошибку, так как свойство Caption имеет текстовый тип String , а использованные переменные - цифровой тип Integer . Значит, нужно преобразовать значение переменной C в текстовый тип. Для этого есть встроенная функция IntToStr . Строка в нашей "программе", вызывавшая ошибку, должна выглядеть так:

Label1.Caption:= IntToStr(C) ;

Такая программа, кроме показа числа 15 , ни на что не способна. Мы должны научиться вводить в программу другие числа. Используем компоненты Edit . Введённые числа будут содержаться в свойстве Text этих компонентов. Расположим на форме два компонента Edit , один компонент Label и кнопку Button , по нажатию на которую и будем проводить вычисления. В компоненты Edit1 и Edit2 будем вводить числа для суммирования. Чтобы переместиться в редактор кода, щёлкнем дважды по нашей кнопке Button1. Мы попадём прямо в сформированную для нас средой Delphi заготовку обработчика нажатия на кнопку, непосредственно между операторами begin и end . Напишем такой простой код:

procedure TForm1.Button1Click(Sender: TObject);
var A, B, C: Integer;//Не забудьте описание переменных
begin
//Начало кода:
A:= Edit1.Text;
B:= Edit2.Text;
C:= A+B;
Label1.Caption:= IntToStr(C);
//Конец кода
end ;

При попытке исполнить этот код Delphi покажет ошибки по аналогичной причине - переменные A и B имеют цифровой тип Integer , а свойство Text - текстовый тип String . Исправить ошибки поможет встроенная функция StrToInt , выполняющая обратное преобразование - текст в целое число. Операторы присвоения переменным A и B должны выглядеть так:

A:= StrToInt(Edit1.Text);
B:= StrToInt(Edit2.Text);

В данном случае переменные A, B, C использовались для наглядности. Можно обойтись одной строчкой:

Label1.Caption:=IntToStr(StrToInt(Edit1.Text)+StrToInt(Edit2.Text));

Аналогично, имеются функции и для преобразования в строку и обратно действительных чисел c плавающей (Float ing англ.) запятой, имеющих тип Real . Для преобразования в строку - FloatToStr , обратно - StrToFloat .
Часто результаты вычислений, имеющие тип Delphi Real , имеют после запятой длинный "хвост" цифр. При выводе такой переменной в текстовом виде необходимо ограничить количество цифр после запятой. Как это можно сделать, описывается также в Уроке Delphi


Неочевидные особенности вещественных чисел Взяться за эту статью меня побудили появляющиеся время от времени вопросы на Круглом Столе, вызванные непониманием внутреннего представления вещественных чисел. Когда-то описание внутреннего представления таких чисел было неотъемлемой частью любой сколь-нибудь серьёзной книги по программированию, но сейчас у авторов появились более интересные предметы для обсуждения: COM/DCOM, ActiveX, OLE и многое другое. На вещественные числа просто не хватает места. И люди, начавшие программирование с Delphi и не имеющие опыта работы в более старых средах, часто оказываются совершенно беспомощными перед непонятным поведением программы, содержащей дробные вычисления. Надеюсь, моя статья прольёт свет на эти вопросы и сделает поведение дробей более предсказуемым.
Двоичные дроби
Для начала – немного математики. В школе мы проходим два вида дробей – простые и десятичные. Десятичные дроби, по сути дела, представляют собой разложение числа по степеням десяти. Так, запись 13.6704 означает число, равное 1*10 1 +3*10 0 +6*10 -1 +7*10 -2 +0*10 -3 +4*10 -4 . Но внутреннее представление всех чисел в компьютере, в том числе и вещественных – не десятичное, а двоичное. Поэтому используются двоичные дроби. Они во многом похожи на десятичные, но основание степени у них двойка. Так, число 101.1101=1*2 2 +0*2 1 +1*2 0 +1*2 -1 +1*2 -2 +0*2 -3 +1*2 -4 . То есть в десятичном представлении это число равно 5.8125, в чём нетрудно убедиться с помощью любого калькулятора. Теперь вспомним научный формат записи десятичного числа. Первым в этой записи идёт знак числа – плюс или минус. Дальше идёт так называемая мантисса – число от 1 до 10. Затем идёт экспонента – степень десяти, на которую надо умножить мантиссу, чтобы получить нужное число. Итак, уже упоминавшееся число 13.6704 запишется в этом формате как 1.36704*10 1 (или 1.36704E1 по принятым в компьютере правилам). Если записываемое число меньше единицы, экспонента будет отрицательной. Аналогичная запись существует и в двоичной системе. Так, 101.1101 запишется в виде 1.011101*10 10 (Везде использована двоичная форма записи, так что 10 10 означает 2 2). Именно такое представление используется в компьютере. Двоичная точка в такой записи не остаётся на одном месте, а сдвигается на величину, указанную в экспоненте, поэтому такие числа называются числами с плавающей точкой (floating point numbers).
Вещественные типы Delphi
В Delphi существует четыре вещественных типа: Single, Double, Extended и Real. Их общий формат одинаков: Знак Экспонента Мантисса Знак – это всегда один бит. Он равен нулю для положительных чисел и единице для отрицательных. Что же касается размеров мантиссы и экспоненты, то именно в них и заключается различие между типами. Прежде чем перейти к конкретным цифрам, рассмотрим подробнее тип Real, сделав для этого небольшой экскурс в историю. Real – это стандартный тип языка Паскаль, присутствовавший там изначально. Когда создавался Паскаль, процессоры ещё не имели встроенной поддержки вещественных чисел, поэтому все операции над этим типом сводились к операциям с целыми числами. Соответственно, размер полей в типе Real был подобран так, чтобы оптимизировать эти операции. Микропроцессор Intel 8086/88 и его улучшенные варианты – 80286 и 80386 – также не имели аппаратной поддержки вещественных чисел. Но системы на базе этих процессоров имели возможность подключения так называемого сопроцессора. Эта микросхема работала с памятью через шины основного процессора и обеспечивала аппаратную поддержку вещественных чисел. В системах средней руки гнездо сопроцессора обычно было пустым, так как это удешевляло систему (разумеется, вставить туда сопроцессор не было проблемой). Для каждого центрального процессора выпускались свои сопроцессоры, маркировавшиеся Intel 8087, 80287 и 80387 соответственно. Были даже сопроцессоры, выпускаемые другими фирмами. Они работали быстрее, чем Intel"овские, но появлялись на рынке позже. Тип вещественных чисел, поддерживаемый сопроцессорами, не совпадает с Real. Чтобы обеспечить в своих системах поддержку сопроцессорных типов, Borland вводит в Turbo Pascal типы Single, Double и Extended. Extended – это родной для сопроцессора тип, а типы Single и Double получаются из него очень простым усечением. При загрузке числа типа Single или Double во внутренний регистр сопроцессора последний конвертирует их в Extended. Напротив, при выгрузке чисел этих типов из регистра в память сопроцессор усекает их до нужного размера. Внутренние же операции всегда выполняются с данными типа Extended (впрочем, из этого правила есть исключение, на котором мы остановимся позже, после детального рассмотрения формата различных типов). Single и Double используются для экономии памяти. Ни один из них также не совпадает с типом Real. В системах с сопроцессорами новые типы обрабатываются заметно (в 2-3 раза) быстрее, чем Real (это с учётом того, что тип Real после соответствующего преобразования также обрабатывался сопроцессором; если же сравнивать обработку типа Extended на машине с сопроцессором и Real на машине без сопроцессора, то там на отдельных операциях достигалась разница примерно в 100 раз). Чтобы программы с этими типами можно было выполнять и в системах без сопроцессора, была возможность подключать к ним программный эмулятор сопроцессора. Обработка этих типов эмулятором была медленнее, чем обработка Real. Начиная с 486-ого процессора Intel берёт курс на интеграцию процессора и сопроцессора в одной микросхеме. Процент брака в микросхемах слишком велик, поэтому Intel идёт на хитрость: если у микросхемы брак только в сопроцессорной части, то на этой микросхеме прожигаются перемычки, блокирующие сопроцессор, и микросхема продаётся как процессор 80486SX, не имеющий встроенного сопроцессора (в отличие от полноценной версии, которую назвали 80486DX). Бывали и обратные ситуации, когда сопроцессор повреждений не имел, зато процессор был неработоспособен. Такие микросхемы превращали в «сопроцессор 80487». Но это уже из области экзотики, и, насколько мне известно, до России этот сопроцессор не дошёл. Процессор Pentium во всех своих вариантах имел встроенный сопроцессор. Таким образом, с приходом этого процессора тип Real стал как бы обузой, а на передний план вышли Single, Double и Extended. Чтобы свести к минимуму необходимые переделки программ, Borland ввела новую директиву компилятора: {$REALCOMPATIBILITY ON/OFF}. По умолчанию стоит OFF, что означает отсутствие полной совместимости. В этом случае тип Real в Delphi совпадает с типом Double. Если же совместимость включена, тип Real совпадает со своим прообразом из Паскаля. Существует ещё тип Real48, который всегда, вне зависимости от настроек, совпадает со старым Real. Далее в этой статье под словом “Real” я всегда буду подразумевать старый тип. Отмечу, что всё это появилось только в Delphi 4, в более ранних версиях тип Real48 отсутствовал, а тип Real был всегда старым, шестибайтным. Итак, теперь можно, наконец, добраться до размеров полей.
Тип Размер типа, байт Размер мантиссы, бит Размер экспоненты, бит
Single 4 23 8
Double 8 52 11
Extended 10 64 15
Real 6 40 7
Другие параметры вещественных типов, такие как диапазон и точность, есть в справке Delphi, в которую я рекомендую почаще заглядывать. Рассмотрим тип Single, так как он является самым коротким и, следовательно, самым простым для понимания. Остальные типы отличаются от него только количественно. В дальнейшем числа в формате Single мы будем записывать как s eeeeeeee mmmmmmmmmmmmmmmmmmmmmmm, где s означает знаковый бит, e – бит экспоненты, m – бит мантиссы. Именно в таком порядке эти биты хранятся в четырёхбайтном значении (здесь учтена перестановка байтов; напоминаю, что в процессорах Intel байты в многобайтных значениях переставляются так, что младший байт идёт первым, а старший – последним). В мантиссе хранится двоичное число. Чтобы получить истинное значение мантиссы, к ней надо мысленно добавить слева единицу с точкой (то есть, например, мантисса 1010000000000000000000 означает двоичную дробь 1.101). Таким образом, имея 23 двоичных разряда, мы записываем числа с точностью до 24-ёх двоичных разрядов. Такая запись числа называется нормализованной. Экспонента по определению всегда целое число. Но способ записи экспоненты в вещественных числах не совпадает с обычным способом записи чисел со знаком. Ноль в этом представлении записывается как 01111111. В обычном представлении это равно 127. Соответственно, 10000000 (128 в обычном представлении) означает единицу, а 01111110 (126) означает –1, и так далее (то есть из обычного беззнакового числа надо вычесть 127, и получится число, закодированное в экспоненте). Из описанных выше правил есть исключения. Так, если все биты экспоненты равны нулю (то есть там стоит число –127), то к мантиссе перед её началом надо добавлять не “1.”, а “0.” (денормализованная запись). Это позволяет увеличить диапазон вещественных чисел. Если бы этого исключения не было бы, минимально возможное положительное число типа Single было бы равно примерно 5.9*10 -39 . А так появляется возможность использовать числа до 1.4*10 -45 . Побочным эффектом этого является то, что числа, меньшие, чем 1.17*10 -38 , представляются с меньшей, чем 24 двоичных разряда, точностью. Если все биты в экспоненте равны единице, а в матрице – нулю, то мы получаем комбинацию, известную как INF (от английского Infinity – бесконечность). Эта комбинация используется тогда, когда результат вычислений превышает максимально допустимое форматом число. В зависимости от значения бита s бесконечность может быть положительной или отрицательной. Если же при такой экспоненте в мантиссе хоть один бит не равен нулю, такая комбинация называется NAN (Not A Number – не число). Попытки использования комбинаций NAN или INF приводят к ошибке времени выполнения. Для задания нуля все биты мантиссы и экспоненты должны быть равны нулю (формально это означает 0*10 -127). С учётом описанных выше правил если хотя бы один бит экспоненты не будет равен нулю (т.е. экспонента будет больше -127), запись будет считаться нормализованной, и нулевая мантисса будет рассматриваться как единица. Поэтому никакие другие комбинации значений мантиссы и экспоненты не могут дать ноль. Тип Double устроен точно так же, разница только в количестве разрядов и в том, какое значение экспоненты берётся за ноль. Итак, мы имеем 11 разрядов для экспоненты. За ноль берётся значение 1023. Несколько иначе устроен Extended. Кроме количественных отличий добавляется ещё и одно качественное: в мантиссе явно указывается первый разряд. То есть, мантисса 1010... интерпретируется как 1.01, а не как 1.101, как это было в типах Single и Float. Поэтому если 23-битная мантисса типа Single обеспечивает 24-знаковую точность, а 52-битная мантисса Double – 53-битную, то 64-битная мантисса Extended обеспечивает 64-, а не 65-битную точность. Соответственно, при денормализованной форме записи первый разряд мантиссы явно содержит 0. За ноль экспоненты принимается значение 16383. Тип Real, как уже упоминалось, стоит особняком. Во-первых, в нём используется другой порядок следования битов, а, во-вторых, не используется денормализованная форма. Я не стал детально разбираться с типом Real, потому что сейчас это нужно разве что историкам, но никак не программистам. Напомню, чуть выше я сказал, что сопроцессор всегда выполняет все операции в формате Extended, оговорившись при этом, что есть исключение, к которому я вернусь чуть позже. Если вы прочитали всё, что было изложено выше, теперь у вас достаточно знаний, чтобы понять, что это за исключение. У сопроцессора есть специальный двухбайтный регистр, называемый управляющим словом. Установка отдельных битов этого регистра диктует сопроцессору то или иное поведение. Прежде всего, это связано с тем, какие исключения может возбуждать сопроцессор. Другие биты этого регистра отвечают за то, как будут округляться числа, как сопроцессор понимает бесконечность – всё это можно при необходимости узнать из документации Intel. Нас же будут интересовать только два бита из этого слова – восьмой и девятый. Именно они определяют, как будут обрабатываться числа внутри сопроцессора. Если восьмой бит содержит единицу (так установлено по умолчанию), то десять байт внутренних регистров сопроцессора будут использоваться полностью, и мы получим «полноценный» Extended. Если же этот бит равен нулю, то всё определяется значением бита 9. Если он равен единице, то используются только 53 разряда мантиссы (остальные всегда равны нулю). Если же этот бит равен нулю – только 24 разряда мантиссы. Это увеличивает скорость вычислений, но уменьшает точность. Другими словами, точность работы сопроцессора может быть понижена до типа Double или даже Single. Но это касается только мантиссы, экспонента в любом случае будет содержать 15 бит, так что диапазон типа Extended сохраняется в любом случае. Для работы с управляющим словом сопроцессора в модуле System описана переменная Default8087CW:Word и процедура Set8087CW(CW:Word). При запуске программы в переменную Default8087CW записывается то управляющее слово, которое установила система при запуске программы. Функция Set8087CW записывает новое значение в управляющее слово. Одновременно это новое значение записывается в переменную Default8087CW . Такое поведение этой функции не всегда удобно – иногда бывает нужно сохранить старое значение переменной Default8087CW (впрочем, это несложно сделать, заведя дополнительную переменную). С другой стороны, если значение управляющего слова изменить, не используя Set8087CW (а в дальнейшем мы увидим, что такие изменения могут происходить помимо нашей воли), то с помощью функции Default8087CW просто нет возможности узнать текущее значение управляющего слова. В Delphi 6 и выше появилась функция Get8087CW , позволяющая узнать значение именно контрольного слова, а не переменной Default8087CW . В более ранних версиях единственный способ получить значение этого слова – использование ассемблера, тем более что в Delphi нет проблем с ассемблерными вставками. Установить значение управляющего слова можно с помощью команды FLDCW, прочитать – с помощью FNSTCW. Обе эти команды имеют один аргумент – переменную типа Word. Чтобы, например, установить 53-значную точность, не изменив при этом другие биты управляющего слова, надо выполнить такую последовательность команд: asm FNSTCW MyCW AND MyCW,0FCFFh OR MyCW,200h FLDCW MyCW end; Современные сопроцессоры обрабатывают числа с такой скоростью, что при обычных вычислениях вряд ли может возникнуть необходимость в ускорении за счёт точности - выигрыш будет ничтожен. Эта возможность используется, в основном, в тех случаях, когда вычисления с плавающей точкой составляют значительную часть программы, а высокая точность не имеет принципиального значения (например, в движках для 3D-игр). Однако забывать об этой особенности работы сопроцессора не стоит, потому что она может преподнести один неприятный сюрприз, о котором чуть позже.
Бесконечные дроби
Из школы мы все помним, что не каждое число может быть записано конечной десятичной дробью. Бесконечные же дроби бывают двух видов: периодичные и непериодичные. Примером непериодичной дроби является число «пи», периодичной – число 1/3 или любая другая простая дробь, не представимая в виде конечной десятичной дроби. Для тех, кто забыл математику, напомню, что периодичные дроби – это такие дроби, которые содержат бесконечно повторяющуюся последовательность цифр. Например, 1/9=0.11111…, 1/12=0.08333333…, 1/7=0.142857142857… Для записи таких чисел используют скобки – в них заключают повторяющуюся часть. Те же числа должны быть записаны так: 1/9=0.1(1), 1/12=0.08(3), 1/7=0.1(428571). Впрочем, это было небольшое отступление. Нас сейчас не интересует вопрос о периодичности или непериодичности числа, нам достаточно знать, что не все числа можно представить в виде конечной десятичной дроби. При работе с такими числами мы всегда используем не точное, а приближённое значение, поэтому ответ получается тоже приближённым. Это нужно учитывать в своих расчётах. До сих пор мы говорили о только о десятичных бесконечных дробях. Но двоичные дроби тоже могут быть бесконечными. Даже более того, любое число, выражаемое конечной двоичной дробью, может быть также выражено и десятичной конечной дробью. Но существуют числа (например, 1/5) которые выражаются конечной десятичной дробью, но не могут быть выражены конечной двоичной дробью. Это сильно усложняет жизнь программистам. Далее мы рассмотрим примеры, в которых вещественные типы ведут себя необъяснимо для человека, не знакомого с внутренним форматом их записи. К каждому такому примеру будет дано объяснение. Надеюсь, что изучение этих примеров поможет понять, какие подводные камни подстерегают программиста, использующего вещественные типы, и как эти камни обойти. Все примеры построены одинаково: на форму надо кинуть два компонента – метку (TLabel) и кнопку (TButton). Так как это только примеры, я не стал придумывать имена для этих компонентов, пусть называются Button1 и Label1. Обработчик Button1Click содержит некоторый код, результаты работы которого выводятся на форму через Label1. Таким образом, нужно запустить программу, нажать на кнопку и посмотреть, что будет написано в метке. Я буду приводить только код обработчика Button1Click, так как всё остальное тривиально. Напомню, что в Паскале допускается не ставить точку с запятой перед end"ом (за исключением end"а в описании класса) и перед until"ом. Я предпочитаю пользоваться этой возможностью, так что не надо тыкать в меня пальцем, что я забываю ставить точки с запятой.

Пример первый – «неправильное значение»

Итак, напишем такой код: var R:Single; begin R:=0.1; Label1.Caption:=FloatToStr(R) end; Что мы увидим, когда нажмём кнопку? Разумеется, не «0.1», иначе не было бы смысла писать этот пример. Мы увидим «0.100000001490116». То есть расхождение в девятой значащей цифре. Ну, из справки по Delphi мы знаем, что точность типа Single – 7-8 десятичных разрядов, так что нас, по крайней мере, никто не обманывает. В чём же причина? Просто число 0.1 не представимо в виде конечной двоичной дроби, оно равно 0.0(0011). И эта бесконечная двоичная дробь обрубается на 24-ёх знаках; мы получаем не 0.1, а некоторое приближённое число (какое именно – см. выше). А если мы присвоим переменной R не 0.1, а 0.5? Тогда мы получим на экране 0.5, потому что 0.5 представляется в виде конечной двоичной дроби. Немного поэкспериментировав с различными числами, мы заметим, что точно представляются те числа, которые выражаются в виде m/2 n , где m, n – некоторые целые числа (разумеется, n не должно превышать 24, а то нам не хватит точности типа Single). В качестве упражнения предлагаю доказать, что любое целое число, для записи которого хватает 24-ёх двоичных разрядов, может быть точно передано типом Single.

Пример второй – сравнение

Теперь изменим код так: var R:Single; begin R:=0.1; if R=0.1 then Label1.Caption:="Равно" else Label1.Caption:="Не равно" end; При нажатии кнопки мы увидим надпись «Не равно». На первый взгляд это кажется абсурдом. Действительно, мы уже знаем, что переменная R получает значение 0.100000001490116 вместо 0.1. Но ведь «0.1» в правой части равенства тоже должно преобразоваться по тем же законам, ведь в компьютере всё предопределено. Тут самое время вспомнить, что процессоры Intel работают только с 10-байтным типом Extended, поэтому и левая, и правая часть равенства сначала преобразуется в этот тип, и лишь потом производится сравнение. То корявое число, которое оказалось в переменной R вместо 0.1, хоть и выглядит страшно, но зато представляется в виде конечной двоичной дроби. Информация же о том, что это на самом деле должно означать «0.1», нигде не сохранилось. При преобразовании этого числа в Extended младшие, избыточные по сравнению с типом Single разряды мантиссы просто заполняются нулями, и мы снова получим то же самое число, только записанное в формате Extended. А «0.1» из правой части равенства преобразуется в Extended без промежуточного превращения в Single. А 0.1 – бесконечная в двоичном представлении дробь. Поэтому некоторые из младших разрядов мантиссы будут содержать единицы. Другими словами, мы получим хоть и не точное представление числа 0.1, но всё же более близкое к истине, чем 0.100000001490116. Из-за таких хитрых преобразований оказывается, что мы сравниваем два близких, но всё же не равных числа. Отсюда – закономерный результат в виде надписи «Не равно». Тут уместна аналогия с десятичными дробями. Допустим, в одном случае мы делим 1 на три с точностью до трёх знаков, и получаем 0.333. Потом мы делим 1 на три с точностью то четырёх знаков, и получаем 0.3333. Теперь мы хотим сравнить эти два числа. Для этого приводим их к точности в четыре разряда. Получается, что мы сравниваем 0.3330 и 0.3333. Очевидно, что это разные числа. Если попробовать заменить число 0.1 на 0.5, то мы получим «Равно». Думаю, вы уже знаете почему, но для полноты текста объясню. 0.5 – это конечная двоичная дробь. При прямом приведении её к типу Extended в младших разрядах оказываются нули. Точно такие же нули оказываются в этих разрядах при превращении числа 0.5 типа Single в тип Extended. Поэтому в результате мы сравниваем два числа. Это похоже, как если бы мы делили 1 на 4 с точностью до трёх и до четырёх значащих цифр. В первом случае получили бы 0.250, во втором – 0.2500. Приведя их оба к точности в четыре знака, получим сравнение 0.2500 и 0.2500. Очевидно, что эти цифры равны.

Пример третий – сравнение разных типов

Немного усложним наш пример: var R1:Single; R2:Double; begin R1:=0.1; R2:=0.1; if R1=R2 then Label1.Caption:="Равно" else Label1.Caption:="Не равно" end; Наученные горьким опытом, вы, наверное, ожидаете увидеть надпись «Не равно». Что ж, жизнь вас не разочарует, именно это вы и увидите. Тип Double точнее, чем Single (хотя его точности тоже не хватает для представления бесконечной дроби). В R2 мы получим не 0.100000001490116, а другое число, с точностью 15-16 десятичных знаков. Я не могу назвать точно это число, потому что FloatToStr воспринимает его как 0.1, так что, заменив в первом примере Single на Double, вы увидите 0.1 (только не надо обольщаться, всё равно это не 0.1, просто функция FloatToStr имеет такую особенность работы). Числа в обеих переменных приводятся к типу Extended, но при этом они не меняются и, как были не равны, так и остаются неравными. Это напоминает ситуацию, когда мы сравниваем 0.333 и 0.3333, приводя их к точности в пять знаков: числа 0.33300 и 0.33330 не равны. Мне уже неловко надоедать вам такими очевидными замечаниями, но всё-таки: если в этом примере заменить 0.1 на 0.5, мы увидим «Равно».

Пример четвёртый – вычитание в цикле

Рассмотрим ещё один пример, иллюстрирующий ситуацию, которая часто озадачивает начинающего программиста var R:Single; I:Integer; begin R:=1; for I:=1 to 10 do R:=R-0.1; Label1.Caption:=FloatToStr(R) end; Конечно, если бы в результате выполнения этого примера вы увидели бы ноль, я бы не стал тратить на него время. Но на экране появится -7.3015691270939E-8. Думаю, такой оборот дела уже никого не удивляет. Мы уже знаем про то, что число 0.1 не может быть передано точно ни в одном из вещественных типов, и про преобразования Single в Extended и обратно. При этом постоянно происходят округления, и эти округления приводят к тому, что мы получаем в результате не ноль, а «почти ноль».

Пример пятый – сюрпириз от Microsoft

Изменим в предыдущем примере тип переменной R с Single на Double. Значение, выводимое программой, станет 1.44327637948555E-16. Вполне логичный и предсказуемый результат, так как тип Double точнее, чем Single и, следовательно, все вычисления более точны, мы просто обязаны получить более точный результат. Хотя, разумеется, абсолютная точность (то есть ноль), для нас остаётся недостижимым идеалом. А теперь – вопрос на засыпку. Изменится ли результат, если мы заменим Double на более точный Extended? Ответ не такой однозначный, каким его хотелось бы видеть. В принципе, после такой замены вы должны получить -6.7762635780344E-20. Но в некоторых случаях от замены Double на Extended результат не изменится, и вы снова получите 1.44327637948555E-16. Это зависит от операционной системы. Всё дело в использовании «неполноценного» Extended. При запуске программы любая система устанавливает такое управляющее слово сопроцессора, чтобы Extended был полноценным. Но затем программа вызывает много разных функций Windows API. Какая-то (или какие-то) из этих многочисленных функций в некорректно работают с управляющим словом, меняя его значение и не восстанавливая при выходе. Такая проблема встречается, в основном, в Windows 95 и старых версиях Windows 98. Также имеются сведения о том, что управляющее слово может портиться и в Windows NT, причём эффект наблюдался не сразу после установки системы, а лишь через некоторое время, после доустановки других программ. Проблема именно в некорректности поведения системных функций; значение управляющего слова, устанавливаемое системой при запуске программы, всегда одинаково. Эта проблема известна: например, в исходных кодах VCL можно найти сохранение управляющего слова сопроцессра перед вызовом некоторых API-функций с последующим его восстановлением. Комментарии сообщают, что функция может изменить значение управляющего слова, поэтому необходимо его сохранение и восстановление. Таким образом, приходим к неутешительному выводу: к тем проблемам с вещественными числами, которые обусловлены особенностями их аппаратной реализации, добавляются ещё и баги Windows. Правда, радует то, что в последнее время эти баги встречаются крайне редко - видимо, новые версии системы ведут себя более ответственно. Тем не менее, полностью исключать такую возможность нельзя, особенно если ваша программа будет использоваться на устаревшей технике с устаревшими системами (например, в образовательных учреждениях, финансирование которых оставляет желать лучшего). Чтобы наш пример всегда выдавал правильное значение -6.7762635780344E-20, достаточно поставить в начале нашей процедуры Set8087CW(Get8087CW or $0100), и программа в любой системе будет использовать сопроцессор в режиме максимальной точности. (Если вы используете старые версии Delphi, эту строку можно заменить на Set8087CW(Default8087CW), если, конечно, значения по умолчанию прочих флагов управляющего слова вас устраивают.) Раз уж мы заговорили об управляющем слове, давайте немного поэкспериментируем с ним. Изменим первую строчку на Set8087CW(Get8087CW and $FCFF or $0200). Тем самым мы переведём сопроцессор в режим 53-ёхразрядной точности представления мантиссы. Теперь в любой системе мы увидим 1.44327637948555E-16, несмотря на использование Extended. Если же мы изменим первую строчку на Set8087CW(Get8087CW and $FCFF), то будем работать в режиме 24-ёхразрядной точности. Соответственно, в любой системе будет результат -7.3015691270939E-8. Заметим, что при загрузке в 10-байтный регистр сопроцессора числа типа Extended в режиме пониженной точности «лишние» биты не обнуляются. Только результаты математических операций представляются с пониженной точностью. Кроме того, при сравнении двух чисел также учитываются все биты, независимо от точности. Поэтому код var R:Double; // или Single begin R:=0.1; if R=0.1 then Label1.Caption:="Равно" else Label1.Caption:="Не равно" end; при выборе любой точности даст «Не равно».

Пример шестой – машинное эпсилон

Когда мы имеем дело с вычислениями с ограниченной точностью, возникает такой парадокс. Пусть, например, мы считаем с точностью до трёх значащих цифр. Прибавим к числу 1.00 число 1.00*10 -4 . Если бы всё было честно, мы получили бы 1.0001. Но у нас ограничена точность, поэтому мы вынуждены округлять до трёх значащих цифр. В результате получается 1.00. Другими словами, мы прибавляем к единице некоторое число, большее нуля, а в результате из-за ограниченной точности получаем снова единицу. Наименьшее положительное число, которое при добавлении его к единице даёт результат, не равный единице, называется машинным эпсилон. Понятие машинного эпсилон у новичков нередко путается с понятием наименьшего числа, которое может быть записано в выбранном формате. Это неправильно. Машинное эпсилон определяется только размером мантиссы, а минимально возможное число оказывается существенно меньше из-за сдвига плавающей двоичной точки с помощью экспоненты. Прежде чем искать машинное эпсилон программно, попытаемся найти его из теоретических соображений. Итак, мантисса типа Extended содержит 64 разряда. Чтобы закодировать единицу, старший бит мантиссы должен быть равен 1 (денормализованная запись), остальные биты - нулю. Очевидно, что при такой записи наименьшее из чисел, для которых вполняется условие x>1, получается, когда самый младший бит мантиссы тоже будет равен единице, т.е. x=1.00...001 (в двоичном представлении; между точкой и младшей единицей 62 нуля). Таким образом, машинное эпсилон равно x-1, т.е. 0.00...001. В более привычной десятичной форме записи это будет 2 -63 , т.е. примерно 1.084*10 -19 . Теперь напишем программу для отыскания машинного эпсилон. var R:Extended; begin R:=1; while 1+R/2>1 do R:=R/2; Label1.Caption:=FloatToStr(R) end; В результате на экране появится число 1.0842021724855E-19 в полном соответствии с теоретическими выкладками (если в вашей системе присутствует описанный выше баг с переводом процессора в режим пониженной точности, вместо этого числа вы получите 2.22044604925031E-16, т.е. 2 -52 . Чтобы этого не происходило, исправьте значение управляющего слова). А теперь заменим тип Extended на Double. Результат не изменится. На Single – опять не изменится. Но такое поведение лишь на первый взгляд может показаться странным. Давайте подробнее рассмотрим выражение 1+R/2>1. Итак, все вычисления (в том числе и сравнение) сопроцессор выполняет с данными типа Extended. Последовательность действий такова: число R загружается в регистр сопроцессора, преобразуясь при этом к типу Extended. Дальше оно делится на 2, а затем к результату прибавляется 1, и всё это в Extended, никакого обратного преобразования в Single или Double не происходит. Затем это число сравнивается с единицей. Очевидно, что результат сравнения не должен зависеть от исходного типа R. В этой статье я постарался объяснить внутреннее устройство вещественных чисел с точки зрения процессоров Intel и упомянуть некоторые проблемы, которые с ними связаны. На самом деле все проблемы сводятся к двум: во-первых, не всякое вещественное число может быть представлено точно, и, во-вторых, не всякое вещественное число, представимое в виде конечной десятичной дроби, представимо в виде конечной двоичной дроби. Вторая проблема, наверное, приносит больше неприятностей начинающим пользователям, так как она менее очевидна. Рецепты преодоления этих проблем я сознательно не излагаю, так как оптимальный вариант очень сильно зависит от конкретной задачи. Человеку же, понявшему причины появления проблем, не составит труда в каждом конкретном случае подобрать наиболее приемлемое решение. В этом, собственно, и заключается разница между программистом и ламером: первый разбирается в задаче и находит для неё решение, второй умеет только кидать на форму готовые компоненты и передирать куски чужого кода. А эту статью я писал для начинающих программистов, а не для начинающих ламеров, отсюда и такой стиль.

Огромное спасибо Елене Филипповой за помощь в поиске информации.

Обсуждение материала [ 01-07-2019 03:46 ] 77 сообщений

Теперь обсудим типы данных Delphi, которые программист использует при написании программы. Любая программа на Delphi может содержать данные многих типов:

  • целые и дробные числа,
  • символы,
  • строки символов,
  • логические величины.

Библиотека языка Delphi включает в себя 7 целых типов данных: Shortint, Smallint, Longint, Int64, Byte, Word, Longword, характеристики которых приведены в таблице ниже.

Кроме того, в поддержку языка Delphi входят 6 различных вещественных типов (Real68, Single, Double, Extended, Comp, Currency), которые отличаются друг от друга, прежде всего, по диапазону допустимых значений, по количеству значащих цифр, по количеству байт, которые необходимы для хранения некоторых данных в памяти ПК (характеристики вещественных типов приведены ниже). Также в состав библиотеки языка Delphi входит и наиболее универсальный вещественный тип - тип Real, эквивалентный Double.

Символьный тип Delphi

Кроме числовых типов, язык Delphi располагает двумя символьными типами:

Тип Ansichar - символы c кодировкой ANSI, им ставятся в соответствие числа от 0 до 255;

Тип Widechar - символы с кодировкой Unicode, им ставятся в соответствие числа от 0 до 65 535.

Строковый тип Delphi

Строковый тип в Delphi обозначается идентификатором string. В языке Delphi представлены три строковых типа:

Тип Shortstring — присущ статически размещаемым в памяти ПК строкам, длина которых изменяется в диапазоне от 0 до 255 символов;

Тип Longstring — этим типом обладают динамически размещаемые в памяти ПК строки с длиной, ограниченной лишь объемом свободной памяти;

Тип WideString - тип данных, использующийся для того, чтобы держать необходимую последовательность Интернациональный символов, подобно целым предложениям. Всякий символ строки, имеющей тип WideString, представляет собой Unicode-символ. В отличие от типа Shortstring, тип WideString является указателем, который ссылается на переменные.

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

Пользовательские типы данных

При разработке программ довольно часто оказывается недостаточно тех типов данных, которые представлены языком программирования. Например, бывает удобным совместить в одной переменной сразу ряд однотипных данных, или же предусмотреть хранение данных разных типов, например, строк и чисел. К счастью, в Object Pascal имеется возможность создавать собственные типы данных на основе уже имеющихся, совмещая их, или комбинируя. Например, для создания упорядоченного списка однотипных данных используют массивы (arrays), а для объединения нескольких типов в один - записи (records).

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

Создание того или иного типа данных всегда начинается с декларации, или описания нового типа данных. Делается это в заголовочной части программы или модуля и начинается с ключевого слова type. После того, как новый тип данных определен, можно создавать переменные нового типа - точно так же, как и любого простого. Ну а особенности работы с тем или иным пользовательским типом данных определяются тем, к какому виду он принадлежит (массив, запись и т.д.), и собственно его реализацией (размер и размерность для - массивов, набор других типов - для записей и т.п.).

Массивы

Массив - это упорядоченная структура, состоящая из множества однотипных элементов, имеющих общее имя. Таким образом, при помощи всего лишь одной переменной можно хранить целый набор данных, при этом каждый элемент массива имеет свой собственный индекс, или индексы - в случае, если массив многомерный. Массив объявляется в Object Pascal при помощи ключевого слова Array, необязательных квадратных скобок с указанием числа элементов массива, и типа данных для элементов массива:

Array [индексы] of ;

Количество индексов определяет размерность массива. Так, для одномерного массива (в математике - вектор) требуется всего один индекс, а для двумерного массива (матрицы) понадобится 2 индекса и т.д. Объявления массивов могут выглядеть так:

Type MyArray1 = array of integer; type MyArray2 = array of integer;

В первом случае определен одномерный массив на 100 элементов типа целых чисел, во втором - двумерный массив размерностью 10 на 10, т.е. так же на 100 элементов типа целых чисел. После того, как массив определен, можно создавать переменные соответствующего типа:

Var A1: MyArray1;

Другим вариантом создания массива является одновременное объявление как переменной, так и описания массива:

Var A1: array of Integer;

Если же необходимый размер массива заранее неизвестен, то можно использовать динамические массивы. Они могут определяться как с предварительным объявлением типа, так и без такового - достаточно создать переменную и в качестве ее типа указать массив:

Var DynArray: array of integer;

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

SetLength(DynArray, 10);

Преимуществом динамического массива является то, что по ходу выполнения программы, количество выделяемой под массив память можно изменять.

Для обращения к конкретному элементу массива используется индекс (или индексы) элемента в массиве. Так, первый элемент в массиве типа MyArray1 (array ) имеет индекс, равный 1, а последний - 100. Соответственно, чтобы обратиться к первому элементу массива, скажем для того, чтобы присвоить ему значение, используются записи подобного типа:

A1 := 10;

Здесь мы присвоили элементу массива A1 с индексом 1 значение 10. Считывание данных производится аналогичным образом:

В данном случае переменной x будет присвоено значение 1-го элемента массива A1. При этом важно, чтобы было соблюдено 2 условия: во-первых, тот элемент массива, к которому идет обращение, должен существовать. Т.е. если обратиться к 11-му элементу массива, состоящего из 10 элементом, то произойдет ошибка доступа к памяти. Во-вторых, тип присваиваемых данных должен совпадать. Иначе говоря, если массив определен как целочисленный, то только целочисленные значения можно ему присваивать. При обратной же ситуации (считывания данных) правила несколько мягче, поскольку целое число можно присвоить переменой как целочисленного, так и вещественного типа.

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

Var A1: array of integer; var A2: array of array of integer; ... A1:=5; A2:=5; A1:=5; A2:=5;

В данном примере оба объявленных массива (A1 и A2) полностью идентичны. Точно так же идентичны и обращения к элементам массивов - в обоих случаях можно использовать как синтаксис с отдельными значениями индексов, так и с индексами, перечисляемыми через запятую. Многомерными могут быть не только статические массивы, но и динамические, при этом используется 2-й вариант объявления. Например, двумерный динамический массив вещественных чисел объявляется следующим образом:

Var DynArray: array of array of real;

При выделении памяти под такой массив так же следует учитывать его размерность. Например, если требуется массив размером 10 на 20 элементов, то следует использовать процедуру SetLength со следующими параметрами:

SetLength(DynArray, 10, 20);

В завершение знакомства с массивами нам осталось рассмотреть обещанный ранее пример использования циклов для заполнения массива. Допустим, у нас имеется простой массив-вектор для 10 целых чисел:

Var MyArray: array of integer;

Таким образом, если нам надо заполнить его значениями, следующими по порядку (скажем, числа от 10 до 100 с шагом 10), то вместо того, чтобы последовательно присваивать каждому элементу массива свое значение, можно использовать следующий цикл:

For i:= 1 to 10 do MyArray[i] := i * 10;

Здесь переменная i, являющаяся счетчиком цикла, при каждой его итерации последовательно увеличивается на 1. В результате каждый элемент массива MyArray получает значение этой переменной, умноженной на 10, т.е. 10, 20, 30, 40 и т.д. - чего нам и требовалось. Чтобы убедиться в этом, а заодно продемонстрировать цикл для считывания данных из массива, обратимся к примеру, приведенному в листинге 5.1 (на CD этот пример находится в папке Demo\Part1\ArrayFor):

Листинг 5.1. Запись и считывание данных массивов в цикле

Program arrayfor; {$APPTYPE CONSOLE} var MyArray: array of integer; i:integer; begin for i:= 1 to 10 do MyArray[i] := i * 10; // заполнение массива for i:= 1 to 10 do writeln(MyArray[i]); // вывод массива readln; // ожидание ввода для предотвращения закрытия окна end.

Относительно массивов можно сделать еще одно замечание: один уже известный нам тип данных - строки (string) можно рассматривать как массив символов (char). При этом никаких предварительных преобразований не требуется: если у нас уже есть строка, то всегда можно обратиться к отдельному ее символу как к элементу массива:

Var s: string; c: char; ... s:= Москва; c:= s;

В данном случае переменная c получит в качестве своего значения букву "М", т.е. первый символ строки.

Множества

Иногда бывает удобным ограничить возможные значения переменной только частью значений из множества всех значений, допускаемых ее типом. Допустим, нам нужна переменная типа Char, которая может принимать значения только из строчных латинских символов. В таком случае нам пригодится такое средство, как подмножество, в данном случае - подмножество символов от a до z. Определить его можно так:

Type SmLetter = a..z;

Таким образом, мы получили новый тип данных - SmLetter, который является "урезанным" вариантом типа Char. При использовании такого типа данных, переменные типа SmLetter не смогут принимать значения, выходящие за пределы указанного диапазона:

Var a: SmLetter; ... a:= ; // здесь все правильно, т.к. малая b входит в подмножество a..z a:= B; // ошибка! Прописная B не входит в подмножество a..z

Практическими кандидатами в подмножества могут быть переменные, предназначенные для хранения отдельных фрагментов дат (скажем, от 1 до 12 для месяцев и от 1 до 31 - для дней месяца), для проверки значений, которые программы получает в результате ввода пользователя с клавиатуры, и т.д. Следует лишь учитывать, что подмножества, по понятным причинам, могут принадлежать только к ординарным типам данных - целым числам или символам.

Развитием подмножеств являются множества, или наборы (sets). Они, подобно подмножествам, могут быть выделены в отдельные типы данных, однако допустимые диапазоны их значений определяются несколько иначе. Прежде всего, для определения множества используется ключевое слово set:

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

Type Letters = set of Сhar; var a: Letters; ... a:= ;

Таким образом, определение множества состоит из двух этапов: вначале определяется тип, на основании которого строится подмножество (Letters), затем объявляется переменная этого типа (a), и уже к переменной применяется диапазон. Преимущество здесь состоит в том, что, во-первых, по ходу программы можно менять допустимые диапазоны значений, а во-вторых, сами диапазоны определяются гораздо более гибко. В частности, они могут содержать в себе как ряды отдельных значений, так и подмножества, или их сочетания в любой последовательности. Например, если нам надо выделить только некоторые символы, скажем, прописные от A до K, а так же цифры 1, 3, 4, 5 и 9, то определение группы получится следующим:

Type Letters = set of Char; var a: Letters; ... a:= ;

Для проверки, является ли то или иное значение членом множества, используется операция in. Например, чтобы проверить, относится ли введенный пользователем символ (обозначим его как переменную "c") к множеству a, достаточно такого выражения:

If c in a then ...

Чтобы продемонстрировать работу множеств и операции in, обратимся к примеру, приведенному в листинге 5.2 (Demo\Part1\Ranges).

Листинг 5.2. Операция in и подмножества

Program rangeset; {$APPTYPE CONSOLE} type Letters = set of Char; var a: Letters; c: Char; begin a:= ; // определение группы readln(c); // считывание ввода пользователя if c in a then writeln(Input is Ok!) else writeln(Input is Wrong!); readln; // ожидание ввода для предотвращения закрытия окна end.

Еще одной разновидностью множества является перечисление. Использование перечислений призвано, прежде всего, улучшить читаемость (восприятие) кода программы. Допустим, в программе требуется неоднократно определять текущую раскладку клавиатуры, причем предусмотрено 3 состояния - русская, английская и другая. Разумеется, каждому состоянию можно назначить цифру, скажем, 1, 2 и 3 соответственно, однако по ходу написания программы всякий раз придется вспоминать, что означает та или иная цифра:

If KeyLang = 2 then ... // по прошествии месяца вспомните, что тут значит 2!

На помощь здесь приходят перечисляемые типы. Они определяются следующим образом:

= (, ...);

Например, в варианте для трех значений раскладки клавиатуры мы получим:

Type TKeyLang = (klRussian, klEnglish, klOther);

Согласитесь, что klEnglish куда понятнее, чем просто цифра "2". Более того, на основании заданного таким образом перечисления можно создать тип-множество. Для этого после того, как исходный тип с диапазоном определен, определяют имя типа на его основе:

Type TKeyLang = (klRussian, klEnglish, klOther); TKeyLangs = set of TKeyLang;

ВНИМАНИЕ
Обратите внимание на то, что имена типов начинаются с буквы "T". Хотя это и не является требованием языка, однако начинать названия сложных типов с этой буквы (от слова Type - тип) де-факто является стандартом.

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

Var KeyLang: TKeyLangs; ... if KeyLang = klEnglish then ;

ПРИМЕЧАНИЕ
Объявление типов-перечислений в 2 этапа является наиболее распространенной практикой. При этом исходный тип определяется именем в единственном числе (TKeyLang), а производный - во множественном, с "s" на конце (TKeyLangs).

Записи

Еще одной полезной разновидностью пользовательских видов данных являются записи (records). Запись, подобно массиву, может хранить в себе целый набор данных, но при этом они не должны принадлежать к одному типу. Иначе говоря, если массивы предназначены для хранения каких-либо рядов данных, то записи - для объединения разнородной информации под общим именем. Например, запись - просто незаменимый тип данных для хранения такой информации, как адреса или информация о персоне. И в том и в другом случае под общим именем должны быть собраны данные разного типа - строковые, численные и т.д. Например, для адреса это почтовый индекс, названия города и улицы, а так же номера дома и квартиры. Всю эту разнотипную информацию можно собрать под общей вывеской одной записи.

Объявление записи начинается с ключевого слова record, за которым следует перечисление всех входящих в нее элементов, называемых полями записи, и завершается ключевым словом end:

Record: ; ... : ; end;

Применительно к почтовому адресу определение записи может выглядеть таким образом:

Type TAddress = record PostIndex: integer; City: string; Street: string; HouseNr: integer; FlatNr: integer; end;

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

Var MyAddr: TAddress; ... MyAddr.PostIndex:= 119071; MyAddr.City:= Москва; В том случае, если полем записи является другая запись, то точка используется дважды. Так, если у нас определена еще одна запись, для хранения информации о персоне, то одним из ее полей наверняка окажется адрес, а у нас уже есть подходящий тип данных для этого, так что можно использовать его в качестве поля: type TPerson = record Name: string; Phone: string; Address: TAddress; end;

Таким образом, все поля типа TAddress будут принадлежать также и к типу TPerson, но обращаться к ним надо не напрямую, а через поле записи TPerson соответствующего типа, т.е. через Address. Такие поля называются составными:

Var Anybody: TPerson; ... Anybody.Name:= Вася Иванов; Anybody.Address.PostIndex:= 119071; Anybody.Address.City:= Москва;

В то же время, можно поступить и по другому: создать переменную типа TAddress, заполнить ее значениями, а затем присвоить соответствующему полю переменной типа TPerson. В примере, приведенном в листинге 5.3, продемонстрированы оба способа работы с составными полями, а так же продемонстрировано, что со всеми полями можно делать то же самое, что и с отдельными переменными того же типа.

Листинг 5.3. Записи

Program recdemo; {$APPTYPE CONSOLE} type TAddress = record PostIndex: integer; City: string; Street: string; HouseNr: integer; FlatNr: integer; end; TPerson = record Name: string; Phone: string; Address: TAddress; end; var Anybody: TPerson; Address: TAddress; begin write(Name:); readln(Anybody.Name); write(Phone:); readln(Anybody.Phone); write(Postal Index:); readln(Address.PostIndex); write(City:); readln(Address.City); write(Street:); readln(Address.Street); write(House number:); readln(Address.HouseNr); write(Flat number:); readln(Address.FlatNr); Anybody.Address:=Address; writeln(Anybody.Name); writeln(Anybody.Phone); writeln(Anybody.Address.PostIndex); writeln(Anybody.Address.City); writeln(Anybody.Address.Street); writeln(Anybody.Address.HouseNr); writeln(Anybody.Address.FlatNr); readln; end.

Приведенная в листинге программа последовательно предлагает пользователю ввести свойства - сначала для записи о персоне, а затем - для адреса, после чего полю адреса персоны присваивается значение записи-адреса. После этого все поля последовательно выводятся на экран. Исходный код программы можно найти в папке Demo\Part1\Records (файл recdemo.dpr).

В завершение темы записей рассмотрим еще одну их особенность, характерную для Object Pascal, а именно - возможность сочетать в одном типе записи с разными полями. Называются такие записи вариантными и объявляются так же как и обычные, за исключением того, что содержат дополнительную часть, начинающуюся с ключевого слова case:

Record [ : ; ... : ;] case [ :] of: (); ... : (); end;

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

Type TEmployee = record Name: string; JobTitle: string; case Salaried: Boolean of true: (Salary: Currency); false: (Hourly: Currency); end;

Здесь, в зависимости от того, будет ли значением поля Salaried той или иной переменной типа TEmployee ложь или истина, у нее будет либо поле Salary, либо Hourly. Пример использования подобной вариантной записи вы можете посмотреть в файле varrec.dpr.

Специальные типы данных

Помимо уже рассмотренных типов данных, простых и пользовательских, в Object Pascal имеется ряд иных, специализированных типов. Например, для времени используют тип TDateTime. В принципе, этот тип алогичен вещественному типу Double, однако для работы с такими данными, как временные диапазоны, удобнее использовать специальный тип TDateTime.

Поскольку этот тип является, фактически, вещественным числом, то данные хранятся в нем следующим образом: целая часть числа определяет дату, за которую берется количество дней, прошедших с 31 декабря 1899 года, а дробная определяет время в миллисекундах, прошедших с начала текущего дня. Преимуществом же типа TDateTime является то, что для него предусмотрен целый набор готовых функций, позволяющих работать с датами и временем. Их список приведен в таблице 5.1.

Таблица 5.1. Функции для работы с датой и временем
Функция Описание
Now Возвращает текущие дату и время
Date Возвращает текущую дату (целую часть TDateTime)
Time Возвращает текущее время (дробную часть TDateTime)
DateTimeToStr Преобразует дату и время в строку на основе системных настроек
DateTimeToString Копирует дату и время в указанную строковую переменную
DateToStr Преобразует дату в строку
TimeToStr Преобразует время в строку
FormatDateTime Преобразует дату и время в указанный формат
StrToDateTime Преобразует строку, содержащую написанную надлежащим способом дату и время, в переменную типа TDateTime
StrToDate Преобразует строку в дату в формате TDateTime
StrToTime Преобразует строку во время в формате TDateTime
DayOfWeek Возвращает номер дня недели (от 1 до 7) для указанной даты. Учитывайте, что 1-й день недели – воскресенье
DecodeDate Раскладывает значение типа TDateTime на 3 целых, представляющих собой год, месяц и день месяца
DecodeTime Раскладывает значение типа TDateTime на 4 целых, представляющих собой часы, минуты, секунды и миллисекунды
EncodeDate Объединяет 3 целых, представляющих собой год, месяц и день, в одно значение типа TDateTime
EncodeTime Объединяет 4 целых, представляющих собой часы, минуты, секунды и миллисекунды? в одно значение типа TDateTime

Типичный пример использования функций для работы с датами, может выглядеть примерно таким образом:

Var today, yesterday: TDateTime; s: string; ... today:= Now(); yesterday:= today - 1; s:= TateToStr(yesterday);

Здесь переменной s будет назначено значение, соответствующее вчерашнему дню в формате, принятому в системе (например, "16.07.2005"). Более полный пример работы с датами вы можете посмотреть в Demo\Part1\Dates.

Кроме дат, рассмотрим еще один тип данных - файлы (files). Файлы представляет собой некую последовательность однотипных элементов, размещенных на внешнем носителе, не находящемся в оперативной памяти ПК. В типичном случае таким носителем является жесткий диск. Этот тип данных можно охарактеризовать как одномерный массив без указания размера. Чтобы производить те или иные операции над такими данными, используют специальную переменную файлового типа. Причем в зависимости от того, с какими данными предстоит работать, задают тот или иной тип файла. Например, для работы с текстовыми файлами используют специальный тип TextFile, а если файл содержит в себе ряд чисел, то определяют, какой тип чисел в нем используется:

Var f1: TextFile; // текстовый файл f2: File of integer; // файл с целыми числами f3: File of double; // файл с вещественными числами

Если же файл нетипизированный, (например, бинарный), то используют тип File без каких-либо дополнений:

Var f4: File; // двоичный файл или файл заранее неизвестного типа

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

ПРИМЕЧАНИЕ
Саму файловую переменную называют дескриптором файла, т.е. фактически она лишь указывает программе на место в памяти, через которое обеспечивается доступ к файлу средствами операционной системы.

Для работы с файлами так же предусмотрен целый ряд процедур и функций. Среди них можно отметить уже знакомые нам read/readln и write/writeln. Чтобы эти процедуры работали с файлами, в качестве первого параметра указывают имя файловой переменной (дескриптор файла):

Writeln(f, Текст для записи в файл);

Но прежде, чем производить запись в файл, или начать считывать из него данные, как уже говорилось, надо связать переменную с файлом (назначить дескриптор) и открыть его, попутно назначив режим доступа. Назначение дескриптора файлу производится с помощью процедуры AssignFile, например

AssignFile(f, c:\file.txt);

Что касается открытия файла, то тут дела обстоят несколько сложнее, поскольку следует учитывать тип файла и режим доступа. Так, применительно к текстовым файлам, используют процедуры Reset, Rewrite и Append, открывающие файл на чтение, перезапись и добавление (запись в конец файла), соответственно.

Наконец, следует отметить, что после того, как операции с файлом произведены, его необходимо закрыть. Для закрытия файла используется процедура CloseFile. Таким образом, вариант использование всех этих процедур для чтения и записи файла в общем случае, выглядит таким образом, как показано в листинге 5.4:

Листинг 5.4. Запись и чтение в файлах

Program readwrite; {$APPTYPE CONSOLE} uses SysUtils; var f: TextFile; s: string; begin AssignFile(f, c:\test.txt); // назначаем дескриптор файлу text.txt Rewrite(f); // открываем файл на запись writeln(f, s); // производим запись в файл CloseFile(f); // закрываем файл Reset(f); // открываем файл на чтение readln(f, s); // считываем данные из файла CloseFile(f); // закрываем файл end;

Еще один пример работы с файлами можно посмотреть в Demo\Part1\Files. Вместе с тем, на практике файловые типы данных не часто используются при современном программировании в среде Delphi, поскольку VCL предлагает ряд более удобных и изящных методов для хранения данных на диске, начиная от методов отдельных классов и заканчивая потоками и базами данных.

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

При рассмотрении простых типов мы уже поднимали вопрос их совместимости и преобразования друг в друга. Теперь настала пора рассмотреть этот аспект более внимательно. В частности, если целое число без проблем приводится к вещественному, то как быть в том случае, если требуется обратное преобразование? Выход в данной ситуации состоит в использовании специальных функций преобразования типов. Так, для преобразования вещественного числа в целое используются функции Round и Trunc. Их отличие состоит в том, что первая округляет значение до целого, опираясь на стандартные математические правила, а вторая - просто отбрасывает дробную часть числа. Отметим, что если надо просто отбросить дробную часть числа, оставив тип данных без изменений, то следует использовать другую функцию - Int. Примеры их использования показаны ниже:

Var i: integer; r: real; ... r:= 5.75; i:= Round(r); // i получит значение 6 i:= Trunc(r); // i получит значение 5 r:= Int(r); // r получит значение 5.0

Куда большее количество функций предусмотрено для преобразования числовых типов в строковые и наоборот. С некоторыми из них, предназначенных для дат, мы уже знакомы (см. таблицу 5.1). Другие же представлены в таблице 5.2.

Таблица 5.2. Функции для преобразования чисел в строки и наоборот
Функция Описание
IntToStr Преобразует целое число в строку
StrToInt Преобразует строку в целое число, в случае невозможности преобразования вызывает ошибку
StrToIntDef Преобразует строку в целое число, в случае невозможности преобразования возвращает число, указанное в качестве второго аргумента
FloatToStr Преобразует вещественное число в строку
FloatToStrF Преобразует вещественное число в строку на основе указанного формата
StrToFloat Преобразует строку в вещественное число, в случае невозможности преобразования вызывает ошибку
StrToFloatDef Преобразует строку в вещественное число, в случае невозможности преобразования возвращает число, указанное в качестве второго аргумента
CurrToStr Преобразует число типа Currency в строку
CurrToStrF Преобразует число типа Currency в строку на основе указанного формата
StrToCurr Преобразует строку в число типа Currency, в случае невозможности преобразования вызывает ошибку
StrToCurrDef Преобразует строку в число типа Currency, в случае невозможности преобразования возвращает число, указанное в качестве второго аргумента

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

Str:= FloatToStrF(x, ffGeneral, 10, 2);

Здесь ffGeneral является указанием на формат вывода, 10 определяет максимально возможное число знаков в числе вообще, а 2 - предельно допустимое число знаков после запятой. Помимо ffGeneral, определяющего наиболее обобщенный формат представления чисел, имеются и другие:

  • ffExponent - формат с экспонентой (например, 1.45E10);
  • ffFixed - фиксированный формат (например, 145000.01);
  • ffNumber - "прописной" формат (например, 1,450,000.0);
  • ffCurrency - валютный формат (например, 145 000,00р).

Таким образом, привести то или иное число к строке нужного формата, оказывается достаточно просто - важно лишь определиться, что нужно получить. А еще одна функция - Chr - позволяет преобразовывать маленькие числа (до 255) в символы, т.е. фактически, из типа Byte делает Char.

Что касается обратных преобразований (строк в числа), то тут следует соблюдать определенную осторожность, поскольку далеко не всякая строка может стать числом. В первую очередь это касается преобразований вещественных числе, поскольку нередко вместо отделяющей мантиссу точки может оказаться запятая. В случаях, когда преобразование строки в число невозможно, возникает ошибка выполнения программы. Ее можно обрабатывать самостоятельно (об обработке ошибок будет рассказано во второй части книги), или же поручить это дело функции - в таком случае следует использовать функции с суффиксом Def (StrToIntDef, StrToFloatDef и StrToCurrDef). В качестве второго аргумента они принимают значение, которое следует использовать в том случае, если преобразование невозможно:

X:= StrToIntDef(str, -1);

В данном случае, если в строке str не удастся распознать число, переменной x будет присвоено значение -1.

При всем многообразии типов данных, в Object Pascal существует тип еще один тип, который не имеет типа - вариантный. Называется он Variant. Переменные вариантного типа могут принимать значения любого простого типа, а так же некоторых специальных. В типичном случае использование вариантного типа может выглядеть так:

Var v: variant; ... v:= 5; v:= Строковое значение; v:= 10.54;

Этот пример иллюстрирует, как одной т той же переменной последовательно присваиваются данные 3 разных типов - целочисленного, строкового и вещественного. При этом никакой ошибки не возникает. Однако вариантные данные обрабатываются гораздо медленнее, чем типизированные - практически так же медленно, как программы на языке BASIC. (кстати, в классическом BASIC как раз только вариантные данные и были). Кроме того, использование нетипизированных данных вообще, а в строго типизированном языке - особенно, чревато непредсказуемым поведением программы и прочими ошибками. Поэтому оставим этот тип для внутреннего использования Delphi - в VCL он применяется для работы с OLE и базами данных.

Указатели

Указатели (pointers) - это такой тип переменных, которые хранят адрес в памяти компьютера, по которому расположена другая переменная. Фактически, указатель не содержит значение, а ссылается на него.

Указатели можно задать двумя принципиально разными способами. Во-первых, можно использовать специальный тип - Pointer. При этом будет создан нетипизированный указатель, под который всякий раз надо будет принудительно выделять память, используя функцию GetMem. Другой, как правило более предпочтительный способ, состоит в том, что сразу же создается указатель нужного типа. Делается это при помощи символа "^", предшествующего названию типа:

Var P: ^integer;

Здесь мы определили указатель P, являющийся указателем на переменную целочисленного типа.

После того, как указатель создан, можно связать его с переменной подходящего типа, используя операцию @:

Var P: ^integer; x: integer; ... P:= @x; Теперь к переменной x можно обращаться как непосредственно, так и через ее указатель. В случае обращения через указатель так же используют символ "^": x:= 10; P^ := 10;

В обоих случаях переменная x получит значение 10.

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

Var P: ^integer; ... New(P); // выделение памяти, необходимой для хранения данных типа Integer P^ := 10; // занесение данных в выделенный блок памяти Dispose(P); // освобождение памяти

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

В целом же указатели, как правило, используются для взаимодействия с системными функциями операционной системы (в частности, с Windows API), для взаимодействия со сторонними программами и динамически подключаемыми библиотеками.

Объекты

Самые сложные и интересные типы данных - это объекты. В современных версиях Delphi объекты бывают 3 основных типов: собственно объекты, а так же классы и интерфейсы. Тип объекта (object) достался Delphi от предшественника - языка Pascal with Objects, и в настоящее время практически не используется. Основным типом объектных данных в современных программах является класс (class). Что касается интерфейсов (interface), то они являются разновидностью классов, и предназначены для взаимодействия с системными объектными функциями Windows.

Тема объектов достаточно обширна, поскольку является основой для парадигмы объектно-ориентированного программирования. ООП в Object Pascal рассматривается во второй части настоящего издания.

В предыдущих уроках мы между делом знакомились с типами данных. Всё это время речь шла о простых типах. Сегодня мы обобщим пройденное ранее, а также познакомимся с новым материалом, который необходимо знать в рамках темы "Простые типы данных". Осмысленно подходить к выбору типов данных для используемых в программах переменных необходимо по разным причинам. Во-первых, имея под рукой многообразие доступных типов и умело ими распоряжаясь, можно сократить объём памяти, требуемый программе для работы. Экономию в 1-2 байта никто не заметит, но если речь идёт о больших объёмах данных, эти байты могут вылиться во вполне реальные мегабайты. Во-вторых, разумный выбор типов данных позволяет избежать некоторых ошибок, причём как со стороны программиста (на этапе создания программы), так со стороны пользователя (во время использования программы).

Простые типы данных - общее представление

Простые типы данных названы простыми, потому что они не содержат внутри себя никаких других типов. Кроме того, простые типы данных обеспечивают хранение в памяти только одного значения. К простым типам данных относят следующие:

  • целочисленные;
  • вещественные;
  • логические;
  • строковые (символьные).

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

Целочисленный тип - содержит числовые значения, целые числа. Числа упорядочены по возрастанию: ..., -2, -1, 0, 1, 2, 3, ...
Логический тип - содержит всего 2 значения - True, False, которые тоже упорядочены: False, True (следует из соответствия False - 0, True - 1).
Символьный тип - символы кодовой таблицы. Поскольку каждому символу соответствует свой код, то символы расположены в порядке увеличения кода. К примеру, буквы латинского алфавита A, B, C, D, ... идут в кодовой таблице именно так, т.к. чем дальше от начала алфавита, тем больший код имеет буква. То же самое касается и арабских чисел в кодовой таблице - они идут по порядку: 0, 1, 2, ..., 8, 9. Это позволяет делать такие сравнения, как, например "A" < "Z" (это истинно).

Из того, что перечисленных типы данных упорядочены, следует, что все значения образуют конечную последовательность. Это соответствует нашим представлениям о типах данных - все они имеют свои ограничения. К примеру, нет числового типа данных, который позволил бы хранить сколь угодно большое число. "Большие" типы есть, но "число" "бесконечность" они хранить не могут.

Функции и процедуры для порядковых типов данных

Существует несколько полезных функций и процедур, без использования которых, порой, сложно оперировать порядковыми типами данных:

Pred() - функция возвращает предыдущее значение для выражения, указанного в качестве единственного аргумента.

Примеры: Pred(5) = 4, Pred("E") = "D", Pred(True) = False.

Succ() - функция, обратная для Pred() - возвращает следующее значение.

Примеры: Succ(5) = 6, Succ("E") = "F", Succ(False) = True.

Ord() - возвращает порядковый номер значения в списке значений типа данных. С этой функцией мы уже встречались при работе со строками - с её помощью мы узнавали код символа.

Примеры: Ord("A") = 65, Ord(True) = 1.

Low() - возвращает минимальное значение указанного типа данных.

Примеры: Low(Byte) = 0, Low(Boolean) = False, Low(Char) = #0 (символ с кодом 0).

High() - возвращает максимальное значение указанного типа данных.

Примеры: High(Byte) = 255, High(Boolean) = True, High(Char) = #255 (в русской локали это символ "я").

Ну и ещё две процедуры, с которыми мы уже знакомы:

Dec() - уменьшает значение на единицу.

Inc() - увеличивает значение на единицу.

Не забывайте о втором необязательном параметре этих процедур.

Пользовательские типы данных

На основе порядковых типов данных программист может создать свои собственные типы - перечислимые и интервальные. Они будут рассмотрены ниже.

Целочисленные типы

Как следует из названия, целочисленные типы позволяют хранить целые числа. Среди них есть типы, которые хранят числа со знаком (т.е. положительные или отрицательные), а есть и такие, которые хранят только положительные. Чем большее количество значений может содержать тип, тем больше памяти он занимает. Рассмотрим целочисленные типы данных.

Сначала рассмотрим беззнаковые типы, т.е. те, которые позволяют хранить только положительные числа и ноль:

Byte - значения 0..255 - занимает в памяти 1 байт.

Word - значения 0..65535 - 2 байта.

LongWord - значения 0..4294967295 - 4 байта.

Теперь типы со знаком (отрицательные числа записываются со знаком минус "-" впереди, неотрицательные могут записываться как со знаком "+", так и без него):

ShortInt - значения -128..127 - 1 байт.

SmallInt - значения -32768..32767 - 2 байта.

LongInt - значения -2147483648..2147483647 - 4 байта.

Int64 - значения -2 ^53 ..2 ^53 -1 - 8 байт.

Существуют также 2 общих типа, которые находят своё отражение в вышеперечисленных. Рекомендуется использовать именно эти типы, т.к. компилятор "заточен" под них и создаёт более быстрый и эффективный код:

Integer - значения -2147483648..2147483647 - 4 байта.

Cardinal - значения 0..4294967295 - 4 байта.

Следует отметить, что целые числа могут быть представлены не только в десятичной, но и в шестнадцатеричной системе счисления, т.е. в виде $xxxxxxxx, где x - один из символов 0, 1, ..., 8, 9, A, B, ..., E, F. К примеру, все цвета (точнее, их коды) представляются именно в виде шестнадцатеричных чисел.

Логические типы

С логическими выражениями и с логическим типом данных мы уже знакомы - это тип Boolean , принимающий значения True и False . Помимо Boolean существуют следующие логические типы: ByteBool , WordBool и LongBool . Однако последние введены лишь для обспечения совместимости с другими языками и системами программирования. Использовать рекомендуется только тип Boolean. Логическое значение в памяти занимает 1 байт. На самом деле, конечно, достаточно и одного бита, но оперировать ячейками меньше байта, мы, к сожалению, не можем.

Символьные типы

Символьные типы обеспечивают хранение отдельных символов. Основной тип данных - Char , который содержит символы с кодами 0..255 . Существуют ещё типы AnsiChar и WideChar . Тип AnsiChar эквивалентен типу Char , т.е. по сути это один и тот же тип. Занимает в памяти 1 байт. Для кодирования символов используется код ANSI (American National Standards Institute ). Тип WideChar кодируется международным кодом Unicode и занимает в памяти 2 байта. Таблица Unicode включает символы практически всех языков мира.

Вещественные типы

Из названия следует, что эти типы используются для хранения вещественных, т.е. действительных чисел. Отличаются они границами допустимых значений и точностью, т.е. числом цифр после запятой. Вот эти типы:

Real (он же Double ) - значения от 5.0x10 ^-324 до 1.7x10 ^308 , точность - 15-16 цифр, занимает в памяти 8 байт.

Real48 - значения от 2.9x10 ^-39 до 1.7x10 ^38 , точность - 11-12 цифр, 6 байт памяти.

Single - значения от 1.7x10 ^-45 до 3.4x10 ^38 , точность - 7-8 цифр, 4 байта.

Extended - от 3.6x10 ^-4951 до 1.1x10 ^4932 , точность - 19-20 цифр, 10 байт памяти.

Comp - от -2x10 ^63 +1 до 2x10 ^63 -1 , точность - 19-20 цифр, 8 байт.

Currency - от -922337203685477.5808 до 922337203685477.5807 , точность - 19-20 цифр, в памяти занимает 8 байт.

Как и в случае с целыми числами, перед вещественными числами может стоять знак "+" или "-".

Существует 2 формы записи вещественных чисел - с фиксированной точкой и с плавающей .

Запись с фиксированной точкой представляет собой обычную запись, в которой целая и дробная части отделены друг от друга точкой/запятой.

Запись с плавающей точкой подразумевает запись порядка числа, который отделяется от самого числа буквой "E" (запись "e" тоже допустима). Например, запись 1.5e2 означает число 1.5 с порядком +2, т.е. это 1.5x10 ^2 = 150.

Типы Comp и Currency были введены специально для произведения точных денежных расчётов. При этом, тип Comp , как видно из значений границ диапазона, хранит целые числа, поэтому при задании чисел с дробной частью они автоматически преобразуются в ближайшее целое число.

Перечислимые типы данных

От рассмотрения готовых типов данных перейдём к типам, которые могут быть созданы самим программистом. Один из вариантов, как было отмечено выше, - это перечислимый тип.

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

Значения типа данных перечисляются через запятую, а весь этот набор заключается в круглые скобки. Описание типа должно производиться в специальном разделе раздела описаний - разделе описания типов. Этот раздел предваряется ключевым словом type . Т.е. запись идёт приблизительно так же, как и описание переменных или констант, только вместо var и const пишется type . Сам тип описывается следующим образом: название типа, далее знак равенства и далее само значение. В случае с перечислимым типом это будет набор возможных значений.

Примечание: практически все типы данных в Object Pascal принято называть с буквы "T" (сокращённо от "Type"). Это не закон языка - просто одно из правил хорошего тона. Зная, что "T***" - это тип, вы никогда не ошибётесь, в противном же случае название можно спутать, например, с названием переменной.

Допустим, мы хотии задать тип данных, определяющий один из месяцев года. Мы можем описать его так:

type TMonth = (Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec ) ; var M: TMonth; {...} M:=Jun;

Обратите внимание, что после описания перечислимого типа в программе не может быть переменных, название которых совпадает с названием значений объявленного типа. В нашем примере не может быть переменных "Jan", "Feb" и т.д. При попытке присвоения переменной перечислимого типа значение, не указанное в списке, компилятор выдаст ошибку, поэтому ошибиться не представляется возможным.

Раздел type существует как в модуле всей формы (в этом разделе изначально описана сама форма: TForm1 = class(TForm) ... ), так и в любой подпрограмме. Область действия типа, соответственно, определяется местом в программе, в котором он описан.

Интервальные типы данных

Интервальные типы данных (также их называют ограниченными) получаются из имеющихся типов путём ограничения диапазона значений. Интервал задаётся двумя константами - начальной и конечной границей. При каждом присвоении значения переменной выполняется проверка соответствия нового значения указанному диапазону. Если значение не попадает в диапазон, выдаётся сообщение об ошибке. Во время выполнения программы задание недопустимого значения к ошибке не приводит, зато значение переменной может стать неверным.
Ограниченный тип данных можно создать только на основе простого упорядоченного типа. Значение второй константы (т.е. правой границы) должно быть больше значения первой (левой границы).
Ограниченные типы данных также описывают в разделе type . Формат записи похожий, только между константами-границами ставятся две точки.

Например, мы хотим в программе работать с датами. Можно создать ограниченные типы данных для значений дня, месяца и года (диапазон для значения года следует задать в зависимости от контекста задачи):

type TDay = 1 ..31 ; TMonth = 1 ..12 ; TYear = 1900 ..2100 ;

Помните, что использование ограниченного типа данных не уменьшит объём занимаемой памяти. Это следует из того, что задание интервала - это всего лишь условное задание возможных значений из общего набора значений данного типа.

Заключение

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

a - латинская) - только адрес статьи (URL);
{{статья:122}} - полноценная HTML-ссылка на статью (текст ссылки - название статьи).







2024 © teslales.ru.