означает, что память для нее вообще не выделена, а указатель имеет значение
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).