ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 18.01.2025
Просмотров: 764
Скачиваний: 2
СОДЕРЖАНИЕ
Строковые типы в Delphi. Особенности реализации и использования
Какие строковые типы существуют в Delphi, и чем они отличаются друг от друга?
Преобразование строк из одного типа в другой
Функции для работы со строками, о которых многие часто забывают, или вовсе не знают
If AnsiMatchText(YouName,['Вася','Петя','Гриша']) then
Запись строк в файл и чтение из файла
Использование строк в качестве параметров и результатов функций размещенных в dll.
|
Смещение |
Размер |
Значение |
Назначение |
|
-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, опять будет указывать "куда бог пошлет", Почему, разберитесь сами :).
В завершении этого раздела, я хочу "напугать" Вас:
Будьте крайне осторожны и бдительны при получении и использовании ссылок pChar на динамически размещенные строки,
Знание того, как это все работает, думаю, Вам поможет.