AnsiStartsStr — Функция Delphi

Содержание

Функции Delphi модуля StrUtils

Модуль StrUtils

Предлагаем список функций модуля StrUtils, используемого в среде разработки Delphi.

AnsiContainsStr Возвращается истина, если строка содержит подстроку
AnsiEndsStr Возвращется истина, если строка заканчивается подстрокой
AnsiIndexStr Сравнивает строку со списком строк, возвращает соответствующий индекс
AnsiLeftStr Извлечённые символы слевой стороны строки
AnsiMatchStr Возвращается истина, если строка точно соответствует какой-либо строке из списка
AnsiMidStr Возвращает подстроку из средних символов строки
AnsiReplaceStr Заменяет часть одной строки другой
AnsiReverseString Переворачивает последовательность символов в строке
AnsiRightStr Извлечение символов с правой стороны строки
AnsiStartsStr Возвращается истина, строка начинается подстрокой
AnsiUpperCase Символы нижнего регистра изменяются на символы верхнего регистра
DupeString Создает строку, содержащую копии подстроки
StrToTime Конвертирует строку с временем в значение с типом TDateTime
StuffString Заменяет часть одной строки на другую
Succ Инкрементирует порядковую переменную

Delphi AnsiStartsStr отсутствует

Если я использую AnsiStartsStr в Delphi Vcl, есть ошибка: необъявленный идентификатор

Я уже установил пакет исправлений, но это не помогло. Как я могу это исправить? Я старался:

1 ответ

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

Название документации говорит вам, что вам нужно устройство. Или

  • System.AnsiStrings или
  • System.StrUtils

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

Если у вас возникли проблемы с поиском документации, попробуйте совет здесь: Как я могу найти документацию Delphi? Конечно, вы просто должны нажать на нераспознанный идентификатор и нажать F1.

delphi — Delphi AnsiStartsStr отсутствует

Если я использую AnsiStartsStr в delphi vcl, у вас есть ошибка: uneclared identifier

Я уже установил пакет исправлений, но это не помогло. Как я могу это исправить? Я пытался:

    2 1
  • 22 июн 2020 2020-06-22 03:35:02
  • Jan Wytze

1 ответ

В такой ситуации первая задача — посмотреть в документации. На самом деле есть две функции с этим именем. Они описаны здесь:

Название документации указывает вам нужное устройство. Или

  • System.AnsiStrings или
  • System.StrUtils

Поэтому вам нужно добавить соответствующий блок в свой раздел uses. Я не могу точно сказать, какую функцию вам нужно.

Если у вас есть проблемы с поиском документации, попробуйте здесь совет: как я могу найти документацию Delphi? Конечно, вы должны просто щелкнуть нераспознанный идентификатор и нажать F1.

AnsiStartsStr

Check whether a string starts with a given substring, observing case

Declaration

Source position: strutils.pp line 42

const ASubText : string ;

const AText : string

Arguments

Substring to check for

String to check in

Function result

True if the given string starts with the substring, False if not.

Description

AnsiStartsStr checks AText to see whether it starts with ASubText , and returns True if it does, False if not. The check is performed case-sensitive. Basically, it checks whether the position of ASubText equals 1.

See also

Check whether a string ends with a certain substring

Check whether a string starts with a given substring, observing case

Searches, observing case, for a string in an array of strings.

Checks whether a string contains a given substring

AnsiStartsStr — Функция 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.

Delphi AnsiStartsStr отсутствует

Если я использую AnsiStartsStr в Delphi VCL имеет ошибку: необъявленный идентификатор

Я Allready установлен пакет исправлений, но это не помогло. Как я могу это исправить? Я пытался:

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

Название документации говорит вам устройство вам нужно. Или

  • System.AnsiStrings или
  • System.StrUtils

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

Если у вас есть проблемы поиск документации попробуйте советы здесь: Как я могу найти Delphi документацию? Конечно , вы должны просто быть в состоянии нажать на непризнанного идентификатор и нажмите клавишу F1.

AnsiStartsStr Routine

Unit Edit

Description Edit

(Please provide a description in your own words. It is illegal to use the wording from the Delphi Help.)

Technical Comments Edit

(Known issues / Documentation clarifications / Things to be aware of)

Examples Edit

(Please provide links to articles/source code that show how to use this item.)

See Also Edit

(Please provide links to items specifically related to this item.)

User Comments/Tips Edit

(Please leave your name with your comment.)

Delphi AnsiStartsStr отсутствует

Если я использую AnsiStartsStr в Delphi Vcl, есть ошибка: необъявленный идентификатор

Я уже установил пакет исправлений, но это не помогло. Как я могу это исправить? Я старался:

1 ответ

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

Название документации говорит вам, что вам нужно устройство. Или

  • System.AnsiStrings или
  • System.StrUtils

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

Если у вас возникли проблемы с поиском документации, попробуйте совет здесь: Как я могу найти документацию Delphi? Конечно, вы просто должны нажать на нераспознанный идентификатор и нажать F1.

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

AnsiStartsStr — Функция 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.

Илон Маск рекомендует:  Что такое код hw_api &#62;find
Понравилась статья? Поделиться с друзьями:
Кодинг, CSS и SQL