Вы находитесь на страницах старой версии сайта.
Переходите на новую версию Interface.Ru

предыдущая статья серии

Программирование на языке Delphi
Глава 2. Основы языка Delphi. Часть 4

© А.Н. Вальвачев, К.А. Сурков, Д.А. Сурков, Ю.М. Четырько
Статья была опубликована на сайте rsdn.ru

Файлы

Понятие файла

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

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

В зависимости от типа элементов различают три вида файла:

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

Для работы с файлом, состоящим из типовых элементов переменная объявляется с помощью словосочетания file of, после которого записывается тип элемента:

var
  F: file of TPerson;

К моменту такого объявления тип TPerson должен быть уже описан (см. выше).

Объявление переменной для работы с нетипизированным файлом выполняется с помощью отдельного слова file:

var
  F: file;

Для работы с текстовым файлом переменная описывается с типом TextFile:

var
  F: TextFile;

Работа с файлами

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

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

AssignFile(F, 'MyFile.txt');

В результате этого действия поля файловой переменной F инициализируются начальными значениями. При этом в поле имени файла заносится строка 'MyFile.txt'.

Так как файла еще нет на диске, его нужно создать:

Rewrite(F);

Теперь запишем в файл несколько строк текста. Это делается с помощью хорошо вам знакомых процедур Write и Writeln:

Writeln(F, 'Pi = ', Pi);
Writeln(F, 'Exp = ', Exp(1));

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

После работы файл должен быть закрыт:

CloseFile(F);

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

Reset(F);

Для чтения элементов используются процедуры Read и Readln, в которых первый параметр показывает, откуда происходит ввод данных. После работы файл закрывается. В качестве примера приведем программу, распечатывающую в своем окне содержимое текстового файла 'MyFile.txt':

program Console;

{$APPTYPE CONSOLE}

uses
  SysUtils;

var
  F: TextFile;
  S: string;

begin
  AssignFile(F, 'MyFile.txt');
  Reset(F);
  while not Eof(F) do
  begin
    Readln(F, S);
    Writeln(S);
  end;
  CloseFile(F);
  Writeln('Press Enter to exit...');
  Readln;
end.

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

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

Стандартные подпрограммы управления файлами

Для обработки файлов в языке Delphi имеется специальный набор процедур и функций:

Для работы с нетипизированными файлами используются процедуры BlockRead и BlockWrite. Единица обмена

Указатели

Понятие указателя

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

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

var
  P: Pointer; // переменная-указатель
  N: Integer; // целочисленная переменная

Переменная P занимает 4 байта и может содержать адрес любого участка памяти, указывая на байты со значениями любых типов данных: Integer, Real, string, record, array и других. Чтобы инициализировать переменную P, присвоим ей адрес переменной N. Это можно сделать двумя эквивалентными способами:

P := Addr(N); // с помощью вызова встроенной функции Addr

или

P := @N;      // с помощью оператора @

В дальнейшем мы будем использовать более краткий и удобный второй способ.

Если некоторая переменная P содержит адрес другой переменной N, то говорят, что P указывает на N. Графически это обозначается стрелкой, проведенной из P в N (рисунок 12 выполнен в предположении, что N имеет значение 10):

Рисунок 12. Графическое изображение указателя P на переменную N

Рисунок 12. Графическое изображение указателя P на переменную N

Теперь мы можем изменить значение переменной N, не прибегая к идентификатору N. Для этого слева от оператора присваивания запишем не N, а P вместе с символом ^:

P^ := 10; // Здесь умышленно опущено приведение типа

Символ ^, записанный после имени указателя, называется оператором доступа по адресу. В данном примере переменной, расположенной по адресу, хранящемуся в P, присваивается значение 10. Так как в переменную P мы предварительно занесли адрес N, данное присваивание приводит к такому же результату, что и

N := 10;

Однако в примере с указателем мы умышленно допустили одну ошибку. Дело в том, что переменная типа Pointer может содержать адреса переменных любого типа, не только Integer. Из-за сильной типизации языка Delphi перед присваиванием мы должны были бы преобразовать выражение P^ к типу Integer:

Integer(P^) := 10;

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

var
  P: ^Integer;

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

type
  PInteger = ^Integer;
var
  P: PInteger;

PInteger — это указательный тип данных. Чтобы отличать указательные типы данных от других типов, будем назначать им идентификаторы, начинающиеся с буквы P (от слова Pointer). Объявление указательного типа данных является единственным способом введения указателей на составные переменные, такие как массивы, записи, множества и другие. Например, объявление типа данных для создания указателя на некоторую запись TPerson может выглядеть так:

type
  PPerson = ^TPerson;
  TPerson = record
    FirstName: string[20]; 
    LastName: string[20]; 
    BirthYear: Integer; 
  end;

var
  P: PPerson;

Переменная P, описанная с типом данных PPerson, является указателем и может содержать адрес любой переменной типа TPerson. Впредь все указатели мы будем вводить через соответствующие указательные типы данных. Типом Pointer будем пользоваться лишь тогда, когда это действительно необходимо или оправдано.

Динамическое распределение памяти

После объявления в секции var указатель содержит неопределенное значение. Поэтому переменные-указатели, как и обычные переменные, перед использованием нужно инициализировать. Отсутствие инициализации указателей является наиболее распространенной ошибкой среди новичков. Причем если использование обычных неинициализированных переменных приводит просто к неправильным результатам, то использование неинициализированных указателей обычно приводит к ошибке "Access violation" (доступ к неверному адресу памяти) и принудительному завершению приложения.

Один из способов инициализации указателя состоит в присваивании ему адреса некоторой переменной соответствующего типа. Этот способ мы уже рассмотрели. Второй способ состоит в динамическом выделении участка памяти под переменную соответствующего типа и присваивании указателю его адреса. Работа с динамическими переменными и есть основное назначение указателей. Размещение динамических переменных производится в специальной области памяти, которая называется Heap (куча). Ее размер равен размеру свободной памяти компьютера.

Для размещения динамической переменной вызывается стандартная процедура

New(var P: Pointer);

Она выделяет требуемый по размеру участок памяти и заносит его адрес в переменную-указатель P. В следующем примере создаются 4 динамических переменных, адреса которых присваиваются переменным-указателям P1, P2, P3 и P4:

program Console;

{$APPTYPE CONSOLE}

uses
  SysUtils;

type
  PInteger = ^Integer;
  PDouble = ^Double;
  PShortString = ^ShortString;

var
  P1, P2: PInteger;
  P3: PDouble;
  P4: PShortString;

begin
  New(P1);
  New(P2);
  New(P3);
  New(P4);
  ...
end.

Далее по адресам в указателях P1, P2, P3 и P4 можно записать значения:

P1^ := 10;
P2^ := 20;
P3^ := 0.5;
P4^ := 'Hello!';

В таком контексте динамические переменные P1^, P2^, P3^ и P4^ ничем не отличаются от обычных переменных соответствующих типов. Операции над динамическими переменными аналогичны подобным операциям над обычными переменными. Например, следующие операторы могут быть успешно откомпилированы и выполнены:

if P1^ < P2^ then
  P1^ := P1^ + P2^; // в P1^ заносится 30
P3^ := P1^;         // в P3^ заносится 30.0

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

Dispose(var P: Pointer);

Например, в приведенной выше программе явно не хватает следующих строк:

Dispose(P4);
Dispose(P3);
Dispose(P2);
Dispose(P1);

После выполнения данных утверждений указатели P1, P2, P3 и P4 опять перестанут быть связаны с конкретными адресами памяти. В них будут случайные значения, как и до обращения к процедуре New. Не стоит делать попытки присвоить значения переменным P1^, P2^, P3^ и P4^, ибо в противном случае это может привести к нарушению нормальной работы программы.

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

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

Операции над указателями

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

P3^ := 20;
P1^ := 50;
P3 := P1;  // теперь P3^ = 50

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

Использование одинаковых значений в разных указателях открывает некоторые интересные возможности. Так после оператора P3 := P1 изменение значения переменной P3^ будет равносильно изменению значения P1^.

P3^ := 70; // теперь P3^ = P1^ = 70

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

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

if P1 = P2 then ... // Указатели ссылаются на одни и те же данные

if P1 <> P2 then ... // Указатели ссылаются на разные данные

Чаще всего операции сравнения указателей используются для проверки того, связан ли указатель с динамической переменной. Если еще нет, то ему следует присвоить значение nil (зарезервированное слово):

P1 := nil;

Установка P1 в nil однозначно говорит о том, что указателю не выделена динамическая память. Если всем объявленным указателям присвоить значение nil, то внутри программы можно легко выполнить тестирование наподобие этого:

if P1 = nil then New(P1);

или

if P1 <> nil then Dispose(P1);

Процедуры GetMem и FreeMem

Для динамического распределения памяти служат еще две тесно взаимосвязанные процедуры: GetMem и FreeMem. Подобно New и Dispose, они во время вызова выделяют и освобождают память для одной динамической переменной:

Пример:

New(P4);     // Выделить блок памяти для указателя P4
...
Dispose(P4); // Освободить блок памяти

Следующий отрывок программы даст тот же самый результат:

GetMem(P4, SizeOf(ShortString)); // Выделить блок памяти для P4
...
FreeMem(P4);                     // Освободить блок памяти

С помощью процедуры GetMem одной переменной-указателю можно выделить разное количество памяти в зависимости от потребностей. В этом состоит ее основное отличие от процедуры New.

GetMem(P4, 20); // Выделить блок в 20 байт для указателя P4
...
FreeMem(P4);    // Освободить блок памяти

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

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

ReallocMem(var P: Pointer; Size: Integer) — освобождает блок памяти по значению указателя P и выделяет для указателя новый блок памяти заданного размера Size. Указатель P может иметь значение nil, а параметр Size — значение 0, что влияет на работу процедуры:

Представление строк в памяти

В некоторых случаях динамическая память неявно используется программой, например для хранения строк. Длина строки может варьироваться от нескольких символов до миллионов и даже миллиардов (теоретический предел равен 2 ГБ). Тем не менее, работа со строками в программе осуществляется так же просто, как работа с переменными простых типов данных. Это возможно потому, что компилятор автоматически генерирует код для выделения и освобождения динамической памяти, в которой хранятся символы строки. Но что стоит за такой простотой? Не идет ли она в ущерб эффективности? С полной уверенностью можем ответить, что эффективность программы не только не снижается, но даже повышается.

Физически переменная строкового типа представляет собой указатель на область динамической памяти, в которой размещаются символы. Например, переменная S на самом деле представляет собой указатель и занимает всего четыре байта памяти (SizeOf(S) = 4):

var
  S: string; // Эта переменная физически является указателем

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

SetLength(S, 100); // S получает адрес распределенного блока динамической памяти

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

S2 := S1; // Копируются лишь адреса

Такой подход весьма эффективен как с точки зрения производительности, так и с точки зрения экономного использования оперативной памяти. Его главная проблема состоит в том, чтобы обеспечить удаление блока памяти, содержащего символы строки, когда все адресующие его строковые переменные прекращают свое существование. Эта проблема эффективно решается с помощью механизма подсчета количества ссылок (reference counting). Для понимания его работы рассмотрим формат хранения строк в памяти подробнее.

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

var
  S1, S2: string; // Физически эти переменные являются указателями

И пусть в программе существует оператор, присваивающий переменной S1 значение некоторой функции:

Readln(S1); // В S1 записывается адрес считанной строки

Для хранения символов строки S1 по окончании ввода будет выделен блок динамической памяти. Формат этого блока после ввода значения 'Hello' показан на рисунке 13:

Рисунок 13. Представление строковых переменных в памяти

Рисунок 13. Представление строковых переменных в памяти

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

Если в программе встречается оператор присваивания значения одной строковой переменной другой строковой переменной,

S2 := S1; // Теперь S2 указывает на тот же блок памяти, что и S1

то, как мы уже сказали, копия строки в памяти не создается. Копируется только адрес, хранящийся в строковой переменной, и на единицу увеличивается количество ссылок на строку (рисунок 14).

Рисунок 14. Результат копирования строковой переменной S1 в строковую переменную S2

Рисунок 14. Результат копирования строковой переменной S1 в строковую переменную S2

При присваивании переменной S1 нового значения (например, пустой строки):

S1 := '';

количество ссылок на предыдущее значение уменьшается на единицу (рисунок 15).

Рисунок 15. Результат присваивания строковой переменной S1 нового значения (пустой строки)

Рисунок 15. Результат присваивания строковой переменной S1 нового значения (пустой строки)

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

Интересно, а что происходит при изменении символов строки, с которой связано несколько строковых переменных? Правила семантики языка требуют, чтобы две строковые переменные были логически независимы, и изменение одной из них не влияло на другую. Это достигается с помощью механизма копирования при записи (copy-on-write).

Например, в результате выполнения операторов

  S1 := S2;                // S1 указывает на ту же строку, что и S2
  S1[3] := '-';            // Автоматически создается копия строки

получим следующую картину в памяти (рисунок 16):

Рисунок 16. Результат изменения символа в строке S1

Рисунок 16. Результат изменения символа в строке S1

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

Все, что было сказано выше о представлении в памяти строк, относится только к строкам формата AnsiString. Строки формата WideString тоже хранятся в динамической памяти, но для них не поддерживаются механизм подсчета количества ссылок и механизм копирования по записи. Операция присваивания строковых переменных формата WideString означает выделение нового блока динамической памяти и полное копирование в него всех символов исходной строки. Что же касается коротких строк, то они целиком хранятся по месту объявления: или в области данных программы (если это глобальные переменные), или на стеке (если это локальные переменные). Динамическая память вообще не используется для хранения коротких строк.

Динамические массивы

Одним из мощнейших средств языка Delphi являются динамические массивы. Их основное отличие от обычных массивов заключается в том, что они хранятся в динамической памяти. Этим и обусловлено их название. Чтобы понять, зачем они нужны, рассмотрим пример:

var
  N: Integer;
  A: array[1..100] of Integer; // обычный массив
begin
  Write('Введите количество элементов: ');
  ReadLn(N);
  ...
end.

Задать размер массива A в зависимости от введенного пользователем значения невозможно, поскольку в качестве границ массива необходимо указать константные значения. А введенное пользователем значение никак не может претендовать на роль константы. Иными словами, следующее объявление будет ошибочным:

var
  N: Integer;
  A: array[1..N] of Integer; // Ошибка!
begin
  Write('Введите количество элементов: ');
  ReadLn(N);
  ...
end.

На этапе написания программы невозможно предугадать, какие именно объемы данных захочет обрабатывать пользователь. Тем не менее, Вам придется ответить на два важных вопроса:

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

const
  MaxNumberOfElements = 100;
var
  N: Integer;
  A: array[1.. MaxNumberOfElements] of Integer;
begin
  Write('Введите количество элементов (не более ', MaxNumberOfElements, '): ');
  ReadLn(N);
  if N > MaxNumberOfElements then
  begin
    Write('Извините, программа не может работать ');
    Writeln('с количеством элементов больше , ' MaxNumberOfElements, '.');
  end
  else
  begin
    ... // Инициализируем массив необходимыми значениями и обрабатываем его
  end;
end.

Такое решение проблемы является неоптимальным. Если пользователю необходимо всего 10 элементов, программа работает без проблем, но всегда использует объем памяти, необходимый для хранения 100 элементов. Память, отведенная под остальные 90 элементов, не будет использоваться ни Вашей программой, ни другими программами (по принципу "сам не гам и другому не дам"). А теперь представьте, что все программы поступают таким же образом. Эффективность использования оперативной памяти резко снижается.

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

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

var
  DynArray: array of Integer;

Переменная DynArray представляет собой ссылку на размещаемые в динамической памяти элементы массива. Изначально память под массив не резервируется, количество элементов в массиве равно нулю, а значение переменной DynArray равно nil.

Работа с динамическими массивами напоминает работу с длинными строками. В частности, создание динамического массива (выделение памяти для его элементов) осуществляется той же процедурой, которой устанавливается длина строк — SetLength.

SetLength(DynArray, 50);       // Выделить память для 50 элементов

Изменение размера динамического массива производится этой же процедурой:

SetLength(DynArray, 100);      // Теперь размер массива 100 элементов

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

При уменьшении размера динамического массива лишние элементы теряютяся.

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

Определение количества элементов производится с помощью функции Length:

N := Length(DynArray);          // N получит значение 100

Элементы динамического массива всегда индексируются от нуля. Доступ к ним ничем не отличается от доступа к элементам обычных статических массивов:

DynArray[0] := 5;               // Присвоить начальному элементу значение 5
DynArray[High(DynArray)] := 10; // присвоить конечному элементу значение 10

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

Освобождение памяти, выделенной для элементов динамического массива, осуществляется установкой длины в значение 0 или присваиванием переменной-массиву значения nil (оба варианта эквивалентны):

SetLength(DynArray, 0); // Эквивалентно: DynArray := nil;

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

Также, как и при работе со строками, при присваивании одного динамического массива другому, копия уже существующего массива не создается.

var
  A, B: array of Integer;
begin
  SetLength(A, 100);  // Выделить память для 100 элементов
  A[0] := 5;
  B := A;             // A и B указывают на одну и ту же область памяти!
  B[1] := 7;          // Теперь A[1] тоже равно 7!
  B[0] := 3;          // Теперь A[0] равно 3, а не 5!
end.

В приведенном примере, в переменную B заносится адрес динамической области памяти, в которой хранятся элементы массива A (другими словами, ссылочной переменной B присваивается значение ссылочной переменной A).

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

var
  A, B: array of Integer;
begin
  SetLength(A, 100); // Выделить память для 100 элементов
  A[0] := 10;
  B := A;            // B указывает на те же элементы, что и A
  A := nil;          // Память еще не освобождается, поскольку на нее указывает B
  B[1] := 5;         // Продолжаем работать с B, B[0] = 10, а B[1] = 5
  B := nil;          // Теперь ссылок на блок памяти нет. Память освобождается
end;

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

Не смотря на сильное сходство динамических массивов со строками, у них имеется одно существенное отличие: отсутствие механизма копирования при записи (copy-on-write).

Нуль-терминированные строки

Кроме стандартных строк ShortString и AnsiString, в языке Delphi поддерживаются нуль-терминированные строки языка C, используемые процедурами и функциями Windows. Нуль-терминированная строка представляет собой индексированный от нуля массив ASCII-символов, заканчивающийся нулевым символом #0. Для поддержки нуль-терминированных строк в языке Delphi введены три указательных типа данных:

type
  PAnsiChar = ^AnsiChar;
  PWideChar = ^WideChar;
  PChar = PAnsiChar;

Типы PAnsiChar и PWideChar являются фундаментальными и на самом деле используются редко. PChar — это обобщенный тип данных, в основном именно он используется для описания нуль-терминированных строк.

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

const
  S1: PChar = 'Object Pascal';               // #0 дописывается автоматически
  S2: array[0..12] of Char = 'Delphi/Kylix'; // #0 дописывается автоматически
var
  S3: PChar;

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

S3 := S1;

переменная S3 получит адрес уже существующей строки 'Object Pascal'.

Для удобной работы с нуль-терминированными строками в языке Delphi предусмотрена директива $EXTENDEDSYNTAX. Если она включена (ON), то появляются следующие дополнительные возможности:

В режиме расширенного синтаксиса допустимы, например, следующие операторы:

S3 := S2;     // S3 указывает на строку 'Delphi/Kylix'
S3 := S1 + 7; // S3 указывает на подстроку 'Pascal'

В языке Delphi существует богатый набор процедур и функций для работы с нуль-терминированными строками (см. справочник по среде Delphi).

Переменные с непостоянным типом значений

Тип данных Variant

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

program Console;

{$APPTYPE CONSOLE}

uses
  SysUtils;

var
  V1, V2, V3, V4: Variant;

begin
  V1 := 5;             // целое число
  V2 := 0.8;           // вещественное число
  V3 := '10';          // строка
  V4 := V1 + V2 + V3;  // вещественное число 15.8
  Writeln(V4);         // 15.8
  Writeln('Press Enter to exit...');
  Readln;
end.

Значения переменных с типом Variant

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

Значение Unassigned показывает, что переменная является нетронутой, т.е. переменной еще не присвоено значение. Оно автоматически устанавливается в качестве начального значения любой переменной с типом Variant.

Значение Null показывает, что переменная имеет неопределенное значение. Если в выражении участвует переменная со значением Null, то результат всего выражения тоже равен Null.

Переменная с типом Variant занимает в памяти 16 байт. В них хранятся текущее значение переменной (или адрес значения в динамической памяти) и тип этого значения.

Тип значения выясняется с помощью функции

VarType(const V: Variant): Integer;

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

if VarType(V) and varTypeMask = varString then ...

Код типа

Значение

Описание

varEmpty$0000Переменная содержит значение Unassigned.
varNull$0001Переменная содержит значение Null.
varSmallint$0002Переменная содержит значение типа Smallint.
varInteger$0003Переменная содержит значение типа Integer.
varSingle$0004Переменная содержит значение типа Single.
varDouble$0005Переменная содержит значение типа Double.
varCurrency$0006Переменная содержит значение типа Currency.
varDate$0007Переменная содержит значение типа TDateTime.
varOleStr$0008Переменная содержит ссылку на строку формата Unicode в динамической памяти.
varDispatch$0009Переменная содержит ссылку на интерфейс IDispatch (интерфейсы рассмотрены в главе 6).
varError$000AПеременная содержит системный код ошибки.
varBoolean$000BПеременная содержит значение типа WordBool.
varVariant$000CЭлемент варьируемого массива содержит значение типа Variant (код varVariant используется только в сочетании с флагом varArray).
varUnknown$000DПеременная содержит ссылку на интерфейс IUnknown (интерфейсы рассмотрены в главе 6).
varShortint$0010Переменная содержит значение типа Shortint
varByte$0011Переменная содержит значение типа Byte.
varWord$0012Переменная содержит значение типа Word
varLongword$0013Переменная содрежит значение типа Longword
varInt64$0014Переменная содержит значение типа Int64
varStrArg$0048Переменная содержит строку, совместимую со стандартом COM, принятым в операционной системе Windows.
varString$0100Переменная содержит ссылку на длинную строку.
varAny$0101Переменная содержит значение любого типа данных технологии CORBA
Флаги
varTypeMask$0FFFМаска для выяснения типа значения.
varArray$2000Переменная содержит массив значений.
varByRef$4000Переменная содержит ссылку на значение.

Таблица 10. Коды и флаги варьируемых переменных

Функция

VarAsType(const V: Variant; VarType: Integer): Variant; 

позволяет вам преобразовать значение варьируемой переменной к нужному типу, например:

V1 := '100';
V2 := VarAsType(V1, varInteger);

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

Delphi + ассемблер

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

Встроенный ассемблер

Пользователю предоставляется возможность делать вставки на встроенном ассемблере в исходный текст на языке Delphi.

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

asm
  <оператор ассемблера>
  ...
  <оператор ассемблера>
end;

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

В языке Delphi имеется возможность не только делать ассемблерные вставки, но писать процедуры и функции полностью на ассемблере. В этом случае тело подпрограммы ограничивается словами asm и end (а не begin и end), между которыми помещаются инструкции ассемблера. Перед словом asm могут располагаться объявления локальных констант, типов, и переменных. Например, вот как могут быть реализованы функции вычисления минимального и максимального значения из двух целых чисел:

function Min(A, B: Integer): Integer; register;
asm
  CMP    EDX, EAX
  JGE    @@1
  MOV    EAX, EDX
  @@1:
end;

function Max(A, B: Integer): Integer; register;
asm
  CMP    EDX, EAX
  JLE    @@1
  MOV    EAX, EDX
  @@1:
end;

Обращение к этим функциям имеет привычный вид:

Writeln(Min(10, 20));
Writeln(Max(10, 20));

Подключение внешних подпрограмм

Программисту предоставляется возможность подключать к программе или модулю отдельно скомпилированные процедуры и функции, написанные на языке ассемблера или C. Для этого используется директива компилятора $LINK и зарезервированное слово external. Директива {$LINK <имя файла>} указывает подключаемый объектный модуль, а external сообщает компилятору, что подпрограмма внешняя.

Предположим, что на ассемблере написаны и скомпилированы функции Min и Max, их объектный код находится в файле MINMAX.OBJ. Подключение функций Min и Max к программе на языке Delphi будет выглядеть так:

function Min(X, Y: Integer): Integer; external;
function Max(X, Y: Integer): Integer; external;
{$LINK MINMAX.OBJ}

В модулях внешние подпрограммы подключаются в разделе implementation.

Итоги

Все, что вы изучили, называется языком Delphi. Мы надеемся, что вам понравились стройность и выразительная сила языка. Но это всего лишь основа. Теперь пора подняться на следующую ступень и изучить технику объектно-ориентированного программирования, без которого немыслимо стать профессиональным программистом. Именно этим вопросом в рамках применения объектов в среде Delphi мы и займемся в следующей главе.

следующая статья серии

Дополнительная информация

За дополнительной информацией обращайтесь в компанию Interface Ltd.

Обсудить на форуме Borland

Рекомендовать страницу

INTERFACE Ltd.
Телефон/Факс: +7 (495) 925-0049
Отправить E-Mail
http://www.interface.ru
Rambler's Top100
Ваши замечания и предложения отправляйте редактору
По техническим вопросам обращайтесь к вебмастеру
Дата публикации: 26.02.06