Sources.RU Magazine Поиск по журналу
 

Ссылки

Детальное описание собрать компьютер здесь.

Строковые типы в Delphi. Особенности реализации и использования

Автор: Сысоев Александр Петрович

В этой статье будут освещены следующие вопросы:

  1. Какие строковые типы существуют в Delphi, и чем они отличаются друг от друга
  2. Преобразование строк из одного типа в другой
  3. Некоторые приемы использования строк типа AnsiString:
    1. Функции для работы со строками о которых многие часто забывают или вовсе не знают
    2. Передача строк в качестве параметров
    3. Использование строк в записях
    4. Запись в файл и чтение из файла
    5. Использование строк в качестве параметров и результатов функций размещенных в DLL.

Ну что, интересно? Тогда поехали.

Какие строковые типы существуют в Delphi, и чем они отличаются друг от друга?

В Delphi 1.0 существовал лишь единственный строковый тип String, полностью эквивалентный одноименному типу в Turbo Pascal и Borland Pascal. Однако, этот тип имеет существенные ограничения, о которых я расскажу позднее. Для обхода этих ограничений, в Delphi 2, разработчики из Borland устроили небольшую революцию. Теперь, начиная с Delphi 2, имеются три фундаментальных строковых типа: ShortString, AnsiString, и WideString. Кроме того, тип String теперь стал логическим. Т.е., в зависимости от настройки соответствующего режима компилятора (режим больших строк), он приравнивается либо к типу ShortString (для совместимости со старыми программами), либо к типу AnsiString (по умолчанию). Управлять режимом, можно используя директиву компиляции {$LONGSTRINGS ON/OFF} (короткая форма {$H+/-}) или из окна настроек проекта – вкладка "Compiler" -> галочка "Huge strings". Если режим включен, то String приравнивается к AnsiString, иначе String приравнивается ShortString. Из этого правила есть исключение: если в определении типа String указан максимальный размер строки, например String[25], то, вне зависимости от режима компилятора, этот тип будет приравнен к ShortString соответствующего размера.

Поскольку, как вы узнаете в дальнейшем, типы ShortString и AnsiString имеют принципиальное отличие в реализации, то я вообще не рекомендую пользоваться логическим типом String без указания размера, если Вы, конечно, не пишете программ под Delphi 1. Если же Вы все-таки используете тип String, то я настоятельно рекомендую прямо в коде Вашего модуля указывать директиву компиляции, устанавливающую подразумеваемый Вами режим работы компилятора. Особенно если Вы используете особенности реализации соответствующего строкового типа. Если этого не сделать, то однажды, когда Ваш код попадет в руки другого программиста, не будет никакой гарантии того, что его компилятор будет настроен, так же как и Ваш.

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

Сразу же упомяну о различии между типами AnsiString и WideString. Эти типы имеют практически одинаковую реализацию, и отличаются лишь тем, что WideString используется для представления строк в кодировке UNICODE использующей 16-ти битное представление каждого символа (WideChar). Эта кодировка используется в тех случаях когда необходима возможность одновременного присутствия в одной строке символов из двух и более языков (помимо английского). Например, строк содержащих одновременно символы английского, русского и европейских языков. За эту возможность приходится платить – размер памяти, занимаемый такими строками в два раза больше размера, занимаемого обычными строками. Использование WideString встречается не часто, поэтому, я буду в основном рассказывать о строках типа AnsiString. Но, поскольку они имеют одинаковую реализацию, почти все сказанное относительно AnsiString будет действительно и для WideString, естественно с учетом разницы в размере каждого символа.

Тоже самое касается и разницы между pChar и pWideChar.

Строковый тип AnsiString, обычно используется для представления строк в кодировке ANSI, или других (например OEM) в которых для кодирования одного символа используется один байт (8 бит). Такой способ кодирования называется single-byte character set, или SBCS. Но, очень многие не знают о существовании еще одного способа кодирования многоязычных строк (помимо UNICODE) используемого в системах Windows и Linux. Этот способ называется multibyte character sets, или MBCS. При этом способе, некоторые символы представляются одним байтом, а некоторые, двумя и более. В отличие от UNICODE, строки, закодированные таким способом, требуют меньше памяти для своего хранения, но требуют более сложной обработки. Так вот, строковый тип AnsiString может использоваться для хранения таких строк. Я не буду подробно останавливаться на этом способе кодирования, поскольку он применяется крайне редко. Лично я, ни разу не встречал программ использующих данный способ кодирования.

Знатоки Delphi вероятно мне сразу напомнят еще и о типах pChar (pWideChar) и array [...] of Char. Однако, я считаю, что это не совсем строковые типы, но я расскажу и о них, поскольку они очень часто используются в сочетании со строковыми типами.

Итак, приведу основные характеристики строковых типов:

Тип Максимальный размер строки Размер переменной Объем памяти, требуемый для хранения строки
String[n] где 0 <= n <= 255 n символов n+1 байт n+1 байт
ShortString 255 символов 256 байт 256 байт
AnsiString ~2^31 символов 4 байта 4 байта + (0 .. 2 Гбайт)
WideString ~2^30 символов 4 байта 4 байта + (0 .. 2 Гбайт)
pChar не ограничено 4 байта 4 байта + размер строки +1
pWideChar не ограничено 4 байта 4 байта + (размер строки+1)*2
array [0..n] of Char n n+1 байт n+1 байт
array [0..n] of WideChar n (n+1)*2 байт (n+1)*2 байт

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

array [0..n] of Char

Формально, этот тип не являются строковыми. Однако, в Delphi, он несколько отличаются от остальных типов массивов. А именно, если массив символов имеет нижнюю границу индекса равной 0, то Delphi считает такой тип совместимым по присваиванию со строковыми константами. Например, если переменная a будет описана как a :array[0..20] of Char, то оператор a := 'abc' будет допустим. Причём, значения элементов начиная с a[3] и до a[20] будут установлены в #0.

Больше ничего необычного в этом типе нет. Хотя забыл, есть ещё – оператор @ (получение указателя) для переменной такого типа возвращает значение типа pChar. Это очень удобно, поскольку переменные этого типа очень часто используются как буфер при работе с функциями Windows API. Например:

var
  a :array[0..20] of Char;
...
  GetModuleFileName(GetModuleFileName(HInstance,@a,SizeOf(a));

Здесь, функция GetModuleFileName возвращает результат в массив a.

pChar

Этот тип широко используется в языках C и C++. В Delphi, это не фундаментальный тип, а производный. Его определение выглядит так:

pChar = ^Char

Т.е. переменные этого типа являются указателем, поэтому и имеют размер 4 байта. Формально, значение pChar может указывать как на один символ, так и на строку символов. Однако, общепринято что значения pChar указывают на строки, завершающиеся символом с кодом 0 (#0). В DOSе, такие строки назывались ASCIIZ, но чаще можно встретить название null-terminated string. Наличие такого «концевика» позволяет легко определить реальный размер строки на которую указывает значение pChar.

Не смотря на то, что формально pChar это указатель на Char (^Char), как это часто бывает в Delphi, тип pChar имеет несколько особенностей по сравнению с другими указателями. Таких особенностей несколько.

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

const pc :pChar ='abc';
var   pv :pChar ='abc';

Эти строки, определяют константу pc и переменную pv типа pChar. При этом, и pc и pv указывают на разные области памяти, но содержащие одинаковые значения, состоящие из трех символов: 'a', 'b', 'c', и символа #0. Замечу, что завершающий символ с кодом 0 компилятор добавил автоматически.

Вторая особенность в том, что к переменным типа pChar применимо обращение как к массиву символов. Например, если есть приведенные выше определения, тогда:

C := pv^;   // C будет присвоен символ 'a'. Это обычное обращение
C := pv[0]; // необычно, но С станет равным 'a'
C := pv[1]; // С станет равным 'b'
C := pv[2]; // С станет равным 'c'
C := pv[3]; // С станет равным #0
C := pv[4]; // ОШИБКА!

Замечу, символ с индексом 3 отсутствует в строке, однако, там есть завершающий ее символ с кодом 0. Именно он будет результатом pv[3]. О pv[4] тоже стоит сказать особо. Дело в том, что компилятор не даст ошибки при компиляции, поскольку на этапе компиляции он, в общем случае, не известен реальный размер строки, на которую указывает переменная pv. Однако, на этапе выполнения программы, такое обращение может вызвать ошибку нарушения доступа к памяти (Access Violation). А может и не вызвать, но результатом будет неопределённое значение. Все зависит от «расклада» в памяти. Поэтому, при таком способе обращения необходимо быть внимательным, и выполнять все необходимые проверки, исключающие выход за размеры строки.

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

function StringLength (p :pChar) :Cardinal;
begin
  Result := 0;
  if  p = nil  then Exit;
  while p^ <> #0 do begin
    Inc(Result);
    Inc(p);
  end;
end;

Здесь важно обратить внимание на два нюанса.

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

Второй, это оператор Inc(p) – он «продвигает» указатель на следующий символ. Можно было бы записать его и так: p := p + 1.

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

function StringLength (p :pChar) :Cardinal;
var pp :pChar;
begin
  Result := 0;
  pp := p;
  if  pp <> nil  then
    while pp^ <> #0 do
      Inc(pp);
  Result := (pp-p);
end;

Здесь, выражение pp-p дает «расстояние» между указателями, т.е. число символов между символом, на который указывает указатель p (начало строки) и символом, на который указывает указатель pp (завершающий строку #0).

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

ShortString, и String[n]

ShortString является частным случаем String[n], а если быть более точным, он полностью эквивалентен String[255].

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

var s :String[4];

Оно означает, что для переменной s будет статически выделена область памяти размером 5 байт.

Теперь, выполнение оператора s := 'abc', приведёт к тому, что содержимое этих пяти байт станет следующим: байт 1 = 3, байт 2 = 'a', байт 3 = 'b', байт 4 = 'c', а значение байта 5 будет неопределённо – оно будет зависеть от «расклада» в памяти. Т.е., первый символ строки будет находиться во втором байте. Это неудобно, поэтому к символам строк ShortString принято индексироваться, начиная с 1. Следовательно:

s[1] = 'a' – первый символ строки
s[2] = 'b' – второй символ строки
s[3] = 'c' – третий символ строки
s[4] = все что угодно :).

А как же байт длины? Да все очень просто, к нему можно обратиться как s[0]. Только вот есть маленькая проблемка. Поскольку элементами строки являются символы, то и тип значения s[0] тоже будет символ. Т.е., если Вы хотите получить длину строки в виде целого числа, как это принято у нормальных людей, то надо выполнить соответствующее преобразование типа: Ord(s[0]) = 3 – размер строки.

Теперь, я думаю, Вам станет ясно, почему для типа String[n] существует ограничение 0<=n<=255. Это весь набор значений, которые могут быть представлены в одном байте. На всякий случай, отмечу, что размерность переменной (n) определяет размер выделяемой под эту переменную памяти, и он не зависит от размера строки, которая там хранится. Т.е., если, например переменная описана как String[30], то даже если присвоить ей значение 'abcd', то размер этой переменной все равно останется 31 байт. Это иногда бывает очень удобно, например, при записи (чтении) таких переменных в файл (из файла).

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

function StringLength (s :ShortString) :Cardinal;
begin
  Result := Ord(s[0]);
end;

Как видите, это код существенно компактнее, и быстрее чем соответствующий код для pChar. Именно поэтому, он и применялся в языке Pascal.

Однако ясно, что ограничения переменных этого типа – максимальный размер строки 255 символов, и всегда максимальный размер занимаемой переменной памяти, вне зависимости от реально помещенной в нее строки, заставили разработчиков Delphi вести новые строковые типы. Вот к ним мы сейчас и перейдем.

AnsiString и WideString

Строки этого типа объединили в себе ряд качеств как строк ShortString и их байтом длины, так и строк завершающихся нулем (pChar). Последнее было необходимо, поскольку к моменту их появления, засилье C-ишников было уже так велико :), что большинство системных функций Windows API оперировало строками именно такого формата. А если серьезно, то такой формат строк хоть и более трудоемок в обработке, зато в принципе лишен ограниченности на максимальный размер строки. Ведь хранение размера строки всегда ограниченно какими-либо рамками: если хранить в байте, то ограничение 255 байт; если хранить в слове, то ограничение 65535 символов; и т.д. Однако, совсем отказываться от хранения текущей длинны строки в Borland не стали. Но об этом позже.

Напомню, что еще одним из недостатков статически размещаемых строк ShortString было "расточительство". Т.е. определив переменную такого типа, мы заранее резервировали под нее 256 байт памяти, поэтому, если мы один раз, во всей программе, присвоили ей значение 'abcd', то 251 байт памяти мы просто «пустили на ветер». Казалось бы, а зачем так определили, написали бы String[4], и ничего не потеряли бы. Но, когда мы пишем программу, мы же чаще всего не знаем что мы будем «класть» в эту переменную. Вот и определяем с запасом. Решением этой проблемы стало использование динамически размещаемых строк.

Так как они устроены?

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

var s1, s2 :AnsiString;
...
s1 := 'abc';
s2 := s1;

то в памяти будет храниться лишь ОДИН экземпляр строки 'abc'! Во как.

Как же это происходит?

При выполнении первого оператора (присваивание 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, опять будет указывать "куда бог пошлет", Почему, разберитесь сами :).

В завершении этого раздела, я хочу "напугать" Вас:

Будьте крайне осторожны и бдительны при получении и использовании ссылок pChar на динамически размещенные строки,

Знание того, как это все работает, думаю, Вам поможет.

Функции для работы со строками, о которых многие часто забывают, или вовсе не знают

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

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

procedure SetLength (var S; NewLength: Integer)

Позволяет распределить память для динамической строки. Замечу, если строка уже содержит некоторое число символов, они копируются в результирующий экземпляр. Если конечно влезут. Это позволяет "расширять" уже существующий экземпляр строки.

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

function ComputerNetName :String;
// Возвращает сетевое имя компьютера
var Len :Cardinal;
begin
  Len := MAX_COMPUTERNAME_LENGTH+1;
  SetLength(Result,Len); // выделить память для буфера
  if  GetComputerName(pChar(Result),Len)  then 
    SetLength(Result,Len) // установить точный размер 
  else
    Result := '';
end;

procedure SetString(var s: string; buffer: PChar; len: Integer);

Распределяет память под экземпляр строки, копирует в него len байт из буфера buffer (если он не Nil), и присваивает ссылку на этот экземпляр переменной s. После вызова этой функции, значение счётчика ссылок полученного экземпляра всегда равно 1.

Её удобно использовать, когда нужно преобразовать в строку массив символов, не заканчивающийся #0.

function SysErrorMessage(ErrNo: Integer): string;
// Получение описания ошибки по коду
var
  Size   :Integer;
  Buffer :array[0..1024] of Char;
begin
  Size := FormatMessage
    (FORMAT_MESSAGE_FROM_SYSTEM or FORMAT_MESSAGE_ARGUMENT_ARRAY
    ,nil ,ErrNo ,0 ,@Buffer,SizeOf(Buffer) ,nil
    );
  SetString(Result, Buffer, Size);
end;

procedure UniqueString (var str: string)

Гарантирует что после вызова, str будет указывать на уникальный экземпляр значеничя строки. Т.е., str будет указывать на строку счетчик ссылок которой будет равен 1. Если необходимо, делает копию исходного значения. Обычно нет необходимости использовать эту функцию. Однако, иногда мне надо получить указатель на строку (pChar), для того что бы изменить её. А поскольку, как я уже говорил, указатели pChar не обеспечиваются контролем за изменением строк. Это может привести к побочным изменениям в "других" строках, если счётчик ссылок исходной строки был >1. Вот и приходится применять эту функцию.

procedure Wr   (Msg  :String);
// вывод строки в консольное окно в OEM кодировке
begin
  UniqueString(Msg); // получить "персональный" экземпляр строки
  Windows.CharToOem(pChar(Msg),pChar(Msg)); // преобразование его
  Write(Msg); // вывод преобразованного экземпляра на консоль
end;

Здесь, поскольку параметр передаётся по значению, при вызове будет создана локальная копия переменной (указателя) Msg. Об этом я ещё расскажу ниже. Эта переменная, при завершении процедуры будет уничтожаться, что приведёт к освобождению "персональной" копии экземпляра переданной и преобразованной строки.

function AnsiCompareStr(const S1, S2: string): Integer; 
function AnsiCompareText(const S1, S2: string): Integer; 
function CompareStr(const S1, S2: string): Integer; 
function CompareText(const S1, S2: string): Integer;

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

>0 если S1 > S2
<0 если S1 < S2
=0 если S1 = S2

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

Разница между ними следующая:

Те, чьё имя заканчивается на Str, выполняют точное сравнение с учётом регистра символов, а те, что заканчиваются на Text, выполняют сравнение без учета регистра символов.

Те чьё имя начинается на Ansi, сравнивают с учётом упорядоченности символов определяемой текущей языковой настройкой Windows. Функции без Ansi, сравнивают символы как 8-битные значения, что для некоторых языковых кодировок может дать не совсем правильные результаты. В общем случае, при таком сравнении, правильным можно считать лишь результат сравнения строк содержащих только латинские символы. Например, если сравниваемые строки содержат русскую буквы 'Ё' или 'ё', то результат сравнения будет неправильным. Поэтому, я практически всегда использую функции с префиксом Ansi. Даже учитывая то, что они работают чуть медленнее.

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

function AnsiSameStr		(const S1, S2: string): Boolean; 
function AnsiSameText		(const S1, S2: string): Boolean; 
function SameText		(const S1, S2: string): Boolean;

Эти функции возвращают True если обе строки равны, иначе их результат False. Обратите внимание, на отсутствие в этой группе функции SameStr. Её просто не существует! Да и зачем, если тот же результат можно получить, выполнив выражение S1 = S2.

function AnsiStartsStr    (const ASubText, AText: string): Boolean; 
function AnsiStartsText (const ASubText, AText: string): Boolean;

Возвращают True если строка AText начинается с ASubText.

function AnsiMatchStr   (const AText: string; const AValues: array of string): Boolean; 
function AnsiMatchText (const AText: string; const AValues: array of string): Boolean;

Возвращают True если строка AText встречается среди списка AValues. Пример использования:

if AnsiMatchText(YouName,['Вася','Петя','Гриша']) then
  ShowMessage('Ты мальчик')
else if AnsiMatchText(YouName,['Света','Оля','Настя']) then
  ShowMessage('Ты девочка')
else
  ShowMessage('Я не знаю тебя!');

function AnsiIndexStr   (const AText: string; const AValues: array of string): Integer; 
function AnsiIndexText (const AText: string; const AValues: array of string): Integer;

Возвращают индекс строки AText в списке AValues. Первый элемент списка имеет индекс 0. Если же AText в списке AValues не встречается, возвращает -1. Очень удобные функции, жаль что об их существовании многие не догадываются. Приведу пример использования:

case AnsiIndexText(Cmd,['вверх','вниз','влево','вправо']) of
  0 : Dec(y);
  1 : Inc(y);
  2 : Dec(x);
  3 : Inc(x);
  else ShowMessage('Незнакомая команда: "'+Cmd+'"');
end;

function AnsiReplaceStr	(const AText, AFromText, AToText: string): string; 
function AnsiReplaceText	(const AText, AFromText, AToText: string): string;

Позволяют выполнить замену контекста AFromText на AToText в строке AText.

function StuffString(const AText: string; AStart, ALength: Cardinal; const ASubText: 
string): string;

Заменяет фрагмент строки AText, начиная с позиции AStart и длиной ALength, строкой ASubText.

function StringOfChar(Ch: Char; Count: Integer): string;

Возвращает строку длинной Count, состоящую из символов Ch.

...

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

Некоторые приемы использования строк типа AnsiString

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

Передача строк в качестве параметров

Как известно, есть четыре способа передачи параметров в процедуру или функцию. Вот какие особенности они имеют если параметр - динамическая строка:

  1. входной параметр (const) – его значения нельзя изменять внутри, поэтому, передаётся адрес экземпляра динамической строки, без увеличения счётчика ссылок;

  2. выходной параметр (out) – передаётся адрес строковой переменной которая будет получать результат. В общем случае значение такого параметра на входе в функцию считаются неопределённым, но для динамических строк, он всегда равен пустой строке. Естественно, счётчик ссылок не увеличивается – у пустой строки его просто нет. В месте вызова, и в начале процедуры, выполняется дополнительный код, обеспечивающий очистку строки. Конечно, на него тратится память и время.
  3. параметр переменная (var) – передаётся адрес строковой переменной. Изменение этого параметра отражается изменением переданной переменной. Счётчик ссылок на экземпляр строки не увеличивается, поскольку передаётся не ссылка, а ссылка на ссылку;
  4. параметр-значение – для такого параметра создается локальная копия переданного значения. При этом передаётся адрес экземпляра динамической строки, а в начале процедуры, при создании локальной копии параметра, увеличивается счётчик ссылок на экземпляр. На это тратится память и время.

Прочитав это, должна стать понятным следующее:

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

Запись строк в файл и чтение из файла

Очень часто возникают вопросы вроде такого: Пишу строку в файл, затем читаю её от-туда, в результате получаю белиберду. В качестве кода приводят что-то вроде такого:

var
  Stream :tStream;
  Str :AnsiString;
...
  Stream.WriteBuffer(Str,SizeOf(Str));  // так пишу
...
  Stream.ReadBuffer(Str,SizeOf(Str));   // так читаю

или

var
  f   :file;
  Str :AnsiString;
...
  BlockWrite(f,Str,SizeOf(Str));        // так пишу
...
  BlockRead(f,Str,SizeOf(Str));         // так читаю

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

Такой способ записи/чтения приемлем только для строковых переменных типов ShortString и String[n], поскольку переменные этих типов хранят саму строку, а не указатель на неё. Более того, есть даже одно неявное удобство. Поскольку в начале таких переменных хранится и байт текущей длины строки, то он тоже записывается в файл. Поэтому, при чтении записанной таким образом строки не встает проблемы с определением её действительного размера. Однако надо понимать что таким образом Вы получаете не текстовый файл. Во первых, байт длины может принимать любые значения от 0 до 255, и не всегда будет представлять собой код печатного символа. Во вторых, если, например, строка объявлена как ShortString. То даже если она в данный момент содержит в себе строку 'abcd', в файл будет записано 256 байт. 1 байт длины, 4 байта реальной строки и 251 байт "мусора", не обязательно печатного :).

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

  Stream.WriteBuffer(pChar(Str)^,Length(Str));  // так пишу

или так:

  BlockWrite(f, pChar(Str)^,Length(Str));        // так пишу

Ну а потом, встает естественный вопрос – а как же это прочитать? Ведь неизвестно сколько байт занимает строка, а, следовательно, и неизвестно сколько байт из файла читать. Да и размер буфера неясен. Так вот. Об этом надо позаботиться заранее, ещё когда записываешь строку. Например, записать можно так:

var
  Stream :tStream;
  Str :AnsiString;
  Len :Longint;
...
  Len := Length(Str);
  Stream.WriteBuffer(Len,SizeOf(Len));          // длинна строки
  Stream.WriteBuffer(pChar(Str)^,Length(Str));  // сама строка

или так:

var
  f   :file;
  Str :AnsiString;
  Len :Longint;
...
  Len := Length(Str);
  BlockWrite(f,Len,SizeOf(Len));          // длинна строки
  BlockWrite(f,pChar(Str)^,Length(Str));  // сама строка 

Тогда, можно будет прочитать вот так:

  Stream.ReadBuffer(Len,SizeOf(Len));   // длинна строки
  SetLength(Str,Len);       // выделение памяти под строку
  Stream.ReadBuffer(pChar(Str)^,Len);   // чтение строки  

или так:

  BlockRead(f,Len,SizeOf(Len));   // длинна строки
  SetLength(Str,Len);       // выделение памяти под строку
  BlockRead(f,pChar(Str)^,Len);   // чтение строки  

Что, сложно? Ну, за возможность хранить строки длинной >255 символов приходится платить.

Если же тебе достаточно и 255 символов, то используй ShortString, или String[n].

Есть еще одно, на мой взгляд, замечание. Если Вы обратили внимание на тип переменной Len в моем примере, то возможно у Вас возник вопрос: А почему LongInt, а не Integer? Жаль если у Вас вопрос не возник – либо вы все знаете, либо ничего :). Для остальных поясню: дело в том, что тип LongInt фундаментальный тип, размер которого (4 байта) не будет меняться в последующих версиях Delphi. А тип Integer, это универсальный тип, размерность которого может меняться от версии к версии. Например, для 64-разрядных компьютеров он наверняка "вырастет" до 8-ми байт (64 бита). Лично мне, хочется, что бы файлы данных записанные моей старой версией программы могли быть нормально прочитаны более поздними версиями, возможно скомпилированными уже под 64-разрядной OS.

Использование строк в записях

Проблема возникает тогда, когда одно поле (или несколько полей) имеют тип динамической строки. В этом случае, часто возникает проблема схожая с проблемой записи динамической строки в файл – не все данные лежат в записи, динамические строки представлены в ней лишь указателями. Решается проблема также как и с записью строк в файл. Жаль только что тогда нельзя будет оперировать (записать/прочитать) целиком всей записью. Строки придется обрабатывать особо. Можно сделать, например, так:

var
  r :packed record
       f1 :Integer;
       f2 :array [1..30] of Double;
       f3 :AnsiString;
       f4 :Boolean;
       ...
     end
...
var
  Stream :tStream;
  s   :String;
  Len :Longint;
...
  // запись записи :) в поток
  s := r.f3;  r.f3 := ''; // дабы не писать неправильный адрес
  Stream.WriteBuffer(r,SizeOf(r));  // record с f3 равным Nil
  r.f3 := s; // восстановление значения f3
  Len := Length(s);
  Stream.WriteBuffer(Len,SizeOf(Len));      // длинна строки
  Stream.WriteBuffer(pChar(s)^,Length(s));  // сама строка
...
  // чтение записи из потока
  Stream.ReadBuffer(r,SizeOf(r));   // record (вместо строки лабуда)
  Stream.ReadBuffer(Len,SizeOf(Len));   // длинна строки
  SetLength(r.f3,Len);       // выделение памяти под строку
  Stream.ReadBuffer(pChar(r.f3)^,Len);   // чтение строки  

Обратите внимание на то, что перед записью в поток я делаю так, что бы в поле f3 попал указатель Nil. Если этого не сделать, то в поток попадет адрес текущего экземпляра динамической строки. При чтении, он будет прочитан в поле f3. Т.е. поле f3 станет указывать на какое-то место в памяти. При выполнении SetLength, поскольку Delphi сочтет что текущее значение f3 лежит по указанному адресу, будет попытка интерпретировать лежащую там информацию как динамическую строку. Если же в поток записать Nil, то SetLength, никуда лезть не будет – экземпляра-то нет.

Использование строк в качестве параметров и результатов функций размещенных в DLL.

Многие, наверное, пытались хоть раз написать собственную Dll. Если при этом Вы использовали соответствующего мастера Delphi, то наверняка видели комментарий который он вставляет в начало файла библиотеки:

{ Important note about DLL memory management: ShareMem must be the
  first unit in your library's USES clause AND your project's (select
  Project-View Source) USES clause if your DLL exports any procedures or
  functions that pass strings as parameters or function results. This
  applies to all strings passed to and from your DLL--even those that
  are nested in records and classes. ShareMem is the interface unit to
  the BORLNDMM.DLL shared memory manager, which must be deployed along
  with your DLL. To avoid using BORLNDMM.DLL, pass string information
  using PChar or ShortString parameters. }

Общий смысл этого эпоса в том, что если Ваша Dll экспортирует хотя бы одну процедуру или функцию с типом параметра соответствующим любой динамической строке (AnsiString например), или функцию, возвращающую результат такого типа. Вы должны обязательно и в Dll, и в использующей ее программе, первым модулем в списке импорта (uses) указать модуль ShareMem. И как следствие, поставлять со своей программой и Dll еще одну стандартную библиотеку BORLNDMM.DLL.

Вы не задумывались над вопросами: "Зачем все эти сложности?"; "Что будет если этого не сделать?" и "Можно ли этого избежать?"; "Если да, то как?" Если не задумывались, то самое время сделать это.

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

procedure Y (var s :AnsiString);
// Добавляет в конец строки символ '$', если его там еще нет
begin
  if (Length(s)>0) and (s[Length(s)]<>'$')  then
    s := s+'$';
end;
...
procedure X;
var
  Str :AnsiString;
begin
  ...
  Str := IntToStr(100);
  X(Str);
  ...
end;

Сначала, при выполнении процедуры X, функция IntToStr(100) создаст экземпляр динамической строки '100', и ее адрес будет помещен в переменную Str. Затем, адрес этой переменной будет передан процедуре Y. В ней, при выполнении оператора s := s+'$', будет создан экземпляр новый строки '100$', Экземпляр старой строки '100' станет не нужным и память, выделенная для него при создании, будет освобождена. Кроме того, при завершении процедуры X, будет освобождена и память, выделенная для строки '100$', так как перестанет существовать единственная ссылка на нее - переменная Str.

Всё вроде бы хорошо. Но до тех пор, пока обе процедуры располагаются в одном исполняемом модуле (EXE-файле). Если например поместить процедуру Y в Dll, а процедуру X оставить в EXE, то будет беда.

Дело в том, что выделением и освобождением памяти для экземпляров динамических строк занимается внутренний менеджер памяти Delphi-приложения. Использовать стандартный менеджер Windows очень накладно. Он слишком универсален, и потому медленный, а строки очень часто требуют перераспределения памяти. Вот разработчики Delphi и создали свой. Он ведет списки распределенной и свободной памяти своего приложения. Так вот, вся беда в том, что Dll будет использоваться свой менеджер памяти, а EXE свой. Друг о друге они ничего не знают. Поэтому, попытка освобождения блока памяти выделенного не своим менеджером приведёт к серьезному нарушению в его работе. Причем, это нарушение может проявиться далеко не сразу, и довольно необычным образом.

В нашем случае, память под строку '100' будет выделена менеджером EXE-файла, а освобождаться она будет менеджером DLL. То же произойдет и с памятью под строку '100$', только наоборот.

Для преодоления этой проблемы, разработчики Delphi создали библиотеку BORLNDMM.DLL. Она включает в себя еще один менеджер памяти :). Использование же модуля ShareMem, приводит к тому, что он заменяет встроенный в EXE (DLL) менеджер памяти на менеджер расположенный в BORLNDMM.DLL. Т.е., теперь и EXE-файл и DLL, будут использовать один, общий менеджер памяти.

Здесь важно отметить то, что если какой-либо из программных модулей (EXE или DLL) не будут иметь в списке импорта модуля ShareMem, то вся работа пойдет насмарку. Опять будут работать несколько менеджеров памяти. Опять будет бардак.

Можно обойтись и без внешнего менеджера памяти (BORLNDMM.DLL). Но для этого, надо например заменить встроенный в DLL менеджер памяти, на менеджер, встроенный в EXE. Такое возможно. Есть даже соответствующая реализация от Emil M. Santos, называемая FastShareMem. Найти ее можно на сайте http://www.codexterity.com. Она тоже требует обязательного указания ее модуля FastShareMem в списках используемых модулей EXE и DLL. Но, она по крайней мере не требует таскать за собой ни каких дополнительных DLL'лек.

 

Ну вот, наконец-то и все. Теперь, Вы знаете о строках почти столько же как я :).

Конечно, этим тема не исчерпывается. Например, я ничего не рассказал о мультибайтовых строках (MBCS) используемых для мультиязыковых приложений. Может и еще что-то забыл рассказать. Но, не расстраивайтесь. Я свои знания получал, изучая книги, тексты своих и чужих программ, код сгенерированный компилятором, и т.п. Т.е., все из открытых источников. Значит это все доступно и Вам. Главное, чтобы Вы были любознательными, и почаще задавали себе вопросы "Как?", "Почему?", "Зачем?". Тогда во всем сможете разобраться и сами.



 Desingn by Шишкин Алексей (Лёха).
 ©2004-2008 by sources.ru.