Прежде всего давай четко определимся, что такое тип String в Delphi. В зависимости от директив компилятора тип String может интерпретироваться как ShortString или AnsiString.
- {$H+} или {$LongStrings On} String = AnsiString. По умолчанию. - {$H-} или {$LongStrings Off} String = ShortString. - Можно управлять из окна настроек проекта – “Compiler” -> “Huge strings” - Если при определении типа String указана длина строки: String[32], то вне зависимости от установок компилятора это будет означать объявление ShortString соответствующего размера.
ShortString - короткая строка - sstr: ShortString; - это обычная паскалевская строка, то есть массив (цепочка) символов с зарезервированным в начале байтом для длины строки. - Соответственно максимальная длина короткой строки = 0..255. - Может объявляться так: sstr: String[макс.длина строки];. - Если сослаться на нулевой элемент: sstr [0], то получишь длину строки в виде Char, но лучше использовать для этого Length(sstr)(она делает тоже самое). Ord(sstr[0]) = Length(sstr) - При определении адреса короткой строки (с помощью @sstr или Addr(sstr)) возвращается указатель на байт длины. - Индексы символов (sstr [ i]) начинается с 1. - Значение в байте длины может быть меньше, чем размер строковой переменной : Byte(sstr[0]) ‹= SizeOf(sstr). То есть, хотя длина строки может и меняться, память, занимаемая ShortString, всегда равна 256 байтам. - Короткие строки удобны для записи текстовой информации в файл (т.к. они фиксированной длины).
Чтобы преобразовать ShortString в PChar надо ручками добавить в конец строки терминальный нуль #0 и вернуть адрес первого символа :
function ShortStringToPChar (sstr: ShortString): PChar; begin sstr := sstr + #0; Result := @sstr[1]; end;
AnsiString - длинная строка - astr: AnsiString; - это длинная строка состоящая из символов AnsiChar (тоже, что и Char, пока). Этот тип принят по умолчанию, то есть если сделать определение: var astr: String; - то astr определится как AnsiString.
AnsiString можно представить в виде записи: type // Это описательное определение, TAnsiString = record // не предназначенное для компиляции !!! RefCount: LongWord; //Счетчик ссылок Length: LongWord; // Длина строки Data: array[1..Length+1] of AnsiChar; //Массив символов, нумерация с единицы end;
Так что AnsiString - это указатель на запись, только ссылается он не на начало записи, а на начало поля Data. Ты можешь это проверить сам (Приложение 1). Объяви заголовок строки вот так:
type PStrRec = ^StrRec; StrRec = packed record // Заголовок строки 8 байт refCnt: Longint; // 4 байта – счетчик ссылок length: Longint; // 4 байта – длина строки end;
В коде объяви строку и указатель на структуру заголовка: var S: String; P: PStrRec;
Получи указатель на заголовок строки S:
P := Pointer(Integer(S) - ;
Здесь ты получил указатель на строку S и отступил от начала строки на 8 байт (длину заголовка StrRec). Теперь ты можешь смотреть значение счетчика, получать длину строки и даже менять их (но это не рекомендуется)
- Получить символ по его номеру (индексу) можно, обращаясь со строкой, как с динамическим массивом: astr[ i].
Delphi проверяет: попадает ли индекс в границы диапазона, как и с динамическими массивами (если включена проверка диапазона {$R+}). Но пустая длинная строка представлена нулевым указателем. Поэтому проверка границ пустой строки (при обращении к символу строки по индексу) приводит к ошибке доступа вместо ошибки выхода за границы диапазона.
По умолчанию проверка диапазона выключена ({$R-} или {$RangeChecks Off}), но лучше всегда ее включать, т.к. она помогает отловить многие ошибки, а в релизе сделает прогу менее чувствительной к BufferOverflow-атаке (т.н. строковое и массивное переполнение). По этой же причине всегда включай {$O+} или {$OverflowCheks On}. Выключай их только при серьезной проблеме с производительностью и только в критичных участках кода.
- Длина строки (Length) может изменяться с помощью функции SetLength. На настоящий момент максимальная длина длинной строки = 2 Гб (т.к. размер длины строки - 4 байта). Минимальная длина – 4 байта (пустая строка) Функция SizeOf(astr) возвратит 4 байта при любой длине строки, т.е. возвращается размер указателя. - В конец строки автоматически записывается терминальный нуль #0 (но он не включается в общую длину строки). Поэтому строку легко преобразовать в тип PChar: PChar(astr). С короткой строкой такое преобразование не получится, потому что у нее в конце терминального нуля нет ! - AnsiString поддерживает многобайтовые строки. В отличие от PChar в AnsiString могут быть любые символы, даже несколько терминальных нулей! Но некоторые строковые фукции думают, что терминальный нуль в строке только один и что он в конце (например SysUtils.AnsiPos). Учитывай это! - Счетчик ссылок RefCount используется в операциях присваивания и управляет жизненным циклом строки. Придуман для экономии памяти. Если мы копируем строку в другую переменную ( somestr := astr, то настоящего копирования памяти не происходит, а просто копируется указатель (AnsiString ведь указатель) и увеличивается на 1 счетчик ссылок. А вот если исходная строка изменяется (somestr := astr + 'A', то тогда создается новая уникальная запись со своим счетчиком ссылок. - Если тебе очень нужно создать именно уникальную строку, а не увеличить счетчик ссылок, то используй функцию: procedure UniqueString (var str: string). Она гарантирует, что строка str до этого больше нигде не использовалась и, изменяя эту строку, ты больше нигде не напортачишь. Например может потребоваться указатель на строку PChar при работе с API-функциями. Тогда создай уникальную строку, преобразуй ее в PChar и спокойно передавай в функцию не опасаясь побочных эффектов.
Пример. Вывод русского текста в консольное окно. Это почему-то у многих вызывает трудности.
procedure WriteRussianText(Msg :String); // вывод строки в консольное окно в OEM кодировке begin UniqueString(Msg); // получим уникальный экземпляр строки Windows.CharToOem(PChar(Msg),PChar(Msg)); // преобразуем его Write(Msg); // выведем текст на консоль end;
- Компилятор сам управляет длинными строками, не доверяя это программисту, и вставляет вместо операций со строками свои процедуры. Память для длинной строки выделяется динамически. Компилятор почти всегда (про почти: см. в третьей статье - Переменная Result) инициализирует длинные строки: (примерно так) Pointer(astr) := nil; При выходе из области видимости (из процедуры, при разрушении объекта) компилятор вставляет процедуру финализации и освобождения динамической памяти примерно так: System._LStrClr(S);
PChar - нультерминальная строка - pstr: PChar; - это нультерминальная строка (zero-terminated). Так называется, потому что представляет собой указатель на цепочку символов, заканчивающуюся терминальным нулем #0. Ее еще называют сишной строкой (из языка С, там она определяется как char*). - type PChar = ^Char; - Используется для вызова ANSI-версий API-функций (типа CreateFileA). VCL использует только ANSI-версии API-функций для совместимости со всеми версиями Windows, поэтому вызов CreateFile идентичен CreateFileA. В модуле SysUtils сделаны функции-оболочки для многих API-функций, в которые надо передавать String вместо PChar (все-таки PChar не родной паскалевкий тип). - Delphi хранит терминальный нуль в конце длинных и широких строк для удобства преобразования в PChar, PAnsiChar и PWideChar. - Можно рассматривать PChar, как указатель на array of Char. В этом случае индексы начинаются с нуля. Проверка на выход за границу массива не выполняется! Подпрограмма, сканирующая строку, ищет только #0. - При приведении AnsiString к PChar надо помнить, что Delphi автоматически уничтожает строку, когда она больше не нужна (т.е. когда счетчик ссылок равен 0, например при выходе из процедуры) , и тогда в переменной PChar может оказаться некорректный указатель. Поэтому надо быть осторожным при сохранении указателя PChar для дальнейшего использования (pstr := PChar(astr) , а лучше делать это приведение только при передаче параметров в API-функцию. То же относится и к приведению WideString к PWideChar. Прочитай еще про UniqueString выше. - Операции с PChar проходят медленнее, чем операции с AnsiString, потому-что сначала Delphi сканирует всю строку PChar, что определить ее длину, а уже потом производит с ней действия. - PChar автоматически преобразуется в AnsiString: astr := patr; , но эта операция проходит медленно.
Чтобы избежать накладных расходов можно использовать: procedure SetString(var Str: string; Buffer: PChar; Length: Integer); устанавливает длину Str равной Length и копирует Length символов из Buffer в Str (если Str - короткая строка, то Length должна быть ‹256). Эта процедура используется в исходном коде многих строковых функций Delphi.
PWideChar - PWideChar - скажем так, это "широкая" нультерминальная строка. - Для хранения символа используется 2 байта. Поддерживает стандарт Unicode. - На конце - терминальный нуль #0. - Может рассматриваться как указатель на array of WideChar. Нумерация начинается с нуля. Так же как и PChar - не контролирует выход за границы массива! - Используется для передачи параметров в Unicode-версии API-функций (типа CreateFileW), подпрограммы OLE и COM. - Создать строку PWideChar из String можно с помощью функции StringToOleStr. Только помни, что строка создается динамически и потом надо освободить память с помощью API-функции SysFreeString.
procedure SomeProc(S: String); var OleStr: PWideChar; begin OleStr := StringToOleStr(S); // создаешь широкую строку try CallSomeOLEProc(OleStr); // че-то там вызываешь finally SysFreeString (OleStr); // Освобождаешь память end; end;
WideString - широкая строка - wstr: WideString; - широкая строка. Хранит строку в формате Unicode, то есть использует для хранения символа 2 байта (16-битовые символы WideChar). - Первые 256 символов в Unicode (WideChar) и AnsiChar (Char) совпадают. - Также, как и AnsiString, WideString отслеживает свою длину, дописывает в конец #0 (может быть преобразована в PWideChar), но не содержит счетчика ссылок, поэтому любое присваивание приводит к копированию строки в памяти. - Delphi автоматически по мере надобности расширяет "узкие" строки и сужает "широкие". - При приведении WideString к AnsiString используется кодовая страница ANSI. Преобразование не-ANSI-символов (с индексом больше 255) происходит, как принято в Windows по умолчанию (то есть зависит от национальных настроек). При этом приведении в строке могут оказаться многобайтовые символы. Чтобы управлять процессом преобразования надо напрямую вызывать API-функцию WideCharToMultiByte.
Многобайтовые строки - для сведения - Многобайтовая строка - это строка, в которой символ может занимать более 1 байта (в Windows используются 2 байта). Не надо путать многобайтовые строки с Unicode - это разные вещи, хотя они приводятся друг к другу. В Unicode символ всегда занимает 2 байта, а многобайтовой строке он может занимать 1 или 2 байта (во как!). - Нужны такие строки для некоторых национальных языков (японского, китайского), где используется больше 256 символов. - Байт в многобайтовой строке может быть: одинарным символом, ведущим байтом (первым байтом символа) и завершающим байтом (т.е. вторым байтом). При обработке таких строк надо учитывать этот момент, т.к. символ, выглядящий как 'A' может оказаться вторым байтом многобайтового символа. - Delphi не всегда корректно работает с многобайтовыми строками, и в этом случае нас опять спасает модуль SysUtils, где есть специальные функции. Для определения типа байта (по его индексу) в многобайтовой строке применяются функции SysUtils: ByteType и StrByteType:
type TMbcsByteType = ( mbSingleByte,// Одиночный однобайтовый символ mbLeadByte, // Первый байт многобайтового символа mbTrailByte);// Второй байт многобайтового символа
function ByteType (const S: String; Index: Integer): TMbcsByteType;
- В реальной работе многобайтовая строка может получиться при приведении WideString (Unicode) к AnsiString (String): wstr := astr; - Если планируется программу (или компонент) продавать (да еще за бугром), то при встрече с многобайтовыми строками надо хотя бы корректно завершить работу (конечно лучше, чтобы прога их поддерживала). Встает вопрос: Как определить нужна ли поддержка многобайтовых строк? Ответ: В переменной var SysLocale: TSysLocale; хранится информация о региональных установках Windows по умолчанию и, если поддержка многобайтовых срок нужна, то SysLocale.FarEast = True. - Правильно обрабатывать такие строки особенно важно для имен файлов. Ты ведь собираешься со своей программой на мировой рынок выходить .
На сладкое: resourcestring
- Что ты делаешь, когда тебе надо в программу включить строковые ресурсы? Наверное создаешь файл с расширением mystringresource.rc, пишешь в него по специальным правилам свои строки, компилишь файл в res c помощью brcc32.exe, включаешь в свой экзешник директивой компилятора {$R mystringresource.res} и потом загружаешь из него строки с помошью API-функций. Скажи мне, а нужна тебе такая морока? Разработчики Delphi, как я постоянно убеждаюсь, далеко не дураки и всё уже для тебя придумали. - Всё что требуется от тебя - это объявить с своем модуле строковую константу вот так:
resourcestring MyResString = 'Hello World';
- ВСЁ! Теперь твоя строка будет сохранена в строковом табличном ресурсе, под уникальным номером. Можешь обращаться с ней, как с обычной строковой константой. После компиляции проги можешь открыть ее ResHacker'ом или Restorator'ом и среди других строк увидишь свою. Учти, что номер(идентификатор) ресурса присваивается автоматически и может меняться от компиляции к компиляции. Так что особо на него не полагайся. - Компилятор заменяет строковую константу на вызов LoadResSring для загрузки ресурса во время выполнения программы. - Эти ресурсные строки очень полезны, если потом надо будет локализовать программу для другого языка. Поэтому как resourcestring надо объявлять все строковые констаты в программе: сообщения об ошибках и не только, хинты-подсказки и т.п. Тоже самое и даже в большей степени относится к разработке компонентов. - Delphi сам назначает номера строковым ресурсам, так что можешь не беспокоиться насчет конфликтов идентификаторов resourcestring из разных модулей. - Если ресурсная строка используется в качестве строки формата (например для функции SysUtils.Format), то обязательно включай в нее спецификаторы позиций (потом удобнее переводить будет, т.к. в другом языке и порядок слов другой):
resourcestring ResErrorMsg = 'Ошибка: невозможно сделать %0:s для %1:s , потому-что %2:s';
- Адрес resourcestring - указатель типа PResStringRec, который ты можешь использовать для получения идентификатора ресурса во время работы программы:
type PResStringRec = ^TResStringRec; TResStringRec = packed record Module: ^Cardinal; // Модуль из которого загружен ресурс (чаще всего твой экзешник) Identifier: Integer; // Идентификатор строкового ресурса end;
- Получить номер строкового ресурса можно так:
var ResID: Integer; ResID := PResStringRec(@MyResString).Indentifier;
Успехов! Орехов Роман aka tripsin
Приложение 1.
unit Unit1; // Модуль для изучения AnsiString и его счетчика ссылок interface
type TForm1 = class(TForm) Button1: TButton; Memo1: TMemo; procedure Button1Click(Sender: TObject); // 1. Сначала попробуй процедуру без передачи параметра, потом // раскомментируй параметр везде и проверь, как он передается, // 2. при этом не забудь закомментировать строки обозначенные //*** // 3. Напоследок попробуй передать параметр как (var S: String) procedure GetStringInfo{(S: String)}; end;
var Form1: TForm1; S: String; implementation
{$R *.dfm} procedure TForm1.GetStringInfo{(S: String)}; type PStrRec = ^StrRec; StrRec = packed record // Заголовок строки 8 байт refCnt: Longint; // 4 байта length: Longint; // 4 байта end; var S: String; //*** P: PStrRec; pp: ^Byte; begin S := 'AAAAAAAAAA'; // Так S будет ссылаться на литерал //*** // S := S + 'a'; // Раскомментируешь - S будет в динамической памяти P := Pointer(Integer(s) - ; // Смещаемся к началу заголовка
// Проверяем размер переменной S Memo1.Lines.Add('SizeOf(S) = ' + IntToStr(SizeOf(S))); // Проверяем размер заголовка Memo1.Lines.Add('Sizeof(StrRec) = ' + IntToStr(Sizeof(StrRec))); Memo1.Lines.Add('refCnt = ' + IntToStr(P.refCnt)); // Счетчик ссылок Memo1.Lines.Add('length = ' + IntToStr(P.length)); // Длина строки pp := Pointer(S); // Получаем указатель на строку inc(pp, P.length); // Передвигаемся в конец строки ... // ... и смотрим, что там Memo1.Lines.Add('Байт в конце строки = ' + IntToStr(Integer(pp^))); Memo1.Lines.Add('S = ' + s); // Выводим строку end;
procedure TForm1.Button1Click(Sender: TObject); begin S := 'AAAAAAAAAAAAAAAA'; GetStringInfo{( S)}; end;