.
Строки в Delphi (Pchar, WideString, TStream, поток)
Автор megabax   
17.02.2010 г.
New Page 1

Строки в Delphi (Pchar, WideString, TStream, поток)

Скачать исходники  с примерами к уроку можно здесь.

Для начала определим, какова сущность типа String в Delphi. В зависимости от директив компилятора это может быть либо ShortString, либо AnsiString. по умолчанию в компиляторе Delphi действует директива {$H+} или {$LongStrings On}. В этом случае String У нас равносилен типу AnsiString. Если действует директива {$H-} или {$LongStrings Off} то String у нас становиться  ShortString.

Данной директивой можно управлять путем настроек опций проекта: "Project" -> "Options"*

Строки в Delphi (Pchar, WideString, TStream, поток)

Длина строк настраивается на закладке "Compiler", опция "Huge strings"*:

Строки в Delphi (Pchar, WideString, TStream, поток)

Теперь чем отличается ShortString от AnsiString

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. Функция Szieof, в применении к строке всегда, независимо от длины строки возвратит 4 байта - размер указателя. Именно поэтому сроку в Delphi нельзя просто взять и сериализовать в поток. в частности, такой код работать не будет:

procedure TEasyGameContainer.Serialize(AStream:TStream);
var i,cn:integer; lClassName:string;
begin
   inherited Serialize(AStream);
   cn:=FObjects.Count-1;
   AStream.Write(cn,sizeof(cn));
   for i:=0 to cn do
   begin
      lClassName:=TEasyGameObject(FObjects[i]).ClassName;
     AStream.Write(lClassName,sizeof(lClassName));
     TEasyGameObject(FObjects[i]).Serialize(AStream);
   end;
end;

procedure TEasyGameContainer.Unserialize(AStream:TStream);
var i,cn:integer; lClassName:string;
      lClass:TPersistentClass;
      lObject:TPersistent;
begin
    inherited Unserialize(AStream);
   AStream.Read(cn,sizeof(cn));
   for i:=0 to cn do
   begin
      AStream.Read(lClassName,sizeof(lClassName));
      lClass:=FindClass(lClassName);
      lObject:=lClass.Create;
      TEasyGameObject(lObject).Unserialize(AStream);
      AddGameObject(TEasyGameObject(lObject));
    end;
end;

так как при чтении строки из потока будет прочитан "мусор". Как правильно писать и читать строки из потока описано здесь.

Для того, что бы поэкспериментировать со строкой AnsiString, создадим небольшой тестовый пример. Для начала в раздел тпиов внесем определение структуры заголовка AnsiString:

PStrRec = ^StrRec;
     StrRec = packed record // Заголовок строки 8 байт
     refCnt: Longint; // 4 байта - счетчик ссылок
     length: Longint; // 4 байта - длина строки
end;

Затем добавим строку ввода (TEdit), кнопочку (TButton) и три метки (TLabel), эти компоненты можно найти на закладке Standart*:

TEdit, строки в Delphi

Теперь напишем обработчик события OnClick кнопочки:

procedure TStringsSampleForm.btnAnsiStrDemoClick(Sender: TObject);
var
S: String;
P: PStrRec;
pp: ^char;
begin
    s:=edString.Text;
    P:=Pointer(Integer(S)-8);
    Label1.Caption:='refCnt = ' + IntToStr(P.refCnt);
    Label2.Caption:='length = ' + IntToStr(P.length);
    pp := Pointer(S);
    inc(pp,2);
    Label3.Caption:='Третий символ '+pp^;
end;

Вот что мы увидим, запустив эту программу:

Pchar, WideString, пример строки в Delphi

И так, пойдем дальше. Обращаться к отдельным символам AnsiString можно аналогично ShortString, как к элементам массива - : astr[ i]. 

Delphi проверяет: попадает ли индекс в границы диапазона, как и с динамическими массивами (если включена проверка диапазона {$R+}). Но пустая длинная строка представлена нулевым указателем. Поэтому проверка границ пустой строки (при обращении к символу строки по индексу) приводит к ошибке доступа вместо ошибки выхода за границы диапазона.  По умолчанию проверка диапазона выключена ({$R-} или {$RangeChecks Off}), но лучше всегда ее включать, т.к. она помогает отловить многие ошибки, а в релизе сделает прогу менее чувствительной к BufferOverflow-атаке (т.н. строковое и массивное переполнение). По этой же причине всегда следует включать {$O+} или {$OverflowCheks On}. Выключать их рекомендуется только при серьезной проблеме с производительностью и только в критичных участках кода. 

Длина строки (Length) может изменяться с помощью функции SetLength. На настоящий момент максимальная длина длинной строки = 2 Гб (т.к. размер длины строки - 4 байта). Минимальная длина – 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 и спокойно передавайте в функцию не опасаясь побочных эффектов. 

Компилятор сам управляет длинными строками, не доверяя это программисту, и вставляет вместо операций со строками свои процедуры. Память для длинной строки выделяется динамически. Компилятор почти всегда инициализирует длинные строки: (примерно так) Pointer(astr) := nil; При выходе из области видимости (из процедуры, при разрушении объекта) компилятор вставляет процедуру финализации и освобождения динамической памяти примерно так: System._LStrClr(S); 

PChar - нультерминальная строка 

  • pstr: PChar; - это нультерминальная строка (zero-terminated, иначе еще называется срока с завершающим нулем). Так называется, потому что представляет собой указатель на цепочку символов, заканчивающуюся терминальным нулем #0. Ее еще называют сишной строкой (из языка С, там она определяется как char*). 

  • Определяется тип PChar как 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. 

 

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; 

 


Скриншоты, помеченные знаком * , являются цитатами и иллюстрациями  программного продукта "Delphi", авторское право на который принадлежит "Borland Software Corporation".


 

 







 

Последнее обновление ( 18.02.2013 г. )