
- •Практикум по решению задач в Delphi
- •Содержание
- •1.Лабораторная работа №1. Среда программирования
- •Введение
- •1. Лабораторная работа №1. Среда программирования Delphi
- •1.1. Структура среды программирования
- •Главные составные части среды программирования
- •Дополнительные элементы
- •Инструментальные средства
- •Стандартные компоненты
- •Обзор Палитры Компонент
- •Страница Additional
- •Страница Dialogs
- •Страница System
- •Страница vbx
- •Подробнее об Инспекторе Объектов
- •Сохранение программы
- •TButton, исходный текст, заголовки и z-упорядочивание
- •Тьюторы (интерактивные обучающие программы)
- •Управление проектом
- •Проект Delphi
- •Пункт меню "File"
- •Управление проектом
- •Обзор других пунктов меню
- •Пункт меню Options | Project
- •Конфигурация среды программирования (ide)
- •Рисование и закраска Графические компоненты
- •Свойство объектов Canvas
- •Объект tPaintBox
- •1.2. Примеры
- •1.2.1. Пример программы «Форма с полем для рисования»
- •1.3. Контрольные вопросы
- •1.4. Задания к лабораторной работе №1
- •2. Лабораторная работа №2. Теория чисел
- •2.1. Основные понятия
- •2.2. Пример «неправильного» поведения вещественных типов
- •2.3. Контрольные вопросы
- •2.4. Задания к лабораторной работе № 2 Теория чисел
- •3. Лабораторная работа № 3. Подпрограммы
- •3.1. Процедуры и функции
- •3.2. Примеры
- •3.3. Контрольные вопросы
- •3.4. Задания к лабораторной работе № 3 Подпрограммы
- •4. Лабораторная работа № 4. Строки
- •4.1. Понятие о строковой переменной
- •Как же это происходит?
- •4.2. Примеры
- •4.3. Контрольные вопросы
- •4.4. Задания к лабораторной работе № 4 Строки
- •5. Лабораторная работа № 5. Множества
- •5.1. Описание типа «множество»
- •5.2. Примеры
- •5.3. Контрольные вопросы
- •5.4. Задания к лабораторной работе № 5 Множества
- •6. Лабораторная работа № 6. Записи
- •6.1. Записи
- •6.2. Примеры
- •6.3. Контрольные вопросы
- •6.4.Задания к лабораторной работе № 6 Записи
- •7. Лабораторная работа №7. Файлы
- •7.1. Файлы
- •7.2. Примеры
- •7.3. Контрольные вопросы
- •7. 4. Задания к лабораторной работе №7 Файлы
- •Литература:
Как же это происходит?
При выполнении первого оператора (присваивание s1), указатель, хранящийся в переменной s1 настраивается на область памяти в которой размещена строка 'abc'. При выполнении второго оператора, в s2 попадает тот же адрес! Т.е. в памяти при это присутствует лишь один экземпляр строки 'abc'. Да и зачем нам нужны дублирующие себя строки.
Не нужно программисту заботиться и об освобождении памяти занятой ненужными уже строками. Например, есть такая процедура:
procedure ShowInteger;
var
s :AnsiString;
n :Integer;
begin
n := 123;
s := 'Значение переменной равно '+IntToStr(n);
ShowMessage(s);
end;
Здесь, при выполнении присваивания, Delphi создаст в памяти экземпляр строки 'Значение переменной равно 123', и присвоит адрес этой строки переменной s. Однако, при завершении процедуры, переменная s перестанет существовать – она же локальная. Значит, и экземпляр строки тоже уже не нужен – на него некому будет указывать. Вот поэтому, Delphi автоматически освободит память, выделенную под строку, как только, выполнение процедуры достигнет строки end. Более того, даже если во время выполнения процедуры возникнет исключительная ситуация, при которой "хвост" процедуры может и не выполниться, Delphi всё равно корректно освободит память для всех строк, распределенных в этой процедуре. Достигается это неявным использованием механизма подобного try - finally.
Казалось бы, всё замечательно. Но попробуем усложнить ситуацию.
var
gs :AnsiString; // глобальная переменная
procedure ShowInteger;
var
s :AnsiString;
n :Integer;
begin
n := 123;
s := 'Значение переменной равно '+IntToStr(n);
gs := s;
ShowMessage(s);
end;
Теперь, к моменту завершения процедуры, на экземпляр строки 'Значение переменной равно 123' уже ссылаются две переменные s и gs. И, несмотря на то, что область существования переменной s заканчивается, освобождать память, выделенную под строку на которую она указывает нельзя! Ведь позже, возможны обращения к переменной gs.
Для того, чтобы корректно обрабатывать такие ситуации, Delphi для каждой динамически распределенной строки ведет так называемый "счётчик ссылок". Т.е., как только он присваивает какой-либо из строковых (AnsiString) переменных ссылку на распределенную в памяти строку, то он увеличивает этот счетчик на единицу. Первоначально, при присваивании динамически распределённой строки, первой переменной (в примере s), значение этого счётчика устанавливается равным единице. В последствии, при прекращении жизни каждой строковой переменной, он уменьшает на 1 этот счетчик для той строки на которую она указывает. Если счётчик становится равным 0, то значит более нет строковых переменных, указывающих на данную строку. Значит, ее можно освобождать. Благодаря такому алгоритму, после присваивания в примере значения переменной gs, у строки 'Значение переменной равно 123' счетчик ссылок становится равным 2. Следовательно, при "умирании" переменной s, он декрементируется, и становится равным 1. Т.е. >0, поэтому то Delphi и не освобождает память, занятую строкой.
Еще, этот счётчик используется и для разрешения проблем, связанных со следующей ситуацией:
procedure ShowInteger;
var
s1 :AnsiString;
s2 : AnsiString;
n :Integer;
begin
n := 123;
s1 := 'abc'+IntToStr(n);
s2 := s1;
s2[1] := 'X';
end;
Здесь, как мы уже знаем, после выполнения оператора s2 := s1, обе переменные указывают на один и тот же экземпляр строки 'abc123'. Однако, что же произойдёт когда выполниться оператор s2[1] := 'X'? Казалось бы, в единственном имеющимся в нашем распоряжении экземпляре строки первая буква будет заменена на 'X'. И как следствие, обе строки станут равными 'Xbc123'. s1 то за что "страдает"? Но, к счастью это не так. Здесь на помощь Delphi вновь приходит счетчик ссылок. Delphi, при выполнении этого оператора понимает, что строка на которую указывает s2 будет изменена, а это может повлиять на других. Поэтому, перед изменением строки, проверяется ее счётчик ссылок. Обнаружив, что на нее ссылается более одной строковой переменной, делается следующее: создается копия этой строки со счётчиком ссылок равным 1, и адрес этой копии, присваивается s2; У исходного экземпляра строки, счетчик ссылок уменьшается на 1 – ведь s2 на неё теперь не ссылается. И лишь после этого, происходит изменение первой буквы, теперь уже собственного экземпляра строки. Т.е., по окончанию выполнения этого оператора, в памяти будут находиться две строки: 'abc123' и 'Xbc123'. Причем, s1 будет ссылаться на первую, а s2 на вторую.
При работе со строками определенными как константы, алгоритм работы несколько отличается.
Пример:
procedure ShowInteger;
var s :AnsiString;
begin
s := 'Вася';
ShowMessage(s);
end;
Казалось бы, при завершении работы процедуры, экземпляр строки 'Вася' должен быть уничтожен. Но в данном случае это не так. Ведь, при следующем входе в процедуру, для выполнения присваивания нужно будет вновь где-то взять строку 'Вася'. Для этого, ещё при компиляции, Delphi размещает экземпляр строки 'Вася' в области констант программы, где её даже невозможно изменить, по крайней мере, простыми методами. Но как же при завершении процедуры определить что строка 'Вася' – константная строка, и ее нельзя уничтожать? Все очень просто. Для константных строк, счётчик ссылок устанавливается равным -1. Это значение, "выключает" нормальный алгоритм работы со "счётчиком ссылок". Он не увеличивается при присваивании, и не уменьшается при уничтожении переменной. Однако, при попытке изменения переменной (помните s2[1]:='X'), значение счётчика равное -1 будет всегда считаться признаком того, что на строку ссылается более одной переменной (ведь он не равен 1). Поэтому, в такой ситуации всегда будет создаваться уникальный экземпляр строки, естественно, без декремента счётчика ссылок старой. Это защитит от изменений экземпляр строки-константы.
К сожалению, этот алгоритм срабатывает не всегда.
Где же Delphi хранит "счётчик ссылок"? Причем, для каждой строки свой! Естественно, вместе с самой строкой. Вот что представляет собой эта область памяти, хранящая экземпляр строки 'abc':
Байты с 1 по 4 |
Счётчик ссылок равный -1 |
Байты с 5 по 8 |
Длина строки равная 3 |
Байт 9 |
Символ 'a' |
Байт 10 |
Символ 'b' |
Байт 11 |
Символ 'c' |
Байт 12 |
Символ с кодом 0 (#0) |
Для удобства работы с такой структурой, когда строковой переменной присваивается ссылка на эту строку, в переменную заносится адрес не начала этой структуры, а адрес её девятого байта. Т.е. адрес начала реальной строки (прямо как pChar). Для того, что бы приблизиться к реальной жизни, перепишем приведённую структуру:
Смещение |
Размер |
Значение |
Назначение |
-8 |
4 |
-1 |
Счётчик ссылок |
-4 |
4 |
3 |
Длина строки |
0 |
1 |
'a' |
|
1 |
1 |
'b' |
|
2 |
1 |
'c' |
|
3 |
1 |
#0 |
|
С полем по смещению -8, нам уже должно быть все ясно. Это значение, хранящееся в двойном слове (4 байта), тот самый счетчик, который позволяет оптимизировать хранение одинаковых строк. Значение этого счетчика имеет тип Integer, т.е. может быть отрицательным. На самом деле, используется лишь одно отрицательное значение – "-1", и положительные значения. 0 не используется.
Теперь, обратим внимание на поле, лежащее по смещению -4. Это, четырёхбайтовое значение длинны строки (почти как в ShortString). Думаю, Вы заметили, что размер памяти выделенной под эту строку не имеет избыточности. Т.е. компилятор выделяет под строку минимально необходимое число байт памяти. Это конечно хорошо, но, при попытке "нарастить" строку: s1 := s1 + 'd', компилятору, точнее библиотеке времени исполнения (RTL) придется перераспределить память. Ведь теперь строке требуется больше памяти, аж на целый байт. Для перераспределения памяти нужно знать текущий размер строки. Вероятно, именно для того, что бы не приходилось каждый раз сканировать строку, определяя её размер, разработчики Delphi и включили поле длины, строки в эту структуру. Длина строки, хранится как значение Integer, отсюда и ограничение на максимальный размер таких строк – 2 Гбайт. Кстати, именно потому, что память под эти строки выделяется динамически, они и получили ещё одно свое название: динамические строки.
Ещё немного о нескольких особенностях переменных AnsiString. Важнейшей особенностью значений этого типа является возможность приведения их к типу Pointer. Это впрочем, естественно, ведь в "душе" они и есть указатели, как бы они этого не скрывали. Например, если описаны переменные: s :AnsiString и p :Pointer. То выполнение оператора p := Pointer(s) приведет к тому, что переменная p станет указывать на экземпляр строки. Однако, при этом, очень важно знать: счетчик ссылок этой строки не будет увеличен.
Поскольку, переменные этого типа реально являются указателями, то для них и реально такое значение как Nil – указатель в "никуда". Это значение в переменной типа AnsiString по смыслу приравнивается пустой строке. Более того, чтобы не тратить память и время на ведение счётчика ссылок, и поля размера строки всегда равного 0, при присваивании пустой строке переменной этого типа, реально, присваивается значение Nil. Это не очевидно, поскольку обычно не заметно, но как мы увидим позже, очень важная особенность.
Преобразование строк из одного типа в другой
Преобразование между "настоящими" строковыми типами String[n], ShortString, и AnsiString выполняются легко, и прозрачно. Никаких явных действий делать не надо, Delphi все сделает за Вас. Надо лишь понимать, что в маленькое большое не влезает. Например:
var
s3 :String[3];
s :AnsiString;
...
s := 'abcdef';
s3 := s;
В результате выполнения этого кода, в переменной s3 окажется строка 'abc', а не 'abcdef'. С преобразованием из pChar в String[n], ShortString, и AnsiString, тоже всё очень не плохо. Просто присваивайте, и все будет нормально.
Сложности начинаются тогда, когда мы начинаем преобразовывать "настоящие" строковые типы в pChar. Непосредственное присваивание переменным типа pChar значений строк не допускается компилятором. На оператор p := s где p имеет тип pChar, а s :AnsiString, компилятор выдаст сообщение: "Incompatible types: 'String' and 'PChar'" - несовместимые типы 'String' и 'PChar'. Чтобы избежать такой ошибки, надо применять явное приведение типа: p := pChar(s). Так рекомендуют разработчики Delphi. В общем, они правы. Но, если вспомнить, как хранятся динамические строки - с нулем в конце, как и pChar. А еще и то, что к AnsiString применимо преобразование в тип Pointer. Станет очевидным, что всего, возможно целых три способа преобразования строки в pChar:
var
s :AnsiString;
p1,p2,p3 :PChar;
...
p1 := pChar(s);
p2 := Pointer(s);
p3 := @(s[1]);
Все они, синтаксически правильны. И кажется, что все три указателя (p1, p2 и p3) будут в результате иметь одно и то же значение. Но это не так. Всё зависит от того, что находится в s. Если быть более точным, равно ли значение s пустой строке, или нет:
s <> ''
p1 = p2 <> p3
s = ''
p1 <> p2 = p3
Чтобы была понятна причина такого явления, опишем, как Delphi выполняет каждое из этих преобразований. Переменные AnsiString представляющие пустые строки, реально имеют значение Nil. Так вот:
pChar(s)
Для выполнения преобразования pChar(s), компилятор генерит вызов специальной внутренней функции @LstrToPChar. Эта функция проверяет – если строковая переменная имеет значение Nil, то вместо него, она возвращает указатель на реально размещенную в памяти пустую строку. Т.е. pChar(s) никогда не вернет указатель равный Nil.
Pointer(s)
Тут все просто, такое преобразование просто возвращает содержимое строковой переменной. Т.е. если она при пустой строке содержит Nil, то и результатом преобразования будет Nil. Если же строка не пуста, то результатом будет адрес экземпляра строки.
@(s[1])
Здесь, все совсем по-другому. Перед выполнением такого преобразования, компилятор вставляет код, обеспечивающий ситуацию, когда указатель, хранящийся в s, будет единственным указателем на экземпляр строки в памяти. В нашем примере, если строка не пуста, то будет создана копия исходной строки. Вот ее-то адрес и будет возвращен как результат такого преобразования. Но, если строка пуста, то результатом будет Nil, как и во втором случае.
Теперь, интересно отметить, что если в приведенном примере, преобразование p3 := @(s[1]) выполнить первым, то при не пустой строке в s, все указатели (p1, p2, и p3), будут равны. И содержать они будут адрес "персонального" экземпляра строки.
Вот, теперь, зная, как выполняются те или иные преобразования, Вы сможете всегда выбрать подходящий способ.
Приведем пример. В нем, преобразование, рекомендуемое разработчиками, приводит к "странному" поведению программы:
procedure X1;
var
s :AnsiString;
p :PChar;
begin
s := 'abcd';
p := PChar(s);
p^ := 'X'; // <-
ShowMessage(s);
end;
вызывает ошибку доступа к памяти при выполнении строки, помеченной <=. Почему - предлагаю Вам разобраться самостоятельно. После прочтения данной статьи Ваших знаний для этого достаточно. В тоже время, код:
procedure X1;
var
s :AnsiString;
p :PChar;
begin
s := 'abcd';
p := @(s[1]);
p^ := 'X';
ShowMessage(s);
end;
будет выполнен без ошибок, выведя строку 'Xabcd'. Также как и код:
procedure X1;
var
s :AnsiString;
p :PChar;
begin
s := 'abcd';
s[2] := 'b';
p := PChar(s);
p^ := 'X';
ShowMessage(s);
end;
Рассматривая преобразование AnsiString в pChar (получение адреса строки) нельзя не упомянуть ещё одну серьезную проблему – область действия для полученного таким путем указателя.
Delphi ведет учет всех ссылок на каждый экземпляр динамически распределенной строки. И на основании этого принимает решение об освобождении памяти занимаемой экземпляром строки. Но, это касается только ссылок хранящихся в переменных AnsiString. Ссылки, полученные преобразованием в pChar, не учитываются. Вот демонстрация того, как это может сказаться на поведении программы. Пусть есть следующая функция:
function IntToPChar (n :Integer) :pChar;
var s :AnsiString;
begin
s := IntToStr(n);
Result := PChar(s);
end;
Казалось бы, всё написано правильно. Однако если выполнить такой оператор:
ShowMessage(IntToPChar(100));
То, вместо ожидаемого окна со строкой '100', мы либо получим абракадабру, либо и того хуже – ошибку AV. А все почему? Да, просто, единственным учтённым указателем на экземпляр строки, полученный от IntToStr, будет s. Поэтому, когда его область действия прекращается по выходу из процедуры, экземпляр строки будет уничтожен. А не учтённый указатель, возвращаемый функцией IntToPChar, после этого станет указывать "куда бог пошлёт". Точнее, на то место в памяти, где недавно была строка. Если же переменная s будет объявлена на глобальном уровне, то будет нормально, но всё равно возможны ошибки. Например:
var s :AnsiString; // глобальная переменная
...
function IntToPChar (n :Integer) :pChar;
begin
s := IntToStr(n);
Result := PChar(s);
end;
...
var p100, p200 :pChar;
begin
p100 := IntToPChar(100);
p200 := IntToPChar(200);
...
После второго выполнения функции IntToPChar, указатель p100, опять будет указывать "куда бог пошлет", Почему, разберитесь сами :).
Длинные строки
Особенностью длинных строк является возможность присваивания константного литерала. В зависимости от того, является переменная локальной или нет, генерируется различный код.
Если происходит присваивание локальной переменной:
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';
// Сейчас также две строки
К изменению строки через 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.