Довгі рядки і динамічні масиви в Delphi, Комерція, Різне, статті

Зміст



Серед типів змінних в Delphi є кілька типів, що істотно відрізняються від звичайних.






ПРИМІТКА

У даній статті мова по суті йде про Delphi версії 7 і нижче. В Delphi.NET все, що пов’язано з масивами і рядками засноване на. NET Framework і можна говорити швидше про емуляції поведінки масивів і рядків. Їх реалізація ж зовсім відрізняється від того, що було в Delphi минулих версій. – Прим.ред


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

Розглянемо наступний код:





procedure E1()
var
I: integer;
S: string;
A: TIntegerDynArray;
begin / / Перед привласненням …
end;

Реалізований компілятором код виглядає приблизно так:





procedure E1_D()
var
I: integer;
S: string;
A: TIntegerDynArray;
begin
Pointer(S) := nil;
Pointer(A) := nil; / / Перед привласненням …
_DynArrayClear(A, typeinfo(TIntegerDynArray));
_LStrClr(S);
end;

Що показує наведений приклад:


  1. Мінлива I, має значення типу Integer, розташована в стеці і містить випадкове сміття. На відміну від змінної простого типу, для змінних S і A (Що є покажчиками, також розташовуються в стеку), компілятор завжди вставляє код, ініціалізувалися їх в nil.
  2. При присвоєнні довгим рядкам значень або зміну розміру масиву компілятор вставляє код, динамічно виділяє область пам’яті, і привласнює покажчик на неї цієї змінної. Тобто саме вміст рядки або масиву розташовується в динамічної пам’яті.
  3. Перед виходом із процедури компілятор вставляє спеціальні функції фіналізації, відповідальні за звільнення виділеної динамічної пам’яті.
  4. І, нарешті, компілятор замінює майже всі операції з цими змінними на свої системні функції.


Реалізація


На початку виділеної області пам’яті міститься заголовок, який використовується системними функціями Delphi (визначений у System.pas) Для управління змінними обговорюваних типів. Заголовок описується записом TPvsDynRec. Ось її опис:





Type
TPvsDynRec = packed record
{$IFDEF MSWINDOWS}
sizeFlags: Integer;
{$ENDIF}
refCnt: Integer;
length: Integer;
end;
PPvsDynRec = ^TPvsDynRec;

Для рядків і масивів використання полів цієї структури дещо відрізняється, але сам принцип однаковий. Ось опис цих полів:



Присвоєння і передача в якості параметра


Призначення лічильника посилань – відстежити момент, коли потрібно звільнити пам’ять, і уникнути зайвого копіювання. При присвоєнні змінних, що мають тип динамічного масиву, значення не копіюються, а просто копіюється покажчик. При цьому лічильник збільшується на 1. При зміні рядка або розміру динамічного масиву, перевіряється значення лічильника і якщо він більше 1, то створюється нова копія і вже з нею проводяться зміни.

При виході змінної з області видимості або виклику деструктора об’єкта, вкотором розташована змінна, викликається фіналізація, яка зменшує лічильник і, якщо він дорівнює 0, Пам’ять звільняється.

При передачі динамічного масиву у функцію або процедуру, Delphi, в залежності від типу передачі, вставляє системні функції, що управляють лічильником і фіналізацією. При передачі по значенню лічильник збільшується. Передача за посиланням (var) Або константи (const) Не впливає на лічильник. Так що передача як константи (const) Не тільки робить код більш безпечним, а й оптимізує його.

У зв’язку з деякими відмінностями в поведінці лічильника посилань для рядків і масивів, а також з особливими властивостями масивів, необхідно розглянути ці структури окремо.

Довгі рядки


Особливістю довгих рядків є можливість присвоювання константного літерала. В залежності від того, є змінна локальної чи ні, генерується різний код.

Якщо відбувається присвоювання локальної змінної:





 procedure E2;
var
S: string;
begin
S := “String”; // refCnt = -1
end;

Мінлива S отримає лічильник рівний -1 і буде посилатися прямо на літерал. При присвоєнні інший локальної змінної або передачі в процедуру лічильник ніколи не змінюється. Природно, ні про яке управлінні пам’яттю в даному випадку мова не йде.

В інших випадках (тобто коли рядок міститься в глобальну змінну, поле, елемент масиву і т.п.) Delphi створює звичайну рядок в динамічної пам’яті, в яку копіює привласнюється константу.

Будь-яка зміна рядка замінюється на виклик системних функцій. Якщо лічильник рядки в цей момент не дорівнює 1, Створюється новий рядок. Винятком з цього правила є доступ до вмісту рядка за вказівником (приведення до типу PChar). В цьому випадку вже нічого не контролюється. Нижче наведено два приклади, ілюструють таку поведінку.





procedure E3;
var
S1, S2, S3: string;
begin
S1 := “q”; // refCnt = -1
S2 := S1 + “w”; // refCnt = 1
S3 := S2; / / Зараз в пам’яті розташовуються 2 рядки / / “Q” refCnt = -1 (посилання на яку міститься в S1) / / “Qw” refCnt = 2 (посилання на яку міститься в S2, S3)
S3[1] := “1”; / / А зараз вже три різні рядки / / “Q”; refCnt = -1 (посилання на яку міститься в S1) / / “Qw” refCnt = 1 (посилання на яку міститься в S2) / / “1w” refCnt = 1 (посилання на яку міститься в S3)
end;
procedure E4;
var
S1, S2, S3: string;
begin
S1 := “q”; // refCnt = -1
S2 := S1 + “w”; // refCnt = 1
S3 := S2; / / Зараз в пам’яті розташовуються 2 рядки
// S1 = “q” refCnt = -1
// S2 = S3 = “qw” refCnt = 2
PChar(S3)[0] := “1”; / / Зараз також два рядки
// S1 = “q”; refCnt = -1 / / S2 = S3 = “1w” refCnt = 2 тобто змінилися обидві рядки
end;

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





procedure E5;
var
S: string;
begin
S := “qqqqqqq”; // refCnt = -1
PChar(S)[0] := “1”;
end;

Це код правильно скомпілюйте, але при виконанні видасть помилку порушення доступу. Причина в тому, що рядок S (refCnt = -1) Знаходиться в сегменті пам’яті, захищеному від запису.

Тому здавалося б, нешкідлива процедура:





procedure E6(S: string)
begin
if length(S) > 0 then
PChar(S)[0] := “q”;
//…
end;

викличе помилку порушення доступу при передачі в неї рядки з refCnt = -1.

Щоб отримати унікальну посилання для рядка, що складається з деякої послідовності символів, можна скористатися функцією UniqueString. Це дозволяє прискорити обчислення з рядками, так як при цьому можна буде порівнювати рядки, просто порівнюючи покажчики на них. У таких рядків refCnt завжди дорівнює 1.

Динамічні масиви


На відміну від рядків, динамічний масив не може инициализироваться літералами, тому він не може мати refCnt, рівний -1. Причому Delphi вже не контролює зміна вмісту масиву, а створює новий масив тільки при спробі зміни його розміру (причому навіть якщо сам розмір не змінюється), якщо refCnt > 1.





procedure E7;
var
A1, A2, A3: TIntegerDynArray;
begin
SetLength(A1, 1);
A1[0] := 5;
A2 := A1;
A3 := A1; / / В цей момент існує один примірник масиву, на який посилаються / / Змінні A1, A2 і A3. Елемент з індексом 0 містить значення 5. / / RefCnt цього масиву дорівнює 3.
A1[0] := 1; / / Елемент 0 масиву встановлюється в 1, refCnt при цьому не змінюється.
SetLength(A1, 1); / / Попередня рядок створює копію масиву. При цьому A1 вказує на цю / / Копію, а A2 і A3 вказують на вихідний примірник.
A1[0] := 2; / / Попередня рядок змінює значення нульової комірки копії масиву. / / Вихідний масив, на який посилаються А2 і А3, не змінюється.
end;

У цьому сенсі поведінка масиву і рядки принципово відрізняється. Для ілюстрації цієї відмінності розглянемо дві процедури:





procedure E8(S: string);
begin
if Length(S) > 0 then
S[1] := “t”;
end;
procedure E9(A: TIntegerDynArray);
begin
if Length(A) > 0 then
A[0] := 100;
end;

Після виконання E8 передана рядок не зміниться, так як при S[1] := ‘t’ буде створена нова рядок. Виклик ж E9 призведе до зміни вмісту масиву.

Розглянемо більш цікавий приклад. Нехай масив A не порожній.





procedure E10(A: TIntegerDynArray);
begin
A[0] := 100;
SetLength(A, Length(A));
end;
procedure E11(A: TIntegerDynArray);
begin
SetLength(A, Length(A));
A[0] := 100;
end;

На перший погляд, після виконання будь-якої з цих процедур результат буде однаковий, але це не так. З першої процедури повернеться змінений масив, а з другої незмінений. Тут дуже важливий порядок.

Розглянемо, чому так відбувається. При виклику будь-якої з цих процедур A – локальна змінна на стеку, яка вказує на переданий масив. Нехай вхідний масив A = [0,1,2,3,4].





procedure E10_D(A: TIntegerDynArray);
begin A [0]: = 100; / / змінює вміст масиву A [100,1,2,3,4]
SetLength(A, Length(A)); / / Так як лічильник посилань явно більше 1, / / A тепер посилається на новий масив, копіюючи в нього вміст старого. / / З цього моменту будь-які зміни масиву A ніяк не позначаться на / / Вхідному масиві, а сам новий масив буде звільнений в кінці процедури. A [1]: = 10; / / змінює вміст вже нового масиву A [100,10,3,4]
end;

Основна різниця рядків і масивів в контролі змін вмісту, якщо для рядків будь-яка зміна замінюється викликом системної процедури, яка контролює лічильник посилань, то звернення до елемента масиву виконується, як є. І тільки зміна розміру масиву контролює лічильник посилань. Мабуть творці Delphi, намагалися зберегти ефективність доступу до елементів масиву, і тому вийшла така різниця в поведінці.

Ця різниця особливо помітна при передачі як константним параметром:





procedure E12(const S: string; const A: TIntegerDynArray);
begin S [1]: = ‘q’; / / помилка компіляції S: = ‘qqqq’; / / помилка компіляції A [0]: = 5; / / можна!!! хоча формально заборонено SetLength (A, 5); / / помилка компіляції
end;

Локальна змінна Result


Хоча Result є реальна локальна змінна, її реалізація в питанні ініціалізації дещо відрізняється від вищесказаного. Причиною є те, що вона створюється в викликає процедурою, а в спричинюється функцію передається як прихований параметр. Розглянемо приклад:





function E13: string;
begin
Result := Result + “w”;
end;
procedure E14;
var
S: string;
begin
S := “q”;
S := E13;
// S = “qw”
end;

Результат може здатися несподіваним. Реалізований Delphi код виглядає приблизно так:





function E13_D(var Result: string);
begin
Result := Result + “w”;
end;
procedure E14_D;
var
S: string;
begin
S := “q”;
E13_D(S);
// S = “qw”
end;

В даному випадку сама мінлива S використовується як Result.

Дещо інший приклад працює вже, як і очікується.





procedure E15;
var
S: string;
begin
S := “q”;
S := S + E13;
// S = “qw”
end;

Але реалізований код аналогічний, тільки вводиться тимчасова змінна Tmp виконуюча роль Result





procedure E15_D;
var
S: string;
Tmp: string;
begin
S := “q”;
ResultStr1(Tmp);
S := S + Tmp;
// S = “qw”
end;

Чому поведінка настільки різна? По перше, локальна змінна Result реалізується і ініціалізується в nil в зухвалому функції і передається в спричинюється функцію по посиланню. По друге, сама викликається функція її вже не ініціалізує. З цієї причини, Result як локальна змінна, при вході у функцію може містити будь-яке значення.




ПОПЕРЕДЖЕННЯ

Але покладатися на використання початкового значення Result небезпечно, як показано в наведених прикладах, при різних викликах однієї і тієї ж функції ви можете отримати різні результати.


Змінні типу рядка і динамічні масиви таки покажчики


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

Наприклад:





var
S: string;
procedure E16(const Str: string);
begin
S := ‘www’; / / Str -??????????? Пам’ять звільнена
end;
procedure E17;
begin
S := ‘qqqq’;
E16(S);
end;

Яке значення Str при виклику E16(S) з E17? Формально, раз параметр передається в процедуру E16 за значенням, а тим більше як константа, то зміна глобальної змінної S нічого не повинно змінити. Але так як параметр є тільки покажчик, то зміна глобальної змінної S створить новий рядок, а пам’ять під старою буде звільнена (лічильник посилань дорівнює 1). У даному прикладі це неминуче призведе до помилки доступу до пам’яті. Можна привести безліч подібних прикладів, так що слід краще покладатися на знання реалізації таких типів даних, ніж на формальні правила мови.

Зберігання в змінних іншого типу


Розглянемо код:





var
P: Pointer;
procedure E18;
var
S: string;
begin
S := “qqq”;
S[1] := “w”;
P := Pointer(S);
end;

Питається, куди вказує P після виконання процедури E18? Природно на сміття, так як пам’ять під рядок S по виході з процедури буде звільнена. З цієї ж причини компілятор не дозволяє поміщати довгі рядки і динамічні масиви в варіантну частину записи, він просто не знає, що реально там буде і не може вставити код для управління посиланнями і пам’яттю.

Тим не менш, іноді виникає така необхідність.

Для вирішення даної проблеми потрібно присвоювати в попередньому прикладі





 String(P) := S;

це призведе до збільшення лічильника, і все буде нормально, окрім питання звільнення пам’яті від рядка, так як тип P не string і Delphi не генерує код звільнення пам’яті. Вам самим доведеться викликати





 Finalize(string(P));

Для масиву абсолютно аналогічно.

Багатовимірні динамічні масиви


Якщо одномірні масиви реалізуються як безперервний блок пам’яті, то багатовимірні динамічні масиви реалізуються як масиви покажчиків на масиви.

Наприклад, виклик SetLength (A, 2,3) створює 3 масиву:


В цьому сенсі організація багатовимірного динамічного масиву відрізняється, від статичного. Якщо статичний масив безперервний блок пам’яті, то динамічний масив це масив масивів. Така організація дозволяє створювати не прямокутні масиви. Наприклад:





SetLength(A, 3);
SetLength(A[0], 1);
SetLength(A[1], 2);
SetLength(A[2], 3);

Створює трикутний масив.

Помилка компілятора Delphi


Працюючи ще на Delphi6, я зіткнувся з дуже неприємною помилкою. Розглянемо абсолютно нешкідливу функцію:





function E18(const A: TIntegerDynArray; I, J: integer): integer;
begin
Result := A[I] – A[J];
end;

При включеній оптимізації {$O+} і відключення range-checking {$R-}, Виклик цієї функції приводив до помилки доступу до пам’яті. Компілятор породжував, очевидно, помилковий код:





mov eax, [eax+edx*4]
sub eax, [eax+ecx*4]

Причому ця помилка ніяк не пов’язана з динамічними масивами, те ж саме відбувалося і з функцією:





function E19(const A: Pointer; I, J: integer): integer;
begin
Result := PInteger(Integer(A) + I* SizeOf(Integer)) ^ –
PInteger(Integer(A) + J* SizeOf(Integer)) ^;
end;

Коли в Borland дізналися про цю помилку, вони її швидко виправили і в Delphi6.2, вона вже відсутня. Для тих, хто продовжує працювати з більш старими версіями, я раджу звернути на це увагу. Для правильної роботи функції E18, Її слід дещо змінити:





function E20(const A: TIntegerDynArray; I, J: integer): integer;
var
N: integer;
begin
N := A[I];
Result := N – A[J];
end;

Такий код, хоч і менш оптимально, правильно компілюється у всіх версіях (я сподіваюся).

Функції для роботи з масивами


На жаль, деякі операції, такі, як видалення і вставка в одновимірний масив, вимагають написання коду з копіювання елементів масиву. Пропоновані процедури дозволяють виконати ці операції для довільного масиву одним викликом.


Крім того, для алгоритму швидкого сортування, немає ніякої необхідності що-небудь знати про тип і вмісті масиву, достатньо мати дві процедури: порівняння і обміну. Аналогічно і для двійкового пошуку, необхідна тільки функція порівняння. Це дозволяє написати функції сортування та двійкового пошуку в будь-якому масиві.


Кілька зауважень щодо застосування:


  1. Одним з параметрів цих функцій є RTTI про тип масиву, повертається оператором TypeInfo. І дійсно, як можна змінити розмір масиву, не знаючи розмір його елемента, адже в заголовку масиву розмір елемента не зберігається. І це не штучний прийом, так чинить сама Delphi. Таких функцій, як SetLength або Length фактично не існує, компілятор замінює їх своїми системними функціями. Зокрема виклик SetLength транслюється в DynArraySetLength, з передачею йому RTTI.
  2. Елементами динамічного масиву можуть бути довгі рядки, інтерфейси, динамічні масиви і т.п. Тому при видаленні такого елемента потрібно виконати фіналізації всіх, хто знаходиться в ньому значень, якою б вкладеності вони не були. Пропоновані функції коректно виконують фіналізації.
  3. Функції повністю слідують стандартному поведінки щодо обліку лічильника посилань.
  4. Для роботи з динамічними масивами використовуються ті ж системні процедури, які використовує сама Delphi.

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


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

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

Ваш отзыв

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

*

*