|
|
|||||||||||||||||||||||||||||
|
Длинные строки и динамические массивы в DelphiИсточник: RSDN Magazine #3-2004 Вишневский Павел
Оглавление
Среди типов переменных в Delphi есть несколько типов, существенно отличающихся от обычных.
Для работы с ними компилятор генерирует и вставляет в код специальные функции. К таким типам, прежде всего, относятся длинные строки (AnsiString) и динамические массивы (далее будем называть их динамическими массивами, так как строку можно рассматривать как массив символов, а в дальнейшем нам потребуется некое общее название для этих типов). Рассмотрим следующий код:
Реализуемый компилятором код выглядит приблизительно так:
Что показывает приведенный пример:
РеализацияВ начале выделяемой области памяти содержится заголовок, используемый системными функциями Delphi (определен в System.pas) для управления переменными обсуждаемых типов. Заголовок описывается записью TPvsDynRec. Вот ее описание:
Для строк и массивов использование полей этой структуры несколько отличается, но сам принцип одинаковый. Вот описание этих полей:
Присваивание и передача в качестве параметраНазначение счетчика ссылок - отследить момент, когда нужно освободить память, и избежать лишнего копирования. При присваивании переменных, имеющих тип динамического массива, значения не копируются, а просто копируется указатель. При этом счетчик увеличивается на 1. При изменении строки или размера динамического массива, проверяется значение счетчика и если он больше 1, то создается новая копия и уже с ней производятся изменения. При выходе переменной из области видимости или вызове деструктора объекта, вкотором расположена переменная, вызывается финализация, которая уменьшает счетчик и, если он равен 0, память освобождается. При передаче динамического массива в функцию или процедуру, Delphi, в зависимости от типа передачи, вставляет системные функции, управляющие счетчиком и финализацией. При передаче по значению счетчик увеличивается. Передача по ссылке (var) или константы (const) не влияет на счетчик. Так что передача как константы (const) не только делает код более безопасным, но и оптимизирует его. В связи с некоторыми отличиями в поведении счетчика ссылок для строк и массивов, а также с особыми свойствами массивов, необходимо рассмотреть эти структуры отдельно. Длинные строкиОсобенностью длинных строк является возможность присваивания константного литерала. В зависимости от того, является переменная локальной или нет, генерируется различный код. Если происходит присваивание локальной переменной:
Переменная S получит счетчик равный -1 и будет ссылаться прямо на литерал. При присваивании другой локальной переменной или передаче в процедуру счетчик никогда не меняется. Естественно, ни о каком управлении памятью в данном случае речи не идет. В остальных случаях (то есть когда строка помещается в глобальную переменную, поле, элемент массива и т.п.) Delphi создает обычную строку в динамической памяти, в которую копирует присваиваемую константу. Любое изменение строки заменяется на вызов системных функций. Если счетчик строки в этот момент не равен 1, создается новая строка. Исключением из этого правила является доступ к содержимому строки по указателю (приведение к типу PChar). В этом случае уже ничего не контролируется. Ниже приведены два примера, иллюстрирующих такое поведение.
К изменению строки через PChar нужно относится с осторожностью. Рассмотрим код:
Это код правильно скомпилируется, но при выполнении выдаст ошибку нарушения доступа. Причина в том, что строка S (refCnt = -1) находится в сегменте памяти, защищенном от записи. Поэтому казалось бы, безобидная процедура:
вызовет ошибку нарушения доступа при передаче в нее строки с refCnt = -1. Чтобы получить уникальную ссылку для строки, состоящей из некоторой последовательности символов, можно воспользоваться функцией UniqueString. Это позволяет ускорить вычисления со строками, так как при этом можно будет сравнивать строки, просто сравнивая указатели на них. У таких строк refCnt всегда равен 1. Динамические массивыВ отличие от строк, динамический массив не может инициализироваться литералами, поэтому он не может иметь refCnt, равный -1. Причем Delphi уже не контролирует изменение содержимого массива, а создает новый массив только при попытке изменения его размера (причем даже если сам размер не меняется), если refCnt > 1.
В этом смысле поведение массива и строки принципиально отличается. Для иллюстрации этого различия рассмотрим две процедуры:
После выполнения E8 переданная строка не изменится, так как при S[1] := ‘t’ будет создана новая строка. Вызов же E9 приведет к изменению содержимого массива. Рассмотрим более интересный пример. Пусть массив A не пуст.
На первый взгляд, после выполнения любой из этих процедур результат будет одинаковый, но это не так. Из первой процедуры вернется измененный массив, а из второй неизмененный. Здесь очень важен порядок. Рассмотрим, почему так происходит. При вызове любой из этих процедур A - локальная переменная на стеке, указывающая на переданный массив. Пусть входной массив A = [0,1,2,3,4].
Основная разница строк и массивов в контроле изменений содержимого, если для строк любое изменение заменяется вызовом системной процедуры, контролирующей счетчик ссылок, то обращение к элементу массива выполняется, как есть. И только изменение размера массива контролирует счетчик ссылок. Видимо создатели Delphi, старались сохранить эффективность доступа к элементам массива, и поэтому получилась такая разница в поведении. Эта разница особенно заметна при передаче как константным параметром:
Локальная переменная ResultХотя Result есть реальная локальная переменная, ее реализация в вопросе инициализации несколько отличается от вышесказанного. Причиной является то, что она создается в вызывающей процедуре, а в вызываемую функцию передается как скрытый параметр. Рассмотрим пример:
Результат может показаться неожиданным. Реализуемый Delphi код выглядит примерно так:
В данном случае сама переменная S используется как Result. Несколько другой пример работает уже, как и ожидается.
Но реализуемый код аналогичен, только вводится временная переменная Tmp исполняющая роль Result
Почему поведение столь различно? Во первых, локальная переменная Result реализуется и инициализируется в nil в вызывающей функции и передается в вызываемую функцию по ссылке. Во вторых, сама вызываемая функция ее уже не инициализирует. По этой причине, Result как локальная переменная, при входе в функцию может содержать любое значение.
Переменные типа строки и динамические массивы все-таки указателиЕще раз хотелось бы напомнить, что переменная типа длинная строка или динамический массив есть указатель. Непонимание этого может привести к непредсказуемым эффектам. Например:
Каково значение Str при вызове E16(S) из E17? Формально, раз параметр передается в процедуру E16 по значению, а тем более как константа, то изменение глобальной переменной S ничего не должно изменить. Но так как параметр есть только указатель, то изменение глобальной переменной S создаст новую строку, а память под старой будет освобождена (счетчик ссылок равен 1). В данном примере это неминуемо приведет к ошибке доступа к памяти. Можно привести множество подобных примеров, так что следует лучше полагаться на знание реализации таких типов данных, чем на формальные правила языка. Хранение в переменных другого типаРассмотрим код:
Спрашивается, куда указывает P после выполнения процедуры E18? Естественно на мусор, так как память под строку S по выходе из процедуры будет освобождена. По этой же причине компилятор не позволяет помещать длинные строки и динамические массивы в вариантную часть записи, он просто не знает, что реально там будет и не может вставить код для управления ссылками и памятью. Тем не менее, иногда возникает такая необходимость. Для решения данной проблемы нужно присваивать в предыдущем примере
это приведет к увеличению счетчика, и все будет нормально, кроме вопроса освобождения памяти от строки, так как тип P не string и Delphi не генерирует код освобождения памяти. Вам самим придется вызвать
Для массива абсолютно аналогично. Многомерные динамические массивыЕсли одномерные массивы реализуются как непрерывный блок памяти, то многомерные динамические массивы реализуются как массивы указателей на массивы. Например, вызов SetLength(A,2,3) создает 3 массива:
В этом смысле организация многомерного динамического массива отличается, от статического. Если статический массив непрерывный блок памяти, то динамический массив это массив массивов. Такая организация позволяет создавать не прямоугольные массивы. Например:
Ошибка компилятора DelphiРаботая еще на Delphi6, я столкнулся с очень неприятной ошибкой. Рассмотрим абсолютно безобидную функцию:
При включенной оптимизации {$O+} и отключения range-checking {$R-}, вызов этой функции приводил к ошибке доступа к памяти. Компилятор порождал, очевидно, ошибочный код:
Причем эта ошибка никак не связана с динамическими массивами, то же самое происходило и с функцией:
Когда в Borland узнали об этой ошибке, они ее быстро исправили и в Delphi6.2, она уже отсутствует. Для тех, кто продолжает работать с более старыми версиями, я советую обратить на это внимание. Для правильной работы функции E18, ее следует несколько изменить:
Такой код, хоть и менее оптимально, правильно компилируется во всех версиях (я надеюсь). Функции для работы с массивамиК сожалению, некоторые операции, такие, как удаление и вставка в одномерный массив, требуют написание кода по копированию элементов массива. Предлагаемые процедуры позволяют выполнить эти операции для произвольного массива одним вызовом.
Кроме того, для алгоритма быстрой сортировки, нет никакой необходимости что-либо знать о типе и содержимом массива, достаточно иметь две процедуры: сравнения и обмена. Аналогично и для двоичного поиска, необходима только функция сравнения. Это позволяет написать функции сортировки и двоичного поиска в любом массиве.
Несколько замечаний по применению:
Ссылки по теме
|
|