Програмування на мові Delphi. Глава 2. Основи мови Delphi. Частина 4, Різне, Програмування, статті

попередня стаття серії


Зміст



Файли


Поняття файлу


З точки зору користувача файл – це іменована область даних на диску або будь-якому іншому зовнішньому носії. У програмі файл постає як послідовність елементів деякого типу. Так як розмір одного файлу може перевищувати обсяг всієї оперативної пам'яті комп'ютера, доступ до його елементів виконується послідовно за допомогою процедур читання і запису.

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

Залежно від типу елементів розрізняють три види файлу:


Для роботи з файлом у програмі оголошується файлова змінна. У файлової змінної запам'ятовується ім'я файлу, режим доступу (наприклад, тільки читання), інші атрибути. Залежно від виду файлу файлова змінна описується по-різному.

Для роботи з файлом, що складається з типових елементів мінлива оголошується за допомогою словосполучення 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

Тепер ми можемо змінити значення змінної 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. Подання строкових змінних в пам'яті

Як можна помітити, блок динамічної пам'яті, виділений для зберігання символів рядка, додатково містить два поля, розташованих перед першим символом (по негативним зсувам щодо строкового покажчика). Перше поле зберігає кількість посилань на даний рядок, а друге – довжину рядка.

Якщо в програмі зустрічається оператор присвоювання значення однієї строкової змінної інший строкової змінної,

 S2: = S1; / / Тепер S2 вказує на той же блок пам'яті, що і S1

те, як ми вже сказали, копія рядка в пам'яті не створюється. Копіюється тільки адресу, що зберігається в строкової змінної, і на одиницю збільшується кількість посилань на рядок (рисунок 14).

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

При присвоєнні змінної S1 нового значення (наприклад, порожній рядок):

S1 := ;

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

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

Блок динамічної пам'яті звільняється, коли кількість посилань на рядок стає рівним нулю. Цим забезпечується автоматичне звільнення невикористовуваної пам'яті.

Цікаво, а що відбувається при зміні символів рядка, з якою пов'язано кілька строкових змінних? Правила семантики мови вимагають, щоб дві рядкові змінні були логічно незалежні, і зміна однієї з них не впливало на іншу. Це досягається за допомогою механізму копіювання при запису (copy-on-write).

Наприклад, в результаті виконання операторів

 S1: = S2; / / S1 вказує на ту ж рядок, що і S2
S1 [3]: = -; / / Автоматично створюється копія рядки

отримаємо таку картину в пам'яті (малюнок 16):

Малюнок 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 Мінлива мiстить посилання на рядок формату 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 Мінлива мiстить посилання на довгий рядок.
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 ми і займемося в наступному розділі.

наступна стаття серії


Посилання по темі



Схожі статті:


Сподобалася стаття? Ви можете залишити відгук або підписатися на RSS , щоб автоматично отримувати інформацію про нові статтях.

Коментарів поки що немає.

Ваш отзыв

Поділ на параграфи відбувається автоматично, адреса електронної пошти ніколи не буде опублікований, допустимий HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

*