Чтение онлайн

ЖАНРЫ

О чём не пишут в книгах по Delphi

Григорьев Антон Борисович

Шрифт:

 P := PChar(S);

 if Pointer(S) = P then Label1.Caption := 'Равно'

 else Label1.Caption := 'Не равно';

end;

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

UniqueString
— результат не изменится. Однако выводы делать рано. Рассмотрим следующий пример (листинг 3.29).

Листинг 3.29. Сравнение указателя после приведения пустой строки к
PChar

procedure TForm1.Button11Click(Sender: TObject);

var

 S: string;

 P: PChar;

begin

 S := '';

 P := PChar(S);

 if Pointer(S) = P then Label1.Caption : = 'Равно'

 else Label1.Caption := 'He
равно';

end;

От предыдущего он отличается только тем, что строка

S
имеет пустое значение. Тем не менее на экране мы увидим Не равно. Связано это с тем, что приведение строки
AnsiString
к типу
PChar
на самом деле не является приведением типов. Это скрытый вызов функции
_LStrToPChar
, и сделано так для того, чтобы правильно обрабатывать пустые строки.

Значение

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

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

Из-за того, что две одинаковые строки

AnsiString
разделяют одну область памяти, на неожиданные эффекты можно натолкнуться, если модифицировать содержимое строки в обход стандартных механизмов. Следующий код (листинг 3.30, пример SideChange на компакт-диске) иллюстрирует такую ситуацию.

Листинг 3.30. Побочное изменение переменной
S2
при
изменении
S1

procedure TForm1.Button1Click(Sender: TObject);

var

 S1, S2: string;

 P: PChar;

begin

 S1 := 'Test';

 UniqueString(S1);

 S2 := S1;

 P := PChar(S1);

 P[0] := 'F';

 Label1.Caption := S2;

end;

В этом примере требует комментариев процедура

UniqueString
. Она обеспечивает то, что счетчик ссылок на строку будет равен единице, т.е. для этой строки делается уникальная копия. Здесь это понадобилось для того, чтобы строка
S1
хранилась в динамической памяти, а не в сегменте кода, иначе мы получили бы Access violation, как и во втором случае рассмотренного ранее примера Constants (см. листинг 2.17).

В результате работы этого примера на экран будет выведено не Test, a Fest, хотя значение

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

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

UniqueString
и только потом работать с ней через
PChar
, если в этом есть необходимость.

Рассмотрим еще один пример, практически не отличающийся от предыдущего (листинг 3.31).

Листинг 3.31. Отсутствие побочного изменения переменной
S2
при изменении
S1

procedure TForm1.Button2Click(Sender: TObject);

var

 S1, S2: string;

 P: PChar;

begin

 S1 := 'Test';

 UniqueString(S1);

 S2 := S1;

 P := @S1[1];

 P[0] := 'F';

 Label1.Caption := S2;

end;

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

S1
по прежнему изменяется в обход стандартных механизмов Delphi.

Вся разница между двумя примерами заключается в том, как получается указатель на строку. В первом примере он является результатом приведения типа строки к

PChar
, а во втором — операции взятия адреса первого символа строки. По идее, это должно приводить к одинаковому результату, однако компилятор, зная, что указатель получается, возможно, для того, чтобы с его помощью менять содержимое строки, вставляет сюда неявный вызов
UniqueString
. В результате этого для
S1
выделяется в динамической памяти другая область, чем для
S2
, и манипуляции с содержимым
S1
больше не затрагивают
S2
.

Неявный вызов

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

Примечание

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

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

Хотя символ

#0
и добавляется в конец каждой строки
AnsiString
, он уже не является признаком ее конца, т.к. длина строки хранится отдельно. Это позволяет размещать символы
#0
и в середине строки. Но нужно учитывать, что полноценное преобразование такой строки в
PChar
невозможно — это иллюстрируется примером Zero на компакт-диске (листинг 3.32).

Поделиться с друзьями: