AnsiString — Тип Delphi


Содержание

AnsiString — общие вопросы

Для начала краткая информация

AnsiString — класс динамической строки емкостью до 232-1 символов. Базой для создания этого класса послужил паскалевский тип String, который был расширен в соответствии с возможностями C++.

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

Наследием Паскаля является также то, что символы в нем нумеруются с 1, а не с 0, как это принято в C/C++

Создание строки

Благодаря наличию перегруженных конструкторов строку можно создавать из большого количества базовых типов языка C++, например:

получение символа из строки

Получить символ из строки типа AnsiString можно с помощью индексного оператора:

Получение символьного массива, содержащего значение AnsiString

Функции библиотеки исполнения C и функции WinAPI требуют, чтобы строки передавались им в виде символьных массивов. В AnsiString для этого предназначены функции c_str и data. Разница между ними в том, что для пустой строки c_str возвращает «», а data — NULL. Это отличие может быть проиллюстрировано следующим примером:

Копирование значения AnsiString в символьный массив Unicode

Для этого предназначена функция WideChar, а размер требуемого массива можно узнать, воспользовавшись функцией WideCharBufSize

Как узнать, сколько символов в строке?

Для этого надо воспользоваться функцией Length.

Как удалить в строке концевые пробелы?

Это делают функции Trim, TrimLeft и TrimRight соответственно с обоих концов, только с начала и только с конца.

Работа с фрагментами строки

AnsiString предоставлякт возможность поиcка(Pos), вставки(Insert), удаления(Delete) и получения копии(SubString) фрагмента текста в строке:

Как зарезервировать строке определенный размер

Иногда бывает нужно зарезервировать для строки пространство, достаточное для хранения определенного количества символов. Сделать это можно при помощи функции SetLength

Работа со строковыми типами данных в Delphi

Строковый тип данных — один из самых часто используемых в программах тип. Действительно, без него не обходится практически ни один алгоритм. Даже программы, выполняющие исключительно математические операции, порой, написаны с использованием строковых типов данных.

Строка — это последовательность символов. В Object Pascal существует несколько строковых типов. Вот основные из них:

Для большинства целей подходит тип AnsiString (иногда называется Long String ).

Стандартные функции обработки строк:

1) Функция Length(Str: String) — возвращает длину строки (количество символов). Пример:

var
Str: String; L: Integer;
< . >
Str := ‘Hello!’ ;
L := Length(Str);

2) Функция SetLength(Str: String; NewLength: Integer) позволяет изменить длину строки. Если строка содержала большее количество символов, чем задано в функции, то «лишние» символы обрезаются. Пример:

var Str: String;
< . >
Str := ‘Hello, world!’ ;
SetLength(Str, 5);

3) Функция Pos(SubStr, Str: String) — возвращает позицию подстроки в строке. Нумерация символов начинается с единицы (1). В случае отсутствия подстроки в строке возращается 0. Пример:

var Str1, Str2: String; P: Integer;
< . >
Str1 := ‘Hi! How do you do?’ ;
Str2 := ‘do’ ;
P := Pos(Str2, Str1);

4) Функция Copy(Str: String; Start, Length: Integer) — возвращает часть строки Str, начиная с символа Start длиной Length. Ограничений на Length нет — если оно превышает количество символов от Start до конца строки, то строка будет скопирована до конца. Пример:

var Str1, Str2: String;
< . >
Str1 := ‘This is a test for Copy() function.’ ;
Str2 := Copy(Str1, 11, 4);

5) Процедура Delete(Str: String; Start, Length: Integer) — удаляет из строки Str символы, начиная с позиции Start длиной Length. Пример:

var Str1: String;
< . >
Str1 := ‘Hello, world!’ ;
Delete(Str1, 6, 7);

6) Функции UpperCase(Str: String) и LowerCase(Str: String) преобразуют строку соответственно в верхний и нижний регистры:

var Str1, Str2, Str3: String;
< . >
Str1 := ‘hELLo’ ;
Str2 := UpperCase(Str1); < Str2 = "HELLO" >
Str3 := LowerCase(Str1);

Строки можно сравнивать друг с другом стандартным способом:

var Str1, Str2, Str3: String; B1, B2: Boolean;
< . >
Str1 := ‘123’ ;
Str2 := ‘456’ ;
Str3 := ‘123’ ;
B1 := (Str1 = Str2); < B1 = False >
B2 := (Str1 = Str3);

Если строки полностью идентичны, логическое выражение станет равным True.

Дополнительные функции обработки строк:

В модуле StrUtils.pas содержатся полезные функции для обработки строковых переменных. Чтобы подключить этот модуль к программе, нужно добавить его имя ( StrUtils ) в раздел Uses .

1) PosEx(SubStr, Str: String; Offset: Integer) — функция аналогична функции Pos() , но позволяет задать отступ от начала строки для поиска. Если значение Offset задано (оно не является обязательным), то поиск начинается с символа Offset в строке. Если Offset больше длины строки Str, то функция возратит 0. Также 0 возвращается, если подстрока не найдена в строке. Пример:

uses StrUtils;
< . >
var Str1, Str2: String; P1, P2: Integer;
< . >
Str1 := ‘Hello! How do you do?’ ;
Str2 := ‘do’ ;
P1 := PosEx(Str2, Str1, 1); < P1 = 12 >
P2 := PosEx(Str2, Str1, 15);

2) Функция AnsiReplaceStr(Str, FromText, ToText: String) — производит замену выражения FromText на выражение ToText в строке Str. Поиск осуществляется с учётом регистра символов. Следует учитывать, что функция НЕ изменяет самой строки Str, а только возвращает строку с произведёнными заменами. Пример:

uses StrUtils;
< . >
var Str1, Str2, Str3, Str4: String;
< . >
Str1 := ‘ABCabcAaBbCc’ ;
Str2 := ‘abc’ ;
Str3 := ‘123’ ;
Str4 := AnsiReplaceStr(Str1, Str2, Str3);

3) Функция AnsiReplaceText(Str, FromText, ToText: String) — выполняет то же самое действие, что и AnsiReplaceStr(), но с одним исключением — замена производится без учёта регистра. Пример:

uses StrUtils;
< . >
var Str1, Str2, Str3, Str4: String;
< . >
Str1 := ‘ABCabcAaBbCc’ ;
Str2 := ‘abc’ ;
Str3 := ‘123’ ;
Str4 := AnsiReplaceText(Str1, Str2, Str3);

4) Функция DupeString(Str: String; Count: Integer) — возвращает строку, образовавшуюся из строки Str её копированием Count раз. Пример:

uses StrUtils;
< . >
var Str1, Str2: String;
< . >
Str1 := ‘123’ ;
Str2 := DupeString(Str1, 5);

5) Функции ReverseString(Str: String) и AnsiReverseString(Str: AnsiString) — инвертируют строку, т.е. располагают её символы в обратном порядке. Пример:

uses StrUtils;
< . >
var Str1: String;
< . >
Str1 := ‘0123456789’ ;
Str1 := ReverseString(Str1);

6) Функция IfThen(Value: Boolean; ATrue, AFalse: String) — возвращает строку ATrue, если Value = True и строку AFalse если Value = False. Параметр AFalse является необязательным — в случае его отсутствия возвращается пустая строка.

uses StrUtils;
< . >
var Str1, Str2: String;
< . >
Str1 := IfThen(True, ‘Yes’ ); < Str1 = "Yes" >
Str2 := IfThen(False, ‘Yes’ , ‘No’ );

Мы рассмотрели функции, позволяющие выполнять со строками практически любые манипуляции. Как правило, вместо строки с указанным типом данных, можно использовать и другой тип — всё воспринимается одинаково. Но иногда требуются преобразования. Например, многие методы компонент требуют параметр типа PChar , получить который можно из обычного типа String функцией PChar(Str: String) :

uses ShellAPI;
< . >
var FileName: String;
< . >
FileName := ‘C:\WINDOWS\notepad.exe’ ;
ShellExecute(0, ‘open’ , PChar(FileName), » , » , SW_SHOWNORMAL);

Тип Char представляет собой один-единственный символ. Работать с ним можно как и со строковым типом. Для работы с символами также существует несколько функций:

Chr(Code: Byte) — возвращает символ с указанным кодом (по стандарту ASCII):

Ord(X: Ordinal) — возвращает код указанного символа, т.е. выполняет противоположное действие функции Chr() :

var X: Integer;
< . >
X := Ord( ‘F’ );

Из строки можно получить любой её символ — следует рассматривать строку как массив. Например:

var Str, S: String; P: Char;
< . >
Str := ‘Hello!’ ;
S := Str[2]; < S = "e" >
P := Str[5];

В этой статье описаны основные приёмы работы со строковыми типами данных. Как правило, этих данных достаточно для написания любого алгоритма.

Ссылки по теме

Популярные статьи
Информационная безопасность Microsoft Офисное ПО Антивирусное ПО и защита от спама Eset Software


Бестселлеры
Курсы обучения «Atlassian JIRA — система управления проектами и задачами на предприятии»
Microsoft Office 365 для Дома 32-bit/x64. 5 ПК/Mac + 5 Планшетов + 5 Телефонов. Подписка на 1 год. Электронный ключ
Microsoft Windows 10 Профессиональная 32-bit/64-bit. Все языки. Электронный ключ
Microsoft Office для Дома и Учебы 2020. Все языки. Электронный ключ
Курс «Oracle. Программирование на SQL и PL/SQL»
Курс «Основы TOGAF® 9»
Microsoft Windows Professional 10 Sngl OLP 1 License No Level Legalization GetGenuine wCOA (FQC-09481)
Microsoft Office 365 Персональный 32-bit/x64. 1 ПК/MAC + 1 Планшет + 1 Телефон. Все языки. Подписка на 1 год. Электронный ключ
Windows Server 2020 Standard
Курс «Нотация BPMN 2.0. Ее использование для моделирования бизнес-процессов и их регламентации»
Антивирус ESET NOD32 Antivirus Business Edition
Corel CorelDRAW Home & Student Suite X8

О нас
Интернет-магазин ITShop.ru предлагает широкий спектр услуг информационных технологий и ПО.

На протяжении многих лет интернет-магазин предлагает товары и услуги, ориентированные на бизнес-пользователей и специалистов по информационным технологиям.

Хорошие отзывы постоянных клиентов и высокий уровень специалистов позволяет получить наивысший результат при совместной работе.

String и PAnsiString

Я пишу программу на определение IP адресов в сети. И в функцию надо передавать значение типа PAnsiString.

Если поступать так: IP:=’192.168.1.1′, то все работает. Но сканирование происходит в цикле
и надо присваивать IP:=’192.168.1.’+IntToStr(‘счетчик цикла’). Пробовал сначала присваивать это значение переменной типа String:

str:=’192.168.1.’+IntToStr(‘счетчик цикла’),
а потом

IP:=PAnsiString(str).
Присваивает только первый символ.
Как в цикле переменной IP присваивать значение.

14.05.2011, 11:09

Разработать функцию Add(const S1,S2:string):string
Помагите написать функцию. Пробовал, сам — получился бред! Разработать функцию Add(const.

Описание полей данных пользовательских классов : БИБЛИОТЕКА имя – String автор – String стоимость – float
1. Определить в отдельном программном модуле (*.pas) пользовательский класс в соответствии с.

Implicit string cast from ‘AnsiString’ to ‘string’ и т.д
Помогите исправить ошибки в программе. type TStudent=record group:shortstring; //группа.

Создайте класс Animal. Добавьте поля string Name, string Kind, string Areal, int Population
Здравствуйте! По C# есть задачка, с которой я так и не разобрался :( Задача: Создайте.

Создайте класс Animal. Добавьте поля string Name, string Kind, string Areal, int Population
Создайте класс Animal. Добавьте поля string Name, string Kind, string Areal, int Population.

AnsiString — Тип Delphi

В выражениях Delphi поддерживает три физических строковых формата: короткий (ShortString), длинный (LongString) и широкий (WideString). Их можно комбинировать в операторах присваивания и выражениях (все необходимые преобразования Delphi выполняет автоматически).
Переменные типов AnsiString и WideString — это динамически распределяемые массивы символов, максимальная длина которых ограничивается только наличием памяти. Разница между ними состоит в том, что в AnsiString знаки записываются в формате char, а в WideString— в формате WideChar. Обычно вполне достаточно одного типа AnsiString, однако при работе с международными наборами символов, такими как UNICODE, удобнее использовать WideString.
Тип ShortString—это, по существу, массив Array [0..255] of char. Первый его элемент задает динамическую длину строки, которая может принимать значения от 0 до 255 символов. Символы, составляющие строку, занимают места от 1 до 255. Тип ShortString предназначен, в основном, для обеспечения совместимости с ранними версиями Delphi и Borland Pascal.
Логический строковый тип именуется просто String. Отнесение его к типу AnsiString или ShortString задается командой $Н. По умолчанию задается < $Н+>, и String совпадает с AnsiString. Если задать команду <$Н- >, то String будет совпадать с ShortString и иметь максимальную длину, равную 255 символам.
Для совместимости с другими языками программирования в Delphi поддерживается класс строк с конечным нулем. Зарезервированных слов или идентификаторов для этого класса не существует.
Строки с конечным нулем состоят из ненулевых символов и оканчиваются символом с порядковым номером 0 (#0). В отличие от типов AnsiString, ShortString и WideString, строки с нулевым окончанием не имеют указателя длины. Конец в этих стооках обозначается нулем.
Физически строки с нуль-окончанием подобны массивам символов с нумерацией элементов от нуля, наподобие array [ 0 . . X] of char, где Х — некоторое положительное целое, большее нуля, хотя никаких объявлении подобного рода не происходит. Вместо этого определяется переменная-указатель PChar и распределяется необходимый объем памяти. При необходимости строке AnsiString можно присвоить тип PChar.
В табл. 1.7 перечислены некоторые процедуры и функции обработки данных строковых типов.

Совет: Программисты, работающие на С, привыкли записывать все строки в массивы с нуль-окончанием. Фактически они применяют в выражениях не строковые переменные, а указатели на них. Программисты, работающие на Basic, привыкли использовать строку как одно целое. Для типа AnsiString из Delphi годятся оба подхода.

AnsiString — Тип Delphi

цитата:
The representation of the classic AnsiString type was the following:
-8: Ref count
-4: length
String reference address: First char of string

In Delphi 2009 the representation for reference-counted strings becomes:
-12: Code page
-10: Elem size
-8: Ref count
-4: length
String reference address: First char of string

Так что вы неправы дважды — 1) что это было «в сильно раньшие времена»; 2) про ссылку на heap. Всё хранится вместе, никаких ссылок.

цитата: bislomet:
olivenoel
а какие из string (http://docwiki.embarcadero.com/RADStudio/XE3/en/String_Types) -ов Вы при этом имеете в виду?
Если речь идет о null-terminated — то они вполне себе user managable, а не automatic. И вполне могут утекать куда угодно.
Уточните!

FastMM (или кто там в ХЕ2), говорит unexpected memory leak UnicodeString x1.


Погуглила немного, оказывается такое имеет место быть, если строка является частью структуры (record). Но есть подозрение на одно место, где в массив AnsiChar присвается литеральное выражение (корое вроде по сути UnicodeString)

5. olivenoel , 26.06.2013 10:09
6. bislomet , 26.06.2013 11:46
Spirit-1
Вы заблуждаетесь. Лучше почитайте help к delphi xe3 (по ссылке, которю я давал выше)
Вы говорите об эпохе «Delphi до XE» — а это таки «сильно раньшие времена»

[pre|AnsiString represents a dynamically allocated string whose maximum length is limited only by available memory.

An AnsiString variable is a structure containing string information. When the variable is empty — that is, when it contains a zero-length string, the pointer is nil and the string uses no additional storage. When the variable is nonempty, it points to a dynamically allocated block of memory that contains the string value. This memory is allocated on the heap.
[/pre]

Добавление от 26.06.2013 11:47:

olivenoel
не путайте строку с array of char, это же не «C»

olivenoel
не путайте строку с array of char, это же не «C»

не буду Но вопрос остается, может ли «потеря» младших (старших) байтов при автоконвертации UnicodeString в AnsiString (такая конвертация тоже имеет место быть в виде

) вести к утечкам?

проблему вроде бы решила. Для одного из классов с Р. в поле и New (для поля) в конструкторе, не был переопределен деструктор, в котором должен быть Dispose для Р. -поля.

7. olivenoel , 26.06.2013 12:09
8. Spirit-1 , 26.06.2013 14:42
bislomet
Причём тут AnsiString? Я вам привёл кусок описания для нового юникодного (!) String. Обратите внимание, что на самом деле подход практически не поменялся. Добавились поля Code Page и Elem Size по сравнению со стрингами до D2009 (ссылаюсь на Канту опять же). Ну и если сравнивать с AnsiString, то, конечно, поле длинны стало четырёхбайтовым.
9. MBo , 26.06.2013 15:19
Spirit-1

Ну и если сравнивать с AnsiString, то, конечно, поле длинны стало четырёхбайтовым.

В Ansistring оно тоже было четырехбайтовым. Возможно, имелись в виду ShortString?

10. Spirit-1 , 26.06.2013 15:23
Да, насчёт длины ерунду сказал. Остальное всё в силе.
11. bislomet , 26.06.2013 15:52
Spirit-1
Вы опять-таки заблуждаетесь. Чуть дальше в том же help написано:
The UnicodeString type has exactly the same structure as the AnsiString type.

Т.е. все, что не является ShortString — устроено именно таким образом — по мнению разработчиков
12. Spirit-1 , 26.06.2013 16:14
bislomet
Попробуйте смотреть чуть дальше хэлпа от XE3.

Повторюсь, что начиная с версии D2009 эти два типа суть одно и то же, поскольку тип AnsiString определен как UnicodeString с фиксированной Code Page.
Я же сравниваю с AnsiString-ом, который был до 2009-ой Delphi. И привёл выдержку из книги — какая структура стринга была до 2009-й версии и какая стала после.

На всякий случай повторю:

цитата:
The representation of the classic AnsiString type was the following:
-8: Ref count
-4: length
String reference address: First char of string

In Delphi 2009 the representation for reference-counted strings becomes:
-12: Code page
-10: Elem size
-8: Ref count
-4: length
String reference address: First char of string

Читайте внимательно, если кроме дойча английским разумеете.

13. bislomet , 26.06.2013 17:38
Spirit-1
Повторюсь, что начиная с версии D2009 эти два типа суть одно и то же, поскольку тип AnsiString определен как UnicodeString с фиксированной Code Page.
Это утверждение никоим образом не противоречит моему и не подтверждает Ваше.
Они таки одинаковые — но обе структуры имеют указатель на heap..

Я как-то больше верю хелпу от разработчиков.
А вот с чтением проблемы скорее у Вас — иначе Вы бы заметили, что я Вам именно что английские ссылки приводил.

Добавление от 26.06.2013 17:50:

Spirit-1
таки вот что Вы имеете в виду: а вот то, что я имею в виду:Здесь: Long String Types (http://docwiki.embarcadero.com/RADStudio/XE4/en/Internal_Data_Formats)
Т.е. Вы говорите о том, как выглядит та структура, об указателе на которую говорю я

14. Imlb , 26.06.2013 19:32
bislomet
об указателе на которую говорю я

Так было всегда или с очень давних времен.

«перед строкой» было в сильно раньшие времена,

И продолжается до сих пор.

15. bislomet , 27.06.2013 08:50
Imlb
Так было всегда или с очень давних времен.

Не всегда, а только после появления в Delphi длинных строк
16. Spirit-1 , 27.06.2013 15:02
А разве в дельфях длинные строки не были с самой первой версии?
17. bislomet , 27.06.2013 15:05
Spirit-1
нет, долгое время макс. длина строки в delphi была 255 символов — то, что впоследствии стало называться ShortString. И вот там как-раз в нулевом байте хранилась длина строки.
Именно для совместимости с «теми еще» строками все нынешние строки в Delphi индексируются с 1.
18. Spirit-1 , 27.06.2013 17:37
bislomet
Посмотрел. Действительно, в первых строки были короткими. Длинные пошли со второй версии. Так что фраза «долгое время макс. длина строки в delphi была 255 символов» не соответствует действительности.
19. bislomet , 27.06.2013 17:54
Spirit-1
Не совсем так — дело в том, что тип string все еще оставался 255-байтным.
а появился длинный AnsiString, который не был синонимом string ни в коем случае!

А сам тип string перестал быть ShortString куда позже, где-то в районе 6й версии.

20. Spirit-1 , 27.06.2013 18:21
Ничего подобного. String в Delphi 2.0 стал = AnsiString, старый String (короткий) стал называться ShortString’ом.

цитата (Spirit-1): А разве в дельфях длинные строки не были с самой первой версии?

Нет, обычные борланд-паскалевской реализации (байт с индексом 0 — длинна строки).

bislomet
Меня больше интересует слово «ANSI», чем 32 битный «длинн». Анси типа означает АНСИ и не хухры-мухры ? Или же нормально в ногу со временем там реально живет UTF8 ?

А от это паноптикум.
-Труп умер 50 лет назад
-Да, но продлим ресурс его креслу еще на 1000 лет. А то и должность завхоза была вакантна.
-Только у нас ANSI завхоз в ANSI кресле.

21. vertur , 28.06.2013 03:49
22. Spirit-1 , 28.06.2013 09:52
vertur
Мы уже этот вопрос отработали . Короткие строки были в первой версии, начиная со второй пошли длинные (по дефолту).
23. AzikAtom , 05.07.2013 11:54
olivenoel

А не в этом ли дело? В помощи написано The New procedure creates a new dynamic variable and sets a pointer variable to point to it. P is a variable of any pointer type. The size of the allocated memory block corresponds to the size of the type that P points to. The newly created variable can be referenced as P^. If there isn’t enough memory available to allocate the dynamic variable, an EOutOfMemory exception is raised. For more information on handling run-time library exceptions, see Handling RTL Exceptions

When an application is finished using a dynamic variable created with New, it should dispose of the memory allocated for the variable using the Dispose standard procedure.
Т.е., при объявлении переменной типа TMyRecType память уже была выделена, а вы её ещё раз выделили и старый указатель потеряли.

24. Spirit-1 , 05.07.2013 13:13
AzikAtom
Не могу утверждать точно (проверить прямо сейчас не могу), но вроде бы если TMyRecType не является указателем, то New(FMyRec) не скомпилируется.

цитата: Spirit-1:
AzikAtom
Не могу утверждать точно (проверить прямо сейчас не могу), но вроде бы если TMyRecType не является указателем, то New(FMyRec) не скомпилируется.

скорее всего там таки был PMyRecType и я ошиблась при перепечатке кода

25. olivenoel , 05.07.2013 13:42
26. AzikAtom , 05.07.2013 14:03
Spirit-1
Написано:

Значит, это не указатель и вручную не надо память брать.

olivenoel
скорее всего там таки был PMyRecType и я ошиблась при перепечатке кода
Вот если бы у вас было:

тогда да, и NEW и последующий DISPOSE, а сейчас можно попробовать убрать этот NEW().
Правда, если вы эти переменные потом собираете в списки, то надо бы сначала сохранить указатель на самую первую переменную, которая создаётся автоматически, и затем восстановить его. Правда, тогда нет смысла объявлять саму переменную и лучше сделать изначально переменную типа ^TMyRecType.

27. CyberStorm , 05.07.2013 17:53
Утечка в Delphi может быть если используется string в структуре record.

например утечка будет:
TMyRecord=record
S:string;
End;

утечки не будет:
TMyRecord=record
S:shortstring; // или S:string[100]
End;

то же справедливо для размещенных в записях массивах,
утечка будет:
TMyRecord=record
A:array of integer;
End;

утечки не будет:
TMyRecord=record
A:array[0..1] of integer;
End;

Применение SetLength( ,0) для строковых переменных и открытых массивов записей позволяет избежать утечки памяти.

28. olivenoel , 08.07.2013 19:03
что делает Move, кроме переноса данных? Возвращет память из-под перенесенных данных менеджеру памяти?
29. AzikAtom , 08.07.2013 21:31
olivenoel
Нет. Просто копирует.
30. olivenoel , 09.07.2013 10:26
тогда MoveMemory и CopyMemory делают одно и то же? Зачем тогда две ф-ции?
31. Spirit-1 , 09.07.2013 12:15
Посмотрите здесь (http://www.delphimaster.net/view/2-1167297116) , всё станет понятно.

нет, еще больше непонятно. В частности

куда смотрит Move?

32. olivenoel , 09.07.2013 12:30
33. AzikAtom , 09.07.2013 12:44
olivenoel
тогда MoveMemory и CopyMemory делают одно и то же? Зачем тогда две ф-ции?
Видать, для удобства программистов. Щелчок правой кнопкой мыши на MoveMemory и выбрать «Find Declaration».
Открывается реализация:

Добавление от 09.07.2013 12:47:

olivenoel
Кстати, вопрос с утечками закрыт? В чём было дело?

34. olivenoel , 09.07.2013 12:53
вроде закрыт. Где-то был непарный вызов New в конструкторе (без Dispose в диструкторе). Сейчас заменила все указатели на структуры на сами структуры. Осталась где-то одна утечка Unknown x1.

Я попыталась уйти вообще от неуправляемых строк внутри библиотеки. Они есть только в теле экспортируемых ф-ций, где преобразуются в нормальные (alias) string и дальше уже работается с ними. Надо только оттестировать с ComInterop из-под C#

35. Spirit-1 , 09.07.2013 13:07
olivenoel
куда смотрит Move?
По указанной ссылке не всё истина. Фильтруйте немного. Там просто топик обсуждения. Конкретно выражение «Ну как же — процедура Move смотрит, откуда ее вызывают, в зависимости от этого
выполняет разные действия.» в той теме — полный бред, конечно.

AzikAtom
Так что, пользуйтесь move() и не парьтесь — она написана на асме.
Я бы не был столь категоричен. Там под IFDEF либо паскалевская реализация, либо на асме под x86.

36. AzikAtom , 09.07.2013 14:08
Spirit-1
В Delphi 4 такая реализация:

В Delphi 7 ещё не смотрел.

olivenoel
куда смотрит Move?
Он только смотрит, чтобы копирование при наложении было корректным. Т.е., то, что было в источнике гарантированно оказывается в приёмнике.

цитата: Spirit-1:
olivenoel
куда смотрит Move?
По указанной ссылке не всё истина. Фильтруйте немного. Там просто топик обсуждения. Конкретно выражение «Ну как же — процедура Move смотрит, откуда ее вызывают, в зависимости от этого
выполняет разные действия.» в той теме — полный бред, конечно.

Добавление от 09.07.2013 14:13:

В Delphi XE2 реализация посложнее:

37. olivenoel , 09.07.2013 14:11
38. Spirit-1 , 09.07.2013 14:39
AzikAtom
Delphi2010:
39. olivenoel , 09.07.2013 16:49
как правильно организовать обмен строками в длл (с учетом СОМ)? Сигнатура метода такая

в метод приходим с именем xls-файла для конвертации. Уходим — с именем полученного после конвертации файла (т.е. фактически изменяется только расширение, потому длина строки не меняется).

сдается мне подобные методы первые кандидаты на утечки.

40. AzikAtom , 09.07.2013 17:00
Если я правильно понимаю концепцию, то в библиотеке память строки не освобождается, и передаётся в неё только указатель, значит, просто там же поменять символы и при выходе из процедуры строка будет изменённая.

цитата: CyberStorm:
Утечка в Delphi может быть если используется string в структуре record.

например утечка будет:
TMyRecord=record
S:string;
End;

утечки не будет:
TMyRecord=record
S:shortstring; // или S:string[100]
End;



так же посмотрела исходники FastMM — если тип утечки не ссылочный (т.е. класс или строка или юникодстрока), то она сообщается как Unknown.
41. olivenoel , 09.07.2013 19:07
42. CyberStorm , 09.07.2013 23:47
Я что-то не понял вашего термина «не зачищаются»
Перечислимые типы в Delphi по умолчанию имеют размер byte т.е. статический размер и утечек из-за них не может быть, при уничтожении класса память, выделенная под переменные при создании класса, высвобождается автоматически.
В конце Destroy я обычно всегда добавляю inherited — правило хорошего тона

цитата: CyberStorm:
Я что-то не понял вашего термина «не зачищаются»
Перечислимые типы в Delphi по умолчанию имеют размер byte т.е. статический размер и утечек из-за них не может быть, при уничтожении класса память, выделенная под переменные при создании класса, высвобождается автоматически.
В конце Destroy я обычно всегда добавляю inherited — правило хорошего тона

если остаются строки, то остается и енум-поле. Сообщение от FastMM гласит: UnicodeString x2 Unknown x1. Или кратно, если объектов было несколько.

Насчет inherited. Вот смотрю я на деструктор TObject (а вроде от него наследуют все классы, даже если это не официально прописано при объявлении класса)

Добавление от 10.07.2013 10:46:

будет ли утечка в случае

Добавление от 10.07.2013 10:47:

и изменится ли что-то, если стрки сделать короткими?

43. olivenoel , 10.07.2013 10:09
44. CyberStorm , 10.07.2013 10:58
Два раза вызов конструктора?

Инициализацию переменных надо вынести в отдельный метод класса, например Init и вызывать его из конструктора

судя по коду, если уберете вызов конструктора из конструктора то все должно быть нормально.

цитата: CyberStorm:
Два раза вызов конструктора?

знаю, что тащить из других языков дурная привычка, но в шарпе, например, можно такое:

цитата: CyberStorm:
Инициализацию переменных надо вынести в отдельный метод класса, например Init и вызывать его из конструктора

судя по коду, если уберете вызов конструктора из конструктора то все должно быть нормально.

Добавление от 10.07.2013 11:24:

как грамотно делать этот Init «сквозным» во всех классах иерархии? В базовом сделать его виртуальным, а потом переопределять во всех наследниках, вызывая при этом родительский? Он нигде не потеряется?

45. olivenoel , 10.07.2013 11:11
46. Spirit-1 , 10.07.2013 11:40
olivenoel
как грамотно делать этот Init «сквозным» во всех классах иерархии? В базовом сделать его виртуальным, а потом переопределять во всех наследниках, вызывая при этом родительский? Он нигде не потеряется?
В родительском классе virtual. В дочерних, при необходимости перекрытия метода, override. Нигде не потеряется.

цитата: CyberStorm:
Утечка в Delphi может быть если используется string в структуре record.

например утечка будет:
TMyRecord=record
S:string;
End;

утечки не будет:
TMyRecord=record
S:shortstring; // или S:string[100]
End;

если применить все это к моей Персоне:

создается три экземпляра Персоны. Утечки: UnicodeString x2 TPerson x3 UnicodeString x4

меняю длинную строку на короткую в

создаю опять три экземпляра. Получаю «чистые» утечки TPerson x3. Куда деваются строки, а точнее откуда они повляются в случае длинных строк?

47. olivenoel , 10.07.2013 12:09
48. Spirit-1 , 10.07.2013 14:03
Создание UnicodeString есть вызов GetMem (см. _NewUnicodeString в system.pas).
Ссылки на созданные строки хранятся в некоторой таблице, формат которой в D2010 примерно такой:

У меня есть подозрение, что в некоторый момент срабатывает что-то вроде GC, который проходит по этой таблице и освобождает память из-под тех строк, у которых RefCount == 0. В тестовом примере вполне может случиться так, что «строковой GC» не успевает сработать, а FastMM рапортует о неосвобождённой памяти. Для чистоты эксперимента я бы насоздавал несколько сотен тысяч объектов с большими/длинными строками и сравнил отчеты об утечках на выходе из программы с теоретически рассчитанными. Если «строковой GC» успел сработать, то количество UnicodeString утечек будет отличаться от общего количества созданных строк.

P.S. Тяжело разобраться, когда полных исходников внутреннуй кухни нет. Остаётся только гадать .

49. CyberStorm , 10.07.2013 17:00
olivenoel Взял ваш код, скомпилировал на XE2, запустил — утечек памяти нет.

Даже убрал из Destroy присвоение:
FName.FFName := »;
FName.FLName := »;
утечек все равно нет, компилятор Delphi достаточно интеллектуальный, чтобы освободить из памяти строки в record при уничтожении экземпляра класса

Давайте посмотрим как вы создаете 3 экземпляра класса вариантов других не вижу. И какая версия Delphi у вас?

PS. Это не .NET, освобождение памяти строки происходит сразу при выходи из области видимости переменной или при вызове деструктора для строк определенных в заголовке класса.

цитата: CyberStorm:
olivenoel Взял ваш код, скомпилировал на XE2, запустил — утечек памяти нет.

Даже убрал из Destroy присвоение:
FName.FFName := »;
FName.FLName := »;
утечек все равно нет, компилятор Delphi достаточно интеллектуальный, чтобы освободить из памяти строки в record при уничтожении экземпляра класса

Давайте посмотрим как вы создаете 3 экземпляра класса вариантов других не вижу. И какая версия Delphi у вас?

у меня тоже ХЕ2. Оно заработало без утечек сегодня после 3-ей или 4-ой перекомпиляции. То ли он компайлер старые дцу-шки подхватывал, то ли звезды не сошлись.

50. olivenoel , 10.07.2013 19:32
51. olivenoel , 11.07.2013 16:50
нашла езе одно место, где появляется Unknown утечка (а точнее строка опять же)

Добавление от 11.07.2013 16:58:

и еще, правильно ли я понимаю, что любое присваивание чего либо переменной типа WideString, автоматично вызывает ф-ции аллокации памяти в системном менеджере памяти? Т.е. после ф-ции

тоже надо вызывать StrDispose/SysFreeString?

52. AzikAtom , 11.07.2013 17:33
olivenoel
можно ли побороть утечку, вызывая StrDispose на стороне хост-приложения?
Да. А можно в функцию передавать готовый буфер для заполнения? В winapi так и делают. Тогда контроль за памятью принадлежит вызывающей стороне.

WideString
Так ведь, WideString это как string только для 16 битных символов.

53. Spirit-1 , 11.07.2013 17:38
olivenoel
Вызов Result := »; при типе функции PWideChar выглядит несколько подозрительно.

цитата: AzikAtom:
olivenoel
можно ли побороть утечку, вызывая StrDispose на стороне хост-приложения?
Да. А можно в функцию передавать готовый буфер для заполнения? В winapi так и делают. Тогда контроль за памятью принадлежит вызывающей стороне.

WideString
Так ведь, WideString это как string только для 16 битных символов.

так ведь WideString полностью совместима с BStr, которая управляется олешным (т.е. общим для всех приложений, системным так сказать) менеджером памяти.

По поводу буффера. Где-то читала, что компилятор дельфи передает Result в виде out параметра. Что-то вроде

эквивалентно (точнее преобразуется не то парсером кода, не то самим компайлером)

Добавление от 11.07.2013 17:56:

цитата: Spirit-1:
olivenoel
Вызов Result := »; при типе функции PWideChar выглядит несколько подозрительно.

чем? Надо что ли nil возвращать?

54. olivenoel , 11.07.2013 17:56
55. Spirit-1 , 11.07.2013 18:15
Я бы Nil возвращал. Все-таки результат есть указатель, и это было бы логичнее.

Добавление от 11.07.2013 18:17:

olivenoel
И тогда все пользователи этой длл должны будут сильно вдаваться в подробности реализации
Кстати, правильно ли я понимаю, что память выделяется в dll, а освобождаться должна вызывающей стороной?

56. olivenoel , 11.07.2013 18:22
правильно Кто-то же ее должен освобождать. Или?

может все же дешевле переименовать все PWideChar в BStr и не морочить разные места?

Добавление от 11.07.2013 18:32:

то что получится на стороне хоста? WideString будет существовать только в scope как временная переменная. При выходе и scope она уничтожится (освободится ли. ). Т.е. Result по выходе из scope будет показывать в небо?

57. Spirit-1 , 11.07.2013 18:44
olivenoel
правильно Кто-то же ее должен освобождать. Или?
Реально это сильно неправильно. Аллоцирование памяти внутри осуществляется средствами конкретного memory manager’а в пуле памяти самой DLL. Освободить такое может только программа с аналогичным MM, и то она будет рыться в чужом огороде. Попробуйте скомпилировать вызывающую программу из другой версии Delphi (чтобы она использовала другой memory manager). И удивитесь, поскольку нифига работать не будет. Или другой вариант — вызывающая программа вообще, к примеру, на MS VC. Как будете память освобождать? Какими вызовами? (За доп. информацией можно поискать, для чего раньше в дельфях использовали borlandmm.dll)
В Вашем случае правильно делать именно так, как посоветовал AzikAtom. Вызывающая программа сама выделяет буфет. Функции DLL заполняют его (буфер) данными. Освобождение памяти делает тоже вызывающая программа. Вот это будет кошерно.
58. AzikAtom , 11.07.2013 18:58
olivenoel
При выходе и scope она уничтожится (освободится ли. )
Да.

Т.е. Result по выходе из scope будет показывать в небо?
Не в небо, а на освобождённую область памяти, где ещё какое-то время будет находится результат, потому что не успел затереться новой переменной.
Лучше всего делать как в winapi: выделить память под строку и передать вместе с переменной, где содержится размер буфера. Функция заполняет этот буфер и в переменной возвращает реальный размер строки.

цитата: Spirit-1:
olivenoel
правильно Кто-то же ее должен освобождать. Или?
Реально это сильно неправильно. Аллоцирование памяти внутри осуществляется средствами конкретного memory manager’а в пуле памяти самой DLL. Освободить такое может только программа с аналогичным MM, и то она будет рыться в чужом огороде.

цитата: Spirit-1:
olivenoel
Попробуйте скомпилировать вызывающую программу из другой версии Delphi (чтобы она использовала другой memory manager). И удивитесь, поскольку нифига работать не будет. Или другой вариант — вызывающая программа вообще, к примеру, на MS VC. Как будете память освобождать? Какими вызовами? (За доп. информацией можно поискать, для чего раньше в дельфях использовали borlandmm.dll)
В Вашем случае правильно делать именно так, как посоветовал AzikAtom. Вызывающая программа сама выделяет буфет. Функции DLL заполняют его (буфер) данными. Освобождение памяти делает тоже вызывающая программа. Вот это будет кошерно.

это ВинАпи стиль. Есть еще СОМ стиль. И там все «играют в одной песочнице» вызовами SysAllocString/SysFreeString/Sys. . Для этого надо все вызовы строковые заставить работать с BStr. Или?

59. olivenoel , 11.07.2013 19:03
60. AzikAtom , 11.07.2013 20:14
olivenoel
ну так WideString и аллоцируется OS’ью в системном ММ
Так вы возвращаете не WideString, а PWideString — указатель на строку. Как вы его дальше используете?
61. Spirit-1 , 11.07.2013 21:52
Я вижу в Вашем коде StrAlloc. Он вызывает GetMem, который выделяет память из СВОЕЙ кучи.

цитата: Spirit-1:
Я вижу в Вашем коде StrAlloc. Он вызывает GetMem, который выделяет память из СВОЕЙ кучи.

Добавление от 12.07.2013 10:31:

цитата: AzikAtom:
olivenoel
ну так WideString и аллоцируется OS’ью в системном ММ
Так вы возвращаете не WideString, а PWideString — указатель на строку. Как вы его дальше используете?

Добавление от 12.07.2013 11:37:

Такой код компилируется и даже, за редким исключением, даёт ожидаемый результат. Но тем не менее, в этом коде грубая ошибка. Указатель, возвращаемый функцией, указывает на область памяти, которая считается свободной — после того как переменная S вышла за пределы области видимости, память, которую занимала эта строка, освободилась. Менеджер памяти может в любой момент вернуть эту память системе (тогда обращение к ней вызовет Access violation) или задействовать для других целей (тогда новая информация перетрёт содержащуюся там строку). Проблема маскируется тем, что обычно результат используется немедленно, до того как менеджер памяти что-то сделает с этим блоком. Тем не менее, полагаться на это и писать такой код не стоит. Под использованием PChar в комментарии имеется ввиду использование его таким образом, как он используется в API-функциях: программа выделяет память для буфера, указатель на этот буфер передаёт в DLL как PChar, а DLL только заносит в этот буфер требуемое значение.

с областями видимости возвращаемых переменных у меня все намного проще — они глобальные переменные в модуле library — т.е. существуют пока загружена длл. Многие ф-ции, возвращающие строки были описаны как:

и все работало (не знаю правда сколько процентов «повезло» в этом). Если поменять это на

Заметки о Pascal, Delphi и Lazarus

Следует ожидать переводов разделов справочной системы Delphi, компиляций из учебников, переводы статей, «путевые заметки» и прочие интересности. Блог прежде всего ориентирован на студентов, но опытных людей я тоже буду рад видеть;-)

четверг, 28 апреля 2011 г.

Строковые типы

О строковых типах

Строка представляет собой последовательность символов. Delphi поддерживает следующие предопределенные строковые типы:

62. olivenoel , 12.07.2013 10:03

2^31 символов

2^30 символов

2^30 символов

Тип Максимальная длина Требует памяти Используется для
ShortString 255 символов От 2 до 256 байт Обратной совместимости
AnsiString От 4 байт до 2GB 8-битные (ANSI) символы, DBCS ANSI, MBCS ANSI, символы Unicode и т.д.
UnicodeString От 4 байт до 2GB Символы Unicode, 8-битные (ANSI) символы, многопользовательские серверы и приложения с поддержкой нескольких языков
WideString От 4 байт до 2GB Символы Unicode; многопользовательские серверы и приложения с поддержкой нескольких языков. Использование UnicodeString обычно более предпочтительно, чем использование WideString (кроме приложений COM).

Замечание: Строковый тип по умолчанию — UnicodeString. Тип WideString предназначен для совместимости с типом COM BSTR. В общем случае следует использовать UnicodeString для не-COM приложений. Для большей части задач тип UnicodeString является предпочтительным. Тип string является псевдонимом для типа UnicodeString.


Строковые типы могут смешиваться при присваивании и построении выражений, — компилятор автоматически производит необходимые преобразования. Однако строки, передаваемые по ссылке в функцию или процедуру (как параметры var и out) должны соответствовать по типу формальным параметрам. Строки могут быть явным образом преобразованы в строки других типов. Тем не менее, преобразование мультибайтовой строки в однобайтовую строку может привести к потере данных.

Есть несколько особых строковых типов, о которых следует упомянуть:

  • Тип AnsiString зависит от кодовой страницы и объявлен следующим образом Это тип AnsiString, который имеет возможность обрабатывать свои внутренние данные в определенной кодовой странице.
  • Тип RawByteString это тип AnsiString($FFFF). RawByteString позволяет передавать строковые данные любой кодовой страницы без выполнения преобразований кодовой страницы. RawByteString должен использоваться только как параметр const, параметр-значение или значение, возвращаемое функцией. Никогда не следует передавать его по ссылке и создавать экземпляры этого типа путем объявления переменных.
  • UTF8String представляет собой строку, закодированную при помощи UTF-8 (переменное количество байт Unicode). Это тип зависит от кодовой страницы и представляет собой AnsiString с кодовой страницой UTF-8.

Зарезервированное слово string действует как общий идентификатор строковых типов. Например:

создает переменную S, которая содержит строку. На платформе Win32, компилятор интерпретирует тип string (когда он встречается в коде без числа в квадратных скобках) как UnicodeString.

На платформе Win32, вы можете использовать директиву <$H->для переключения от типа string к ShortString. Потенциально это полезный прием для работы со старым 16-битным кодом Delphi или Turbo Pascal в ваших новых программах.

Следует учесть, что ключевое слово string так же используется для объявления типов ShortString с заранее определенной длинной.

Сравнение строк выполняется путем определения порядковых номеров элементов стоящих на соответствующих позициях. При сравнении строк неравной длины, каждый символ в более длинной строке при отсутствии соответствующего символа в более короткой строке считается за большее значение. Например, ‘AB’ больше, чем ‘A’; то есть выражение ‘AB’ > ‘A’ возвращает значение True. Строки с нулевой длинной представляют собой наименьшие значения при сравнении.

Вы можете индексировать строковую переменную так, как будто вы обрабатываете массив. Если S – это строка типа, отличного от UnicodeString, а i – это целочисленное выражение, то S[i] представляет i-ый байт в S, который может не быть i-ым символом или вообще не быть целым символом в том случае, если вы вы работаете с мультибайтовой строкой (MBCS). Таким же образом индексирование переменной типа UnicodeString может не обеспечить доступ к конкретному символу. Если строка содержит символы в Базовом Многоязыковом Плане (Basic Multilingual Plane (BMP)), все символы являются двухбайтовыми, а индексирование обеспечит доступ к символам строки. Однако, если некоторые символы не присутствуют в BMP, индексируемый элемент может не оказаться суррогатной парой – целым символом.

Стандартная функция Length возвращает количество элементов в строке. Как было указано, количество элементов не обязательно равно количеству символов. Процедура SetLength устанавливает длину строки. Следует учесть, что функция SizeOf возвращает количество байт, необходимых для представления переменной или типа, и только для типов ShortString это число будет равно количеству символов. Для всех остальных типов строк функция SizeOf возвращает количество байт в указателе (поскольку эти типы являются указателями).

Для типов ShortString или AnsiString S[i] является значением типа AnsiChar. Для W второму символу в MyString. В следующем примере используется стандартная функция UpCase для преобразования значения MyString в заглавные буквы:

Будьте внимательны индексируя строки таким образом, поскольу перезапись конца строки может привести к ошибкам доступа (access violation). Кроме того, следует избегать передачи индексов строк как параметров var, поскольку это снижает эффективность кода.

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

Короткие строки

Переменные типа ShortString могут хранить от 0 до 255 однобайтовых символов. Поскольку длина ShortString может динамически изменяться, в памяти статически выделяется 256 байт, при этом первый байт содержит длину строки, а оставшиеся 255 доступны для хранения символов. Если переменная S имеет тип ShortString, то Ord(S[0]), как и Length(S), возвращаяет длину S; присваивание значения S[0], как и вызов SetLength, изменяет длину S. ShortString поддерживается только для обратной совместимости.

Язык Delphi поддерживает короткострочные типы — фактически, подтипы типа ShortString – в которых максимальная длина строки может быть от 0 до 255 символов. Количество символов определяется по числу, указываемому в квадратных скобках и идущему после зарезервированного слова string. Например:

создает переменную с именем MyString, максимальная длина строки у которой – 100 символов. Это эквивалентно объявлениям:

Переменные, объявленные таким образом, занимают в памяти столько места, сколько в точности требуется для хранения их значений. То есть указанная максимальная длина строки плюс один байт. В нашем примере MyString использует 101 байт по сравнению с 256 байтами для переменной предопределенного типа ShortString.

Если при присваивании значения переменной типа ShortString, длина значения превышает максимальную длину строки для типа, — значение обрезается.

Стандартные функции High и Low работают с идентификаторами короткострочных типов и переменными этих типов. High возвращает максимальную длину строки, а Low возвращает ноль.

Тип AnsiString

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

Переменная типа AnsiString – это структура, содержащая строковую информацию. Когда переменная пуста (то есть содержит строку нулевой длины), указатель имеет значение nil и строка не использует никакого дополнительной памяти для хранения. Когда переменная непуста, она указатель указывает на динамически выделенный блок памяти, содержащий строковое значение. Эта память динамически выделена в куче, но управление ею происходит полностью автоматически и не требует дополнительного пользовательского кода. Структура типа AnsiString содержит 32-битный индикатор, 32-битный счетчик ссылок, 16-битный индикатор для количества байт на символ и 16 бит на кодовую страницу.

Тип AnsiString представляет собой однобайтовую строку. С однобайтовыми наборами символов (single-byte character set (SBCS)) каждый байт в строке представляет собой один символ. В мультибайтовых наборах символов (multibyte character set (MBCS)) элементы являются однобайтовыми, при этом одни символы представляются одним байтом, а другие – более чем одним байтом. Мультибайтовые наборы символов (в особенности двухбайтовые (double-byte character sets (DBCS))) широко используются для азиатских языков. Строка типа AnsiString может содержать символы MBCS.

Доступ к символам AnsiString по индексу начинается с единицы. Индексирование многобайтовых строк ненадежно, поскольку S[i], представляет i-ый байт (не обязательно i-ый символ) в S. Тем не менее, стандартные функции для работы с AnsiString имеют мультибайтовые компоненты, которые так же реализуют сортировку символов по региональному стандарту.(Имена мультибайтовых функций обычно начинаются с префикса Ansi, например, мультибайтовая версия StrPos — AnsiStrPos.) Поддержка мультибайтовых символов зависит от операционной системы и базируется на локальных настройках.

Поскольку переменные типа AnsiString имеют указатели, две или более переменных может ссылаться на одно и то же значение без потребления дополнительной памяти. Компилятор функционирует таким образом для экономии ресурсов и более быстрого выполнения инструкций присваивания. При разрушении значения переменной или при выполнении присваивания, счетчик ссылок для предыдущего значения переменной уменьшается, а счетчик ссылок для нового значения – увеличивается (если таковое существует) – увеличивается. Когда счетчик ссылок для строки становится равным нолю, память высвобождается. Этот процесс называется подсчетом ссылок. Когда индексирование используется для изменения значения единичного символа в строке, производится копирование строки (но только в том случае, когда счетчик ссылок больше единицы). Это называется семантикой копирования при записи.

Тип UnicodeString

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

В наборе символов Unicode каждый символ представлен одним или несколькими байтами. Unicode имеет несколько форматов преобразования (Unicode Transformation Formats) которые используют разные, но эквивалентные друг другу кодировки символов, которые могут быть легко преобразованы друг в друга.

  • В UTF-8, например, символы могут занимать от 1 до 4 байт. В UTF-8 первые 128 символов Unicode предназначены для символов US-ASCII.
  • UTF-16 – еще одна широко используемая кодировка Unicode, в которой символы занимают 2 или 4 байта.Большая часть мировых символов входит в Basic Multilingual Plane и может быть представлена 2 байтами. Остальные символы, которые требуют 2 байта, известны как суррогатные пары (surrogate pairs).
  • В UTF-32 для представления каждого символа требуется 4 байта.

Платформа Win32 поддерживает как однобайтовые и мультибайтовые наборы символов, так и Unicode. Операционная система Windows поддерживает UTF-16.

Тип UnicodeString имеет точно такую же структуру, как и AnsiString. Данные UnicodeString кодируются в UTF-16.

Поскольку UnicodeString и AnsiString имеют одинаковую структуру, они работают похожим образом. Когда переменная UnicodeString непуста, она указывает на динамически выделенный блок памяти, который содержит строковое значение, и работа с памятью для этого – прозрачна для пользователя. Для переменных UnicodeString ведется подсчет ссылок. Две и более переменные могут ссылаться на одно и тоже значение, не потребляя дополнительной памяти.

Экземпляры UnicodeString могут индексировать символы. Индексирование начинается с 1, так же как и для AnsiString.

Тип UnicodeString совместим по присваиванию с остальными строковыми типами. Тем не менее, при присваивание между AnsiString и UnicodeString выполняется соответствующие преобразования. Следует учесть, что присваивание значения типа UnicodeString переменной типа AnsiString не рекомендовано и может повлечь за собой потерю данных.

Delphi так же поддерживает символы и строки Unicode для типов WideChar, PWideChar и WideString.

Тип WideString

Тип WideString представляет собой динамически изменяемую строку, состоящую из 16-битных символов Unicode. В некотором смысле он схож с AnsiString. На платформе Win32, WideString совместим с типом COM BSTR.

WideString подходит для использования в приложениях COM. Тем не менее, WideString не имеет счетчиков ссылок, что для остальных типов приложений тип UnicodeString является более гибким и эффективным.

Индексирование WideString ненадежно, поскольку S[i] представляет iый элемент (не обязательно iый символ) в S.

Для Delphi типы Char и PChar аналогичны типам WideChar и PWideChar, соответственно.

Работа со строками, завершающимися нулевым символом

Множество языков программирования, включая C и C++, не имеют специальных типов для работы со строками. Эти языки и среды, которые разработаны с их помощью, работают со строками, заканчивающимися нулевым символом. Строки, завершающиеся нулевым символом – это массивы символов (индексация начинается с нулевого символа), последний элемент в которых — NUL (#0). Поскольку массив не имеет индикатора длины, первый символ NUL помечает конец строки. Вы можете воспользоваться конструкциями Delphi и специальными подпрограммы в модуле SysUtils для работы с такими строками в тех случаях, когда вы хотите работать с системами, которые их используют.

Следующие объявления типов могут быть использованы для хранения строк, завершающихся нулевым символом:

При включенной директиве расширенного синтаксиса (<$X+>) вы можете присваивать значения строковых констант статическим символьным массивам с индексацией от нуля. (Динамические массивы для этой цели не подойдут.) Если вы инициализируете константу-массив, содержащую строку, которая короче, чем объявленная длина массива, оставшиеся символы устанавливаются в #0.

Использование указателей, массивов и строковых констант

Для работы со строками, завершающихся нулевым символом, часто бывает необходимо использовать указатели. Строковые константы совместимы по присваиванию с типами PChar и PWideChar, которые представляются указателями на массивы значений Char и WideChar, завершающиеся нулевым символом. Например:

P указывает на область памяти, которая содержит завершающуюся нулевым символом копию ‘Hello world!’ Это эквивалентно:

Вы можете также передавать строковые константы в любую функцию, принимающую параметры-значения или параметры-константы типа PChar или PWideChar – например, StrUpper(‘Hello world!’). Как и при присваивании типу PChar, компилятор генерирует копию строки, оканчивающуюся нулевым символом, и передает функции указатель на эту копию. Наконец, вы можете инициализировать константы (отдельно взятые или в составе структурированных типов) типа PChar или PWideChar строковыми литералами. Примеры:

Индексируемые от ноля символьные массивы совместимы с типами PChar и PWideChar. Когда вы используете массив символов вместо указателя, компилятор преобразует массив в константу указательного типа, значение которой соответствует адресу первого элемента массива. Например:

Этот код дважды вызывает SomeProcedure, передавая ей одно и то же значение.

Символьный указатель может быть проиндексирован как массив. В предыдущем примере MyPointer[0] возвращает H. Индекс указывает на смещение, добавляемое к указателю перед получением значения. (Для переменных типа PWideChar индекс автоматически умножается на два.) Таким образом, если P – это символьный указатель, то P[0] эквивалентно P^ и определяет первый символ в массиве, P[1] определяет второй символ и так далее. P[-1] определяет ‘символ’ находящийся слева от P[0]. Компилятор не производит проверки диапазона на таких индексах.

Функция StrUpper иллюстрирует индексирование указателей для проходу по строке, заканчивающейся нулевым символом:

Смешивание строк Delphi и строк, оканчивающихся нулевым символом

Вы можете перемешивать строки (значения типов AnsiString и UnicodeString) и строки, оканчивающиеся нулевым символом (значения типа PChar) в выражениях и инструкциях присваивания, а так же можете передавать значения типа PChar в функции или процедуры, принимающие строковые параметры. Присваивание S := P, где S – строковая переменная, а P – выражение типа PChar, копирует строку, оканчивающуюся нулевым символом в обычную строку.

В двоичной операции, если один операнд является строкой, а второй – значением типа PChar, операнд типа PChar преобразуется в UnicodeString.

Вы можете преобразовывать значение PChar в UnicodeString. Это может быть полезно, если вы хотите выполнить строковую операцию с двумя значениями PChar. Например:

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

  • Если S – это UnicodeString, PChar(S) преобразует S в строку, оканчивающуюся нулевым символом и возвращает указатель на первый символ в S. Такие преобразования используются для Windows API. Например, если Str1 и Str2 – это UnicodeString, вы можете вызвать функцию Win32 API MessageBox следующим образом: Используйте PAnsiChar(S) в том случае, когда S – это строка типа AnsiString.
  • Вы также можете использовать Pointer(S) для преобразования строки в нетипизированный указатель. Но, если S пуста, такое преобразование вернет значение nil.
  • PChar(S) всегда возвращает указатель на блок памяти; если S пуста, возвращается указатель на символ #0.
  • Когда вы преобразуете значение переменной типа UnicodeString или AnsiString в указатель, он действует до тех пор, пока переменной не присваивается новое значение или не выходит из области видимости. Если вы преобразуете любое другое строковое выражение в указатель, он действует только внутри инструкции, в которой выполняется преобразование.
  • Когда вы преобразуете выражение типа UnicodeString или AnsiString в указатель, он должен рассматриваться как доступный только для чтения.

Аналогичные правила действуют при смешивании значений типов WideString и PWideChar.

Тонкости работы со строками в Delphi

Written on 07 Января 2009 . Posted in Delphi

ОГЛАВЛЕНИЕ

Виды строк в Delphi

Для работы с кодировкой ANSI в Delphi существует три вида строк — AnsiString, ShortString и PChar. Между собой они различаются тем, где хранится строка и как выделяется и освобождается память для неё. Зарезервированное слово string по умолчанию означает тип AnsiString, но если после неё стоит число в квадратных скобках, то это означает тип ShortString, а число — ограничение по длине. Кроме того, существует опция компилятора Huge strings (управляется также директивами компилятора <$H+/->и <$LONGSTRINGS ON/OFF>), которая по умолчанию включена, но если её выключить, то слово string станет эквивалентно ShortString или, что то же самое, string[255]. Эта опция введена для обратной совместимости с Turbo Pascal, в новых программах отключать её нет нужды.

Наиболее просто устроен тип ShortString. Это — массив символов с индексами от 0 до N, где N — число символов, указанное при объявлении переменной (в случае использования идентификатора ShortString N явно не указывается и равно 255). Нулевой элемент массива хранит текущую длину строки, которая может быть меньше или равна объявленной (эту длину мы будем далее обозначать M), элементы с индексами от 1 до M — символы, составляющие строку. Значения элементов с индексами M+1..N не определены. Все стандартные функции для работы со строками игнорируют эти символы. В памяти такая переменная всегда занимает N+1 байт.

Ограничения типа ShortString очевидны: так как на хранение длины отводится только один байт, такая строка не может содержать больше 255-ти символов. Кроме того, такой способ записи длины не совпадает с принятым в Windows, поэтому ShortString несовместим с системными строками.

В системе приняты так называемые нуль-терминированные строки: строка передаётся указателем на её первый символ, длина строки отдельно нигде не хранится, признаком конца строки считается встретившийся в цепочке символов #0. Длина таких строк ограничена только доступной памятью и способом адресации (т.е. в Windows это 4294967295 символов). Для работы с такими строками предусмотрен тип PChar. Переменная такого типа является указателем на начало строки. В литературе нередко можно встретить утверждение, что PChar=^Char, однако это неверно: тип PChar встроен в компилятор и не выводится из других типов. Это позволяет выполнять с ним операции, недопустимые для других указателей. Во-первых, если P — переменная типа PChar, то допустимо обращение к отдельным символам строки с помощью конструкции P[N], где N — целочисленное выражение, определяющее номер символа (в отличие от типа ShortString, здесь символы нумеруются с 0, а не с 1). Во-вторых, к указателям типа PChar разрешено добавлять и вычитать целые числа, смещая указатель на соответствующее количество байт вверх или вниз (здесь речь идёт только об использовании операторов «+» и «-«; адресная арифметика с помощью процедур Inc и Dec доступна для любых типизированных указателей, а не только для PChar).

При использовании PChar программист целиком и полностью отвечает за выделение памяти для строки и за её освобождение. Именно это и служит основным источником ошибок у новичков: они пытаются работать с такими строками так же, как и с AnsiString, надеясь, что операции с памятью будут выполнены автоматически. Это очень грубая ошибка, способная привести к самым непредсказуемым последствиям.

Хотя программист имеет полную свободу выбора в том, как именно выделять и освобождать память для нуль-терминированных строк, в большинстве случаев самыми удобными оказываются специально предназначенные для этого функции StrNew, StrDispose и т.п. Их удобство заключается в том, что менеджер памяти выделяет памяти чуть больше, чем требуется для хранения строки, и в эту дополнительную память записывается, сколько байт было выделено. Благодаря этому функция StrDispose удаляет ровно столько памяти, сколько было выделено, даже если в середину выделенного блока был записан символ #0, уменьшающий длину строки.

Компилятор также позволяет рассматривать статические массивы типа Char, начинающиеся с нулевого индекса, как нуль-терминированные строки. Такие массивы совместимы с типом PChar, что позволяет обойтись без использования динамической памяти при работе со строками.

Тип AnsiString объединяет достоинства типов ShortString и PChar: строки имеют фактически неограниченную длину, заботиться о выделении памяти для них не нужно, в их конец автоматически добавляется символ #0, что делает их совместимыми с системными строками (впрочем, эта совместимость не абсолютная; как и когда можно использовать AnsiString в функциях API, можно прочитать здесь).

Переменная типа AnsiString — это указатель на первый символ строки, как и в случае PChar. Разница в том, что перед этой строкой в память записывается дополнительная информация: длина строки и счётчик ссылок. Это позволяет компилятору генерировать код, автоматически выделяющий, перераспределяющий и освобождающий память, выделяемую для строки. Работа с памятью происходит совершенно прозрачно для программиста, в большинстве случаев со строками AnsiString можно работать, вообще не задумываясь об их внутреннем устройстве. Символы в таких строках нумеруются с единицы, чтобы облегчить перенос старых программ, использовавших ShortString.

Счётчик ссылок позволяет реализовать то, что называется copy-on-demand, копирование по необходимости. Если у нас есть две переменные S1, S1 типа AnsiString, присваивание вида S1 := S2 не приводит к копированию всей строки. Вместо этого в указатель S1 копируется значение указателя S2, а счётчик ссылок строки увеличивается на единицу. В дальнейшем, если одну из этих строк потребуется изменить, она сначала будет скопирована (а счётчик ссылок оригинала, естественно, уменьшен) и только потом изменена, чтобы это изменение не затрагивало остальные переменные.

Ниже мы рассмотрим, какие проблемы могут возникнуть при использовании строк разного вида.

Хранение констант

Для работы с этим примером нам понадобится на форму положить пять кнопок и написать следующие обработчики для них (пример Constants):
В этом примере только нажатие на третью и четвёртую кнопку приводит к появлению надписи Test., Первые два обработчика вызывают исключение Access violation в строках, отмеченных звёздочками, а при нажатии пятой кнопки программа обычно работает без исключений (хотя в некоторых случаях оно всё же может возникнуть), но к слову «Test» добавляется какой-то мусор. Разберёмся, почему так происходит.

Все строковые константы, встречающиеся в программе, компилятор размещает в сегменте кода, в области, управление которой никогда не передаётся. Встретив в первом обработчике константу ‘Test’ и определив, что она относится к типу PChar, компилятор выделяет в этой области пять байт (четыре значащих символа и один завершающий ноль), а в указатель P заносится адрес этой константы. Сегмент кода доступен только для чтения, прав на его изменение система программе в целях безопасности не даёт, поэтому попытка изменить то, что находится в этом сегменте, приводит к закономерному результату — Access violation.

В обработчике второй кнопки происходит почти то же самое, с той лишь разницей, что для константы выделяется на восемь байт больше: т.к. в данном случае константа имеет тип AnsiString, ей нужны ещё 4 байта для хранения длины и 4 — для счётчика ссылок. В переменную S записывается указатель на эту константу. Приводя эту переменную к типу PChar, мы, по сути, просто копируем этот указатель в переменную P, а дальше происходит то же самое — попытка изменить страницу памяти, доступную программе только для чтения с тем же самым результатом.

В третьем случае константа, как и раньше, размещается в сегменте кода. Счётчик ссылок у таких констант всегда равен -1 — это значение указывает менеджеру памяти, что это константа, которая не может быть изменена и память для которой не нужно освобождать. Поэтому при любой попытке изменить переменную, которой присвоена такая константа, срабатывает механизм копирования по необходимости: для строки выделяется место в динамической памяти, затем значение константы копируется в эту область, обновляется значение указателя S, а затем выполняется изменение копии, находящейся в динамической памяти. Так как эта память доступна и для чтения, и для записи, исключение не возникает и всё работает так, как и было задумано.

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

В пятом случае мы получаем указатель на этот участок стека. Обратите внимание, что приведение типов в данном случае не работает: для записи в P адреса первого символа строки приходится использовать оператор получения адреса @. Модификация строки проходит, как и в предыдущем случае, успешно, но при присваивании выражения типа PChar свойству типа AnsiString длина строки определяется по правилам, принятым для PChar, т.е. строка сканируется до обнаружения нулевого символа. Но так как ShortString «не отвечает» за то, что будет содержаться в неиспользуемых символах, там может остаться всякий мусор от предыдущего использования стека. Никакой гарантии, что сразу после последнего символа будет #0, нет. Отсюда и появление непонятных символов на экране.

Общий вывод таков: пока мы не вмешиваемся в работу компилятора с типами ShortString и AnsiString, получаем ожидаемый результат. Работа с этими же строками через PChar в обход стандартных механизмов приводит к появлению проблем. Кроме того, при работе со строками PChar необходимо чётко представлять, где и как выделяется для них память, иначе можно получить неожиданную ошибку.

Сравнение строк

Для типов PChar и AnsiString, которые являются указателями, понятие равенства двух строк может толковаться двояко: либо как равенство указателей, либо как равенство содержимого памяти, на которую эти указатели указывают. Второй вариант предпочтительнее, т.к. он ближе к интуитивному понятию равенства строк. Для типа AnsiString реализован именно этот вариант, т.е. сравнивать такие строки можно, ни о чём не задумываясь. Более сложные ситуации мы проиллюстрируем примером Comparisons. В нём девять кнопок, и обработчик каждой из них иллюстрирует одну из возможных ситуаций.
В данном примере мы увидим надпись «Не равно». Это происходит потому, что в этом случае сравниваются указатели, а не содержимое строк, а указатели здесь будут разные. Попытка сравнить строки PChar с помощью оператора сравнения — весьма распространённая ошибка у начинающих. Для сравнения таких строк следует использовать специальную функцию — StrComp.

Следующий пример, на первый взгляд, в плане сравнения ничем не отличается от только что рассмотренного:
Разница только в том, что строки хранятся не в динамической памяти, а в сегменте кода. Тем не менее, на экране появится надпись «Равно». Это происходит, разумеется, не потому, что сравнивается содержимое строк, а потому, что в данном случае два указателя оказываются равны. Компилятор поступает достаточно интеллектуально: видя, что в разных местах используется одна и та же константа, он выделяет для неё место только один раз, а потом помещает в разные указатели один адрес. Поэтому сравнение даёт правильный (с интуитивной точки зрения) результат.

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

Раз уж мы столкнулись с такой особенностью компилятора, немного отвлечёмся от сравнения строк и копнём этот вопрос немного глубже. В частности, распространяется ли интеллект компилятора на константы типа AnsiString.
В этом примере на экран будет выведено «Равно». Как мы видим, указатели равны, т.е. и здесь компилятор проявил интеллект.


Рассмотрим чуть более сложный случай:
В этом случае указатели окажутся не равны. Действительно, с формальной точки зрения константа типа AnsiString отличается от константы типа PChar: в ней есть счётчик ссылок (равный -1) и длина. Однако если забыть о существовании этой добавки, эти две константы одинаковы: четыре значащих символа и один #0, т.е. компилятор, в принципе, мог бы обойтись одной константой. Тем не менее, на это ему интеллекта уже не хватило.

Но вернёмся к сравнению строк. Как мы знаем, строки AnsiString сравниваются по значению, а PChar — по указателю. А что будет, если сравнить AnsiString с PChar?
Этот код выдаст «Равно». Как мы знаем из предыдущего примера, значения указателей не будут равны, следовательно, производится сравнение по содержанию, т.е. именно то, что и требуется. Если исследовать код, который генерирует компилятор, то можно увидеть, что сначала неявно создаётся строка AnsiString, в которую копируется содержимое строки PChar, а потом сравниваются две строки AnsiString. Сравниваются, естественно, по значению.

Для строк ShortString сравнение указателей невозможно, две таких строки всегда сравниваются по значению. Правила хранения констант и сравнения с другими типами следующие:

1. Константы типа ShortString также размещаются в сегменте кода только один раз, сколько бы раз они ни повторялись в тексте.
2. При сравнении строк ShortString и AnsiString первая сначала конвертируется в тип AnsiString, а потом выполняется сравнение.
3. При сравнении строк ShortString и PChar строка PChar конвертируется в ShortString, затем эти строки сравниваются.

Последнее правило таит в себе подводный камень, который иллюстрируется следующим примером:
Здесь формируется строка типа PChar, состоящая из 299 символов «A». Затем формируется строка ShortString, состоящая из 255 символов «А». Очевидно, что эти строки не равны, потому что имеют разную длину. Тем не менее, на экране появится «Равно».

Происходит это вот почему: строка PChar оказывается больше, чем максимально допустимый размер строки ShortString. Поэтому при конвертировании лишние символы просто отбрасываются. Получается строка длиной 255 символов, которая совпадает со строкой ShortString, с которой мы её сравниваем. Отсюда вывод: если строка ShortString содержит 255 символов, а строка PChar — более 255 символов, и её первые 255 символов совпадают с символами строки ShortString, операция сравнения ошибочно даст положительный результат, хотя эти строки не равны.

Избежать этой ошибки поможет либо явное сравнение длины перед сравнением строк, либо приведение одной из сравниваемых строк к типу AnsiString (второй аргумент при этом также будет приведён к этому типу). Следующий пример даёт правильный результат «Не равно»:
Учтите, что конвертирование в AnsiString — операция дорогостоящая в смысле процессорного времени (в этом примере будут выделены, а потом освобождены два блока памяти), поэтому там, где нужна производительность, лучше вручную сравнить длину, а ещё лучше — вообще по возможности избегать сравнения строк разных типов, так как без конвертирования это в любом случае не обходится.

Теперь зададимся глупым, на первый взгляд, вопросом: если мы приведём строку AnsiString к PChar, будут ли равны указатели? Проверим:
Вполне ожидаемый результат — «Равно». Можно, например, перенести строку из сегмента кода в динамическую память с помощью UniqueString — результат не изменится. Однако выводы делать рано. Рассмотрим следующий пример:
От предыдущего он отличается только тем, что строка S имеет пустое значение. Тем не менее, на экране мы увидим «Не равно». Связано это с тем, что приведение строки AnsiString к типу PChar на самом деле не является приведением типов. Это — скрытый вызов функции _LStrToPChar, и сделано это для того, чтобы правильно обрабатывать пустые строки.

Значение » (пустая строка) для строки AnsiString означает, что память для неё вообще не выделена, а указатель имеет значение nil. Для типа PChar пустая строка — это ненулевой указатель на символ #0. Нулевой указатель также может рассматриваться как пустая строка, но не всегда — иногда это рассматривается как отсутствие какого бы то ни было значения, даже пустого (аналог NULL в базах данных). Чтобы решить это противоречие, функция _LStrToPChar проверяет, пустая ли строка хранится в переменной, и, если не пустая, возвращает этот указатель, а если пустая, то возвращает не nil, а указатель на символ #0, который специально для этого размещён в сегменте кода. Таким образом, в случае пустой строки PChar(S) <> Pointer(S), потому что приведение строки AnsiString к указателю другого типа — это нормальное приведение типов без дополнительной обработки значения.

Побочное изменение

Из-за того, что две одинаковые строки AnsiString разделяют одну область памяти, на неожиданные эффекты можно натолкнуться, если модифицировать содержимое строки в обход стандартных механизмов. Следующий код (пример SideChange) иллюстрирует такую ситуацию:
В этом примере требует комментариев процедура UniqueString. Она обеспечивает то, что счётчик ссылок на строку будет равен единице, т.е. для этой строки делается уникальная копия. Здесь это понадобилось для того, чтобы строка S1 хранилась в динамической памяти, а не в сегменте кода, иначе мы получили бы Access violation, как и во втором случае рассмотренного ранее примера Constants.

В результате работы этого примера на экран будет выедено не «Test», а «Fest», хотя значение S2, казалось бы, не должно меняться, потому что мы изменения, которые мы делаем, касаются только S1. Но более внимательный анализ подсказывает объяснение: после присваивания S2 := S1 счётчик ссылок строки становится равным двум, а сама строка разделяется двумя указателями: S1 и S2. Если бы мы попытались изменить непосредственно S1, сначала была бы создана копия этой строки, а потом были бы сделаны изменения в этой копии, а оригинал, на который указывала бы S2, остался бы без изменений. Но, использовав PChar, мы обошли механизм копирования, поэтому строка осталась в единственном экземпляре, и изменения затронули не только S1, но и S2.

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

Нулевой символ в середине строки

Хотя символ #0 и добавляется в конец каждой строки AnsiString, он уже не является признаком её конца, т.к. длина строки хранится отдельно. Это позволяет размещать символы #0 и в середине строки. Но нужно учитывать, что полноценное преобразование такой строки в PChar невозможно — это иллюстрируется примером Zero (в этом примере на форме одна кнопка и две метки):
В первую метку будет выведено число 9 (длина исходной строки), во вторую — 4. Мы видим, что при копировании одной строки AnsiString в другую символ #0 в середине строки — не помеха (вызов UniqueString добавлен для того, чтобы обеспечить реальное копирование строки, а не только копирование указателя). А вот как только мы превращаем эту строку в PChar, информация о её истинной длине теряется, и при обратном преобразовании компилятор ориентируется на символ #0, и строка обрубается.

Потеря куска строки после символа #0 происходит всегда, когда есть преобразование ShortString или AnsiString в PChar, даже неявное. Например, все API-функции работают с нуль-терминированными строками, а визуальные компоненты — просто обёртки над этими функциями, поэтому вывести с их помощью на экран строку, содержащую #0, целиком невозможно.

Но главный подводный камень, связанный с символом #0 в середине строки, заключается в том, что целый ряд стандартных функций для работы со строками AnsiString на самом деле используют API-функции (или даже библиотечные функции Delphi, предназначенные для работы с PChar), что приводит к игнорированию «хвоста» после #0. Следующий код (пример ZeroFind) иллюстрирует эту проблему:
Хотя символ «Z» присутствует в строке, в которой производится поиск, на экран будет выеден «0», что означает отсутствие искомой подстроки. Это связано с тем, что функция AnsiPos использует функции StrPos и CompareString, предназначенные для работы со строками PChar, поэтому поиск за символом #0 не производится. Если заменить в этом примере функцию AnsiPos на Pos, которая работает с типом AnsiString должным образом, на экран будет выведено правильное значение «3».

Описанные проблемы заставляют очень осторожно относиться к использованию символа #0 в середине строк AnsiString — это может стать источником неожиданных проблем.

Строки в записях

Поля в записях могут иметь любой строковый тип без дополнительных ограничений. Однако следует учитывать, что, в отличие от полей простых типов, значения полей типа PChar и AnsiString лежат вне пределов структуры, причем в случае AnsiString это не так бросается в глаза, т.к. вручную выделять и освобождать память не приходится. Это может подложить неприятный сюрприз, если работать со структурой как цельным блоком данных. Чаще всего проблема появляется при записи структуры в поток, файл и т.п. В этом случае записывается только значение указателя, которое не имеет никакого смысла для того, кто потом эти данные читает: такой указатель указывает либо в никуда, либо на данные, никакого отношения к строке не имеющие.

Для иллюстрации этой проблемы, а также методов её решения нам понадобятся два проекта: RecordRead и RecordWrite (лежат в папке RecordReadWrite). Обойтись одним проектом здесь нельзя — указатель, переданный в пределах проекта, остаётся корректным, поэтому проблема маскируется. В проекте RecordWrite три кнопки, соответствующие трём методам сохранения записи в поток TFileStream (в файлы Method1.stm, Method2.stm и Method3.stm соответственно). В три целочисленных поля заносятся текущие час, минута, секунда и сотая доля секунды, строка — произвольная, введённая пользователем в Edit1. Файлы пишутся в текущую папку. В проекте RecordRead три кнопки соответствуют трём методам чтения (каждый — из своего файла). Сначала рассмотрим первый метод — как делать ни в коем случае нельзя.

В проекте RecordWrite:
В проекте RecordRead:
Примечание: В проекте RecordRead объявлена такая же запись TMethod1Record, описание которой во втором случае для краткости опущено.

Запись в файл происходит нормально, но при чтении в строке, отмеченной звёздочкой, скорее всего, возникает исключение Access violation (в некоторых случаях исключения может не быть, но вместо сообщения будет выведен мусор). Причину этого мы уже обсудили выше — указатель Msg, действительный в контексте процесса RecordWrite, не имеет смысла в процессе RecordRead, а сама строка передана не была. Без ошибок этим методом можно передать только пустую строку, потому что пустой строке соответствует указатель nil, имеющий одинаковый смысл во всех процессах. Однако метод передачи строк, умеющий передавать только пустые строки, имеет весьма сомнительную ценность с практической точки зрения.

Самый простой способ исправить ситуацию — изменить тип поля Msg на ShortString. Больше ничего в приведённом коде менять не придётся. Однако использование ShortString имеет два недостатка. Во-первых, длина строки в этом случае ограничена 255-ю символами. Во-вторых, если длина строки меньше максимально возможной, часть памяти, выделенной для структуры, не будет использована. Если средняя длина строки существенно меньше максимальной, то таких неиспользуемых кусков в потоке будет много, т.е. файл окажется неоправданно раздут. Это всегда плохо, а в некоторых случаях — вообще недопустимо, поэтому ShortString можно посоветовать только в тех случаях, когда строки имеют примерно одинаковую длину (напомним, что ShortString позволяет ограничить длину строки меньшим, чем 255, числом символов — в этом случае поле будет занимать меньше места).

С одним из этих недостатков можно бороться: если использовать в записи не ShortString, а статический массив типа Char, можно передавать строки большей, чем 255 символов, длины. Второй метод демонстрирует этот способ.

В проекте RecordWrite:
В проекте RecordRead:
Константа MsgLen задаёт максимальную (вместе с завершающим нулём) длину строки. В приведённом примере она взята достаточно маленькой, чтобы наглядно продемонстрировать, что данный метод имеет ограничения на длину строки. Переделки по сравнению с кодом предыдущего метода минимальны: при записи для копирования значения Edit1.Text вместо присваивания нужно использовать функцию StrPLCopy. В коде RecordRead изменений (за исключением описания самой структуры) вообще нет — это достигается за счёт того, что массив Char считается компилятором совместимым с PChar, а выражения типа PChar могут быть присвоены переменным типа AnsiString — конвертирование выполнится автоматически.

Однако проблему неэффективного использования памяти мы таким образом не решили. Более того, мы до конца не решили и проблему максимальной длины: хотя ограничение на длину строки теперь может быть произвольным, всё равно оно должно быть известно на этапе компиляции. Чтобы полностью избавиться от этих проблем, необходимо вынести строку за пределы записи и сохранять её отдельно, вместе с длиной, чтобы при чтении сначала читалась длина строки, затем выделялась для неё память, и в эту память читалась строка. Именно так работает третий метод.

В проекте RecordWrite:
В проекте RecordRead:
Наконец-то мы получили код, который безошибочно передаёт строку, не имея при этом ограничений длины (кроме ограничения на длину AnsiString) и не расходуя понапрасну память. Правда, сам код получился сложнее. Во-первых, из записи исключено поле типа string, и теперь эту запись можно без проблем читать и писать в поток. Во-вторых, в поток после неё записывается длина строки. В-третьих — сама строка.

Параметры вызова методов ReadBuffer и WriteBuffer для чтения/записи строки требуют дополнительного комментария. Метод WriteBuffer пишет в поток ту область памяти, которую занимает указанный в качестве первого параметра объект. Если бы мы указали саму переменную Msg, то записалась бы та часть памяти, которую занимает эта переменная, т.е. сам указатель. А нам не нужен указатель, нам нужна та область памяти, на которую он указывает, поэтому указатель нужно разыменовать с помощью оператора «^». Но просто взять и применить этот оператор к переменной Msg нельзя — с точки зрения синтаксиса она не является указателем. Поэтому приходится сначала приводить её к указателю (здесь подошёл бы любой указатель, не обязательно нетипизированный). То же самое относится и к ReadBuffer: чтобы прочитанные данные укладывались не туда, где хранится указатель на строку, а туда, где хранится сама строка, приходится использовать такую же конструкцию. И обратите внимание, что прежде чем читать строку, нужно зарезервировать для неё память с помощью SetLength.

Вместо приведения строки к указателю с последующим его разыменованием можно было бы использовать другие конструкции:
и
Это даёт нужный результат и даже является более наглядным: действительно, при чтении и записи мы используем ту область памяти, которая начинается с первого символа строки, т.е. ту, где хранится сама строка. Но этот способ менее производителен: когда мы обращаемся к отдельным символам строки, производится неявный вызов UniqueString, потому что компилятор не знает, что мы будем делать с этим символом и на всякий случай подстраховывается, чтобы не было побочных изменений других строк. В нашем случае мы и так защищены от побочных изменений (при записи строка не меняется, при чтении она и так уникальна — это обеспечивает SetLength), поэтому мы вполне можем обойтись без этой в данном случае излишней опеки со стороны компилятора.

Примечание: Если сделать MsgLen не независимой переменной, а полем записи, можно сэкономить на одном вызове ReadBuffer и WriteBuffer.

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

Выше мы говорили о том, что копирование записей, содержащих поля типа AnsiString, в рамках одного процесса маскирует проблему, т.к. указатель остаётся допустимым и даже (какое-то время) правильным. Но сейчас с помощью приведённого ниже кода (пример RecordCopy) мы увидим, что проблема не исчезает, а просто становится менее заметной.
На экране вместо ожидаемого «Hello. » появится «Good bye». Это происходит вот почему: процедура Move осуществляет простое побайтное копирование одной области памяти в другую, механизм изменения счётчика ссылок при этом не срабатывает. В результате менеджер памяти не будет знать, что после завершения локальной процедуры CopyRecord остаются ссылки на строку «Hello. «. Память, выделенная этой строке, освобождается. Но Rec.Str продолжает ссылаться на эту уже освобождённую память. Для строки S выделяется свободная память — та самая, где раньше была строка LocalRec.Str. А так как Rec.Str продолжает ссылаться на эту область памяти, поэтому обращение к ней даёт строку «Good bye», которая теперь там размещена.

Обратите внимание на последнюю строку — приведение Rec.Str к типу Pointer и обнулению. Это сделано для того, чтобы менеджер памяти не пытался финализировать строку Rec.Str после завершения процедуры, иначе он попытается освободить память, которая уже освобождена, и возникнет ошибка.

Чтобы показать, насколько коварна эта ошибка, рассмотрим следующий код (из того же примера):
От предыдущего случая он отличается только тем, что в нём нет вызовов UniqueString, и строки указывают на константы в сегменте кода, которые никогда не удаляются. На экране получаем вполне ожидаемое «Привет!». Обнулять указатель здесь уже нет смысла, потому что освобождать константу менеджер памяти не будет. Так ошибка оказалась скрытой.

Продолжим наши эксперименты. Запустим пример RecordCopy и понажимаем попеременно кнопки Button1 и Button2. Мы видим, что результат не зависит от порядка, в котором мы нажимаем кнопки.

Модифицируем код: в локальной процедуре обработчика Button1Click уберём из строки «Hello. » восклицательные знаки, сократив её до «Hello». Теперь можно наблюдать интересный эффект: если после запуска нажать сначала Button1, то никаких изменений мы не заметим. А вот если кнопка Button2 будет нажата раньше, чем Button1, то при последующих нажатиях Button1 никаких видимых эффектов не будет. Это связано с тем, что теперь строка «Hello» не равна по длине строке «Good bye», поэтому разместится ли «Good bye» в том же месте памяти, где раньше была «Hello», или в каком-то другом, зависит от истории выделения и освобождения памяти. Если мы начинаем «с чистого листа», память после строки «Hello» остаётся свободной, поэтому туда можно поставить более длинную строку. А вот если раньше память уже выделялась и освобождалась (внутри методов TLabel), то тот кусочек свободной памяти, который достаточен для «Hello», слишком мал для «Good bye», и эта строка размещается в другом месте. А там, куда указывает Rec.Str, остаётся мусор, работать с которым нормально невозможно, поэтому при попытке присвоить это свойству Label1.Caption последнее не меняется.

Примечание: Если увеличить длину строки «Привет!» хотя бы на один символ, чтобы она была не короче, чем «Good bye» (или наоборот, сократить его так, чтобы оно стало короче «Hello»), мы снова увидим, что порядок нажатия кнопок не влияет на результат. Это происходит потому, что строка «Hello» размещается там, где раньше была строка «Привет!», а вот «Good bye» там уже не помещается. Если же обе строки там помещаются (или обе не помещаются), они снова оказываются в одной области памяти. Внимательный читатель может спросить: а при чём здесь длина строки «Привет!», если эта строка хранится в сегменте кода и никогда не освобождается? Дело в том, что когда мы присваиваем эту строку свойству Label1.Caption, внутри методов TLabel происходит её перенос в динамическую память для внутренних нужд этого класса.

Даже на таком простом примере видно, насколько коварна эта ошибка и как незначительные изменения в коде могут кардинально изменить её проявления. Между тем приведённый здесь код — плод долгого «приручения» этой ошибки, чтобы она всегда проявлялась предсказуемым образом. Но даже сейчас мы не можем дать полной гарантии, что у кого-то из читателей из-за какой-то неучтённой мелочи не возникнет ситуация, когда эта ошибка проявляется как-то по-другому (хотя вероятность этого мала). В реальных проектах всё гораздо сложнее, и поведение программы из-за этой ошибки может стать таким неожиданным, а проявление этой ошибки — настолько далёким от того места, где она сделана, что впору будет прыгать вокруг компьютера с бубном, изгоняя бесов. Чтобы не оказаться в таком положении, нужно очень аккуратно работать со строками (а также с другими автоматически финализируемыми типами: динамическими массивами, интерфейсами, вариантами), чтобы тот код, который неявно генерирует компилятор, не оказался в тупике. Чаще всего проблемы возникают при побайтном копировании переменной типа AnsiString (не обязательно в составе записи) или работе с ней как с указателем другого типа. Это не значит, что приводить AnsiString к другим указателям категорически нельзя — выше мы уже делали это, и вполне успешно. Но применяя любой низкоуровневый механизм к таким строкам, разработчик должен чётко представлять, как это отразится на внутренних механизмах работы с ними. Иначе — вот такая непонятная ошибка.

Использование ShareMem

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

Итак, создаём новую динамически компонуемую библиотеку (DLL). Delphi делает нам следующую заготовку:
Самое важное здесь — комментарий. Его следует внимательно прочитать и осознать, а главное — выполнить эти советы, иначе при передаче строк AnsiString между DLL и программой вы будете получать Access violation в самых неожиданных местах. Почему-то многие им пренебрегают, а потом бегут с вопросами в разные форумы, хотя минимум внимательности и отсутствие снобизма по отношению «к этим, из Borland’а, которые навставляли тут никому не нужных комментариев» могли бы уберечь от ошибки.

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

Менеджер памяти реализуется модулем System. Так как DLL компонуется отдельно от использующего её exe-файла, у неё будет своя копия кода System и, следовательно, свой менеджер памяти. И если объект, память для которого была выделена в коде основного модуля программы, попытаться освободить в коде DLL, получится, что освобождать память будет совсем не тот менеджер, который её выделил. А сделать он этого не сможет, т.к. не обладает информацией о выделенном блоке. Результат — ошибка (скорее всего, Access violation при выходе из процедуры). А при работе со строками AnsiString память постоянно выделяется и освобождается, поэтому, попытавшись работать с одной и той же строкой и в главном модуле, и в DLL, мы получим ошибку.

Теперь, когда мы поняли, почему возникает проблема, разберёмся, как ShareMem её решает. Delphi предоставляет возможность заменить стандартный менеджер памяти своим: для этого нужно написать функции выделения, освобождения и перераспределения памяти и сообщить их адреса через процедуру SetMemoryManager — после этого все функции для работы с динамической памятью будут работать через эти функции. Именно это и делает ShareMem: в секции инициализации этого модуля содержится код, заменяющий функции работы с памятью своими, причём эти функции находятся во внешней библиотеке BORLNDMM.DLL. Получается, что и библиотека, и главный модуль работают с одним менеджером памяти, что решает описанные выше проблемы.

Если менеджер памяти попытаться поменять не в самом начале программы, ему придётся освобождать память, которую успел выделить предыдущий менеджер памяти, что приведёт к той же самой проблеме. Поэтому заменить менеджер памяти нужно до того, как будет выполнена первая операция по её выделению. Отсюда возникает требование вставлять ShareMem первым модулем в dpr-файлах главного модуля и DLL — чтобы его секция инициализации была первым выполняемым программой кодом.

Кстати, к совету использовать вместо AnsiString PChar, чтобы избавиться от необходимости использования ShareMem, данному в комментарии, следует относится осторожно: если мы попытаемся, например, вызвать StrNew в основной программе, а StrDispose — в DLL, то получим ту же проблему. Вопрос не в типах данных, а в том, как манипулировать памятью.

Необходимость распространять со своей программой ещё и библиотеку BORNDMM.DLL иногда воспринимается как неудобство (хотя речь идёт только о тех ситуациях, когда программа и так вынуждена использовать другие DLL), поэтому существуют менеджеры памяти сторонних разработчиков, которые решают ту же проблему, что и ShareMem, но без использования библиотеки. Эти менеджеры памяти просто все запросы по работе с памятью передают системному менеджеру памяти, что избавляет их от необходимости хранить какую-либо информацию о выделенных блоках. Такое решение вполне работоспособно, но менее эффективно, особенно если нужно многократно выделять и освобождать небольшие блоки памяти.

Следует также упомянуть о ещё одной альтернативе передачи строк в DLL — типе WideString. Этот тип хранит строку в кодировке Unicode и является, по сути, обёрткой над системным типом BSTR. Работать с WideString так же просто, как и с AnsiString, перекодирование из ANSI в Unicode и обратно выполняется автоматически при присваивании значения одного типа переменной другого. В целях совместимости с COM и OLE для работы с памятью для строк WideString используется специальный системный менеджер памяти (через API-функции SysAllocString, SysFreeString и т.п.), поэтому передавать эти строки из DLL в главный модуль и обратно можно совершенно безопасно даже без ShareMem. Правда, при этом не стоит забывать о расходовании процессорного времени на перекодировку, если основная работа идёт не с Unicode, а с ANSI.

Отметим одну ошибку, которую делают новички, прочитавшие комментарий про ShareMem, но не умеющие работать с PChar. Они пишут, например, такой код для функции, находящейся в DLL и возвращающей строку:
Такой код компилируется и даже, за редким исключением, даёт ожидаемый результат. Но тем не менее, в этом коде грубая ошибка. Указатель, возвращаемый функцией, указывает на область памяти, которая считается свободной — после того как переменная S вышла за пределы области видимости, память, которую занимала эта строка, освободилась. Менеджер памяти может в любой момент вернуть эту память системе (тогда обращение к ней вызовет Access violation) или задействовать для других целей (тогда новая информация перетрёт содержащуюся там строку). Проблема маскируется тем, что обычно результат используется немедленно, до того как менеджер памяти что-то сделает с этим блоком. Тем не менее, полагаться на это и писать такой код не стоит.

Под использованием PChar в комментарии имеется ввиду использование его таким образом, как он используется в API-функциях: программа выделяет память для буфера, указатель на этот буфер передаёт в DLL как PChar, а DLL только заносит в этот буфер требуемое значение.

3.3.1. Виды строк в Delphi

3.3.1. Виды строк в Delphi

Для работы с кодировкой ANSI в Delphi существует три вида строк: AnsiString , ShortString и PChar . Различие между ними заключается в способе хранения строки, а также выделения и освобождения памяти для нее. Зарезервированное слово string по умолчанию означает тип AnsiString , но если после нее следует число в квадратных скобках, то это означает тип ShortString , а число — ограничение по длине. Кроме того, существует опция компилятора Huge strings (управляется также директивами компилятора <$H+/->и <$LONGSTRINGS ON/OFF>, которая по умолчанию включена, но если ее выключить, то слово string станет эквивалентно ShortString ; или, что то же самое, string[255] . Эта опция введена для обратной совместимости с Turbo Pascal, в новых программах отключать ее нет нужды. Внутреннее устройство этих типов данных иллюстрирует рис. 3.2.

Рис. 3.2. Устройство различных строковых типов Delphi

Наиболее просто устроен тип ShortString . Это массив символов с индексами от 0 до N, где N — число символов, указанное при объявлении переменной (в случае использования идентификатора ShortString N явно не указывается и равно 255). Нулевой элемент массива хранит текущую длину строки, которая может быть меньше или равна объявленной (эту длину мы будем далее обозначать M), элементы с индексами от 1 до M — это символы, составляющие строку. Значения элементов с индексами M+1..N не определены. Все стандартные функции для работы со строками игнорируют эти символы. В памяти такая переменная всегда занимает N+1 байтов.

Ограничения типа ShortString очевидны: на хранение длины отводится только один байт, поэтому такая строка не может содержать больше 255 символов. Кроме того, такой способ записи длины не совпадает с принятым в Windows, поэтому ShortString несовместим с системными строками.

В системе приняты так называемые нуль-терминированные строки: строка передается указателем на ее первый символ, длина строки отдельно нигде не хранится, признаком конца строки считается встретившийся в цепочке символов #0 . Длина таких строк ограничена только доступной памятью и способом адресации (т. е. в Windows теоретически это 4 294 967 295 символов). Для работы с такими строками предусмотрен тип PChar . Переменная такого типа является указателем на начало строки. В литературе нередко можно встретить утверждение, что PChar = ^Сhar , однако это неверно: тип PChar встроен в компилятор и не выводится из других типов. Это позволяет выполнять с ним операции, недопустимые для других указателей. Во-первых, если P — переменная типа PChar , то допустимо обращение к отдельным символам строки с помощью конструкции P[N] , где N — целочисленное выражение, определяющее номер символа (в отличие от типа ShortString , здесь символы нумеруются с 0, а не с 1). Во-вторых, к указателям типа PChar разрешено добавлять и вычитать целые числа, смещая указатель на соответствующее число байтов вверх или вниз (здесь речь идет только об операторах «+» и «-«; адресная арифметика с помощью процедур Inc и Dec доступна для любых типизированных указателей, а не только для PChar ).

При работе с PChar программист целиком и полностью отвечает за выделение памяти для строки и за ее освобождение. Именно это и служит основным источником ошибок у новичков: они пытаются работать с такими строками так же, как и с AnsiString , надеясь, что операции с памятью будут выполнены автоматически. Это очень грубая ошибка, способная привести к самым непредсказуемым последствиям.

Хотя программист имеет полную свободу выбора в том, как именно выделять и освобождать память для нуль-терминированных строк, в большинстве случаев самыми удобными оказываются специально предназначенные для этого функции StrNew , StrDispose и т. п. Их преимущество заключается в том, что менеджер памяти выделяет чуть больше места, чем требуется для хранения строки, и в эту дополнительную память записывается, сколько байтов было выделено. Благодаря этому функция StrDispose удаляет ровно столько памяти, сколько было выделено, даже если в середину выделенного блока был записан символ #0 , уменьшающий длину строки.

Компилятор также позволяет рассматривать статические массивы типа Char , начинающиеся с нулевого индекса, как нуль-терминированные строки. Такие массивы совместимы с типом PChar , что позволяет обойтись без использования динамической памяти при работе со строками.

Тип AnsiString объединяет достоинства типов ShortString и PChar : строки имеют фактически неограниченную длину, заботиться о выделении памяти для них не нужно, в их конец автоматически добавляется символ #0 , что делает их совместимыми с системными строками (впрочем, эта совместимость не абсолютная; как и когда можно использовать AnsiString в функциях API, мы рассматривали в разд. 1.1.13.).

Переменная типа AnsiString — это указатель на первый символ строки, как и в случае PChar . Разница в том, что перед этой строкой в память записывается дополнительная информация: длина строки и счетчик ссылок. Это позволяет компилятору генерировать код, автоматически выделяющий, перераспределявший и освобождающий память, выделяемую для строки. Работа с памятью происходит совершенно прозрачно для программиста, в большинстве случаев со строками AnsiString можно работать, вообще не задумываясь об их внутреннем устройстве. Символы в таких строках нумеруются с единицы, чтобы облегчить перенос старых программ, использовавших строки типа ShortString .

Счетчик ссылок позволяет реализовать то, что называется copy-on-demand, копирование по необходимости. Если у нас есть две переменные S1 , S2 типа AnsiString , присваивание вида S1:= S2 не приводит к копированию всей строки. Вместо этого в указатель S1 копируется значение указателя S2 , а счетчик ссылок строки увеличивается на единицу. В дальнейшем, если одну из этих строк потребуется модифицировать, она сначала будет скопирована (а счетчик ссылок оригинала, естественно, уменьшен) и только потом изменена, чтобы это не затрагивало остальные переменные.

Далее мы рассмотрим, какие проблемы могут возникнуть при использовании строк разного вида.

AnsiString — Тип Delphi

Доброго времени суток, уважаемые подписчики!

Сегодня публикуется статья Сысоева Александра Петровича о строковых типах в 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 позволяет легко определить реальный размер строки на которую указывает значение pChar.

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

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

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

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

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

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

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


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

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

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

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

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

ShortString, и String[n]

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

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

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

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

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

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

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

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

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

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

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

При выполнении первого оператора (присваивание s1), указатель, хранящийся в переменной s1 настраивается на область памяти в которой размещена строка ‘abc’. При выполнении второго оператора, в s2 попадает тот же адрес! Т.е. в памяти при это присутствует лишь один экземпляр строки ‘abc’. Да и зачем нам нужны дублирующие себя строки.

Не нужно программисту заботиться и об освобождении памяти занятой ненужными уже строками. Вот например есть такая процедура:

Здесь, при выполнении присваивания, Delphi создаст в памяти экземпляр строки ‘Значение переменной равно 123’, и присвоит адрес этой строки переменной s. Однако, при завершении процедуры, переменная s перестанет существовать — она же локальная. Значит, и экземпляр строки тоже уже не нужен — на него некому будет указывать. Вот поэтому, Delphi автоматически освободит память, выделенную под строку, как только, выполнение процедуры достигнет строки end. Более того, даже если во время выполнения процедуры возникнет исключительная ситуация, при которой «хвост» процедуры может и не выполниться, Delphi всё равно корректно освободит память для всех строк, распределенных в этой процедуре. Достигается это неявным использованием механизма подобного try — finally.

Казалось бы, всё замечательно. Но попробуем усложнить ситуацию.

Теперь, к моменту завершения процедуры, на экземпляр строки ‘Значение переменной равно 123’ уже ссылаются две переменные s и gs. И, несмотря на то, что область существования переменной s заканчивается, освобождать память, выделенную под строку на которую она указывает нельзя! Ведь позже, возможны обращения к переменной gs.

Для того, чтобы корректно обрабатывать такие ситуации, Delphi для каждой динамически распределенной строки ведет так называемый «счётчик ссылок». Т.е., как только он присваивает какой-либо из строковых (AnsiString) переменных ссылку на распределенную в памяти строку, то он увеличивает этот счетчик на единицу. Первоначально, при присваивании динамически распределённой строки, первой переменной (в примере s), значение этого счётчика устанавливается равным единице. В последствии, при прекращении жизни каждой строковой переменной, он уменьшает на 1 этот счетчик для той строки на которую она указывает. Если счётчик становится равным 0, то значит более нет строковых переменных, указывающих на данную строку. Значит, ее можно освобождать. Благодаря такому алгоритму, после присваивания в примере значения переменной gs, у строки ‘Значение переменной равно 123’ счетчик ссылок становится равным 2. Следовательно, при «умирании» переменной s, он декрементируется, и становится равным 1. Т.е. >0, поэтому то Delphi и не освобождает память, занятую строкой.

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

Здесь, как мы уже знаем, после выполнения оператора s2 := s1, обе переменные указывают на один и тот же экземпляр строки ‘abc123’. Однако, что же произойдёт когда выполниться оператор s2[1] := ‘X’? Казалось бы, в единственном имеющимся в нашем распоряжении экземпляре строки первая буква будет заменена на ‘X’. И как следствие, обе строки станут равными ‘Xbc123’. s1 то за что «страдает»? Но, к счастью это не так. Здесь на помощь Delphi вновь приходит счетчик ссылок. Delphi, при выполнении этого оператора понимает, что строка на которую указывает s2 будет изменена, а это может повлиять на других. Поэтому, перед изменением строки, проверяется ее счётчик ссылок. Обнаружив, что на нее ссылается более одной строковой переменной, делается следующее: создается копия этой строки со счётчиком ссылок равным 1, и адрес этой копии, присваивается s2; У исходного экземпляра строки, счетчик ссылок уменьшается на 1 — ведь s2 на неё теперь не ссылается. И лишь после этого, происходит изменение первой буквы, теперь уже собственного экземпляра строки. Т.е., по окончанию выполнения этого оператора, в памяти будут находиться две строки: ‘abc123’ и ‘Xbc123’. Причем, s1 будет ссылаться на первую, а s2 на вторую.

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

Казалось бы, при завершении работы процедуры, экземпляр строки ‘Вася’ должен быть уничтожен. Но в данном случае это не так. Ведь, при следующем входе в процедуру, для выполнения присваивания нужно будет вновь где-то взять строку ‘Вася’. Для этого, ещё при компиляции, 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 Длина строки
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 все сделает за Вас. Надо лишь понимать, что в маленькое большое не влезает. Например:

В результате выполнения этого кода, в переменной 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:

Все они, синтаксически правильны. И кажется, что все три указателя (p1, p2 и p3) будут в результате иметь одно и то же значение. Но это не так. Всё зависит от того, что находится в s. Если быть более точным, равно ли значение s пустой строке, или нет:

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

Для выполнения преобразования pChar(s), компилятор генерит вызов специальной внутренней функции @LstrToPChar. Эта функция проверяет — если строковая переменная имеет значение Nil, то вместо него, она возвращает указатель на реально размещенную в памяти пустую строку. Т.е. pChar(s) никогда не вернет указатель равный Nil.

Тут все просто, такое преобразование просто возвращает содержимое строковой переменной. Т.е. если она при пустой строке содержит Nil, то и результатом преобразования будет Nil. Если же строка не пуста, то результатом будет адрес экземпляра строки.

Здесь, все совсем по другому. Перед выполнением такого преобразования, компилятор вставляет код, обеспечивающий ситуацию, когда указатель, хранящийся в s, будет единственным указателем на экземпляр строки в памяти. В нашем примере, если строка не пуста, то будет создана копия исходной строки. Вот ее-то адрес и будет возвращен как результат такого преобразования. Но, если строка пуста, то результатом будет Nil, как и во втором случае.

Теперь, интересно отметить, что если в приведенном примере, преобразование p3 := @(s[1]) выполнить первым, то при не пустой строке в s, все указатели (p1, p2, и p3), будут равны. И содержать они будут адрес «персонального» экземпляра строки.

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

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

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

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

>0 если S1 > S2
255 символов приходится платить.

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

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

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

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

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

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

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

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

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

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

Сначала, при выполнении процедуры 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) используемых для мультиязыковых приложений. Может и еще что-то забыл рассказать. Но, не расстраивайтесь. Я свои знания получал, изучая книги, тексты своих и чужих программ, код сгенерированный компилятором, и т.п. Т.е., все из открытых источников. Значит это все доступно и Вам. Главное, чтобы Вы были любознательными, и почаще задавали себе вопросы «Как?», «Почему?», «Зачем?». Тогда во всем сможете разобраться и сами.

Приглашаем авторов в рассылку!

С уважением,
координатор рассылки Алексей aka Gelios.

AnsiString — Тип Delphi

Голова опухла настолько, что уже не соображаю, казалось бы, элементарное: как в DelphiXE тупо сконверировать из AnsiString в обычный String, который в DelphiXE в Unicode? B какой Unicode принят в DelphiXE по умолчанию: utf-8, utf-16 или какой-либо другой?

а приравнять не пробовал?

А не прокатывает, когда Edit.Text передаешь как AnsiString-параметр в функцию, равно как и присваиваешь полю результат функции в AnsiString. Казалось бы, проще функцию переписать, но таких несколько тысяч, а сроки поджимают.

Если я понял правильно, то вместо AnsiString я должен использовать либо CirillicString либо AnsiString(1251) и все будет хорошо, или я не прав?
Eraser, премного благодарен за ссылки, многое встало на свои места.

> AlekVolsk (13.04.2011 00:08:04) [4]

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

var
as: AnsiString;
us: string;
begin
as := «Bla-bla-bla»;
us := string(as);
us := «foo»;
as := AnsiString(us);
end;

type
CirillicString = type AnsiString(1251);
var
s: CirillicString;
begin
s := edit1.Text;
end;

Компилятор пишет: Implicit string cast with potential data loss from «TCaption» to «CirillicString», т.е. неявный строковый бросок с потенциальной потерей данных от «TCaption» до «CirillicString». В каких случаях возможна потеря данных из CirillicString и как этого избежать?

Если откровенно туплю — извиняйте, 40 часов уже изза компа не выхожу

> В каких случаях возможна потеря данных из CirillicString
> и как этого избежать?

При преобразовании из Unicode в локальную кодировку, когда в целевой кодировке нет символов исходной.

соответственно, если в edit1.text только символы алфавита (лат/рус), знаки препинания и цифры, при полном отсутствии каких-либо управляющих символов типа табуляции, то все будет ок. или я не прав?
Но в любом случае, хотелось бы как-то избавиться от предупреждения компилятора, это возможно в данном частном случае? да — как, нет — почему?

> при полном отсутствии каких-либо управляющих символов типа
> табуляции

управляющие символы имеют код


> это возможно в данном частном случае? да — как, нет — почему?

Да.
Как — я уже написал.

Все получилось.
Всем премного благодарен!
Приношу всем свои извинения за возможно потраченное на меня время и нервы.

Блин, во всей группе проектов на 2 exe и 16 dll общей численностью более 40 тысяч строк при переходе с D2006 на DXE править пришлось менее 30 строк кода! А пока догнал, что к чему, 2 недели времени и нервов угробил!
Еще раз всем откликнувшимся огромное спасибо!

Илон Маск рекомендует:  Как менять размер текста при изменении размера окна
Понравилась статья? Поделиться с друзьями:
Кодинг, CSS и SQL