String
и PChar
добавили замечательный комментарий, на который мне хотелось бы ответить, но объём заблуждений не позволяет это сделать простым ответом в комментарии.Текст из статьи, реакцией на который был написан комментарий, видимо, такой:
Это означает, что вы не должны использоватьPChar
для указания на строки, а затем изменять строку. Лучше всего избегать таких вещей:// ПРЕДУПРЕЖДЕНИЕ: ПЛОХОЙ ПРИМЕР var S: string; P: PChar; begin S := ParamStr(0); // например, нам вернули 'C:\Test.exe'; P := PChar(S); S := 'Something else';ЕслиS
изменяется на'Something else'
, указательP
не будет изменён и будет продолжать указывать на'C:\Test.exe'
. ПосколькуP
не является строковой (в смыслеString
) ссылкой на этот текст, и у нас нет никакой другой переменной, ссылающейся на него, то его счётчик ссылок станет равным 0, и текст будет удалён из памяти. Это означает, чтоP
теперь указывает на недоступную память.
Сам же комментарий:
Michael Shamrovsky
По факту, командаPChar(S)
, гдеS: String
ничего плохого не сделает, даже если мы после присвоения переменной типаPChar
, поменяем строку. Наша куча символов не измениться, и будет указывать на живую страницу.var S: String; P: PChar; begin S := 'Hallo!'; P := PChar(S); ShowMessage(P); Delete(S, 2, 1); ShowMessage(P); end;Результат -- на экране дваждыHello!
, хотя по Вашим словам будет ЖОПА!
Учим мат часть! В начале выделяется новая страница, в ней образуется копия строки но без лидирующих 4х байт, затем дается ссылка.
PChar
это не просто преобразователь типов, но и копировщик строки!!!
Если вы хотите, вы можете самостоятельно выяснить, что не так с этим экспериментом. Считайте это очередной задачкой. Либо листайте ниже для ответа.
Оставим сладкое ("дважды
Hello!
") на конец, а пока давайте начнём со слона в комнате: "PChar
это не просто преобразователь типов, но и копировщик строки!!!". Это весьма сильное утверждение, которое легко проверить: достаточно посмотреть исходный код подпрограммы преобразования String
к PChar
. Для этого достаточно запустить программу, остановиться на строке присваивания и нажать F7 (вход в подпрограмму) - при условии, конечно, что у вас включены отладочные DCU для системного кода (Use Debug DCUs):Как видим, ни о каком копировании здесь речи не идёт: происходит прямое копирование указателя. Для надёжности можно проверить в машинном отладчике, что никакого иного кода (кроме вызова
_UStrToPWChar
) в строке присвоения нет:Упражнение: почему в машинном отладчике процедура называется
@UStrToPWChar
, а не _UStrToPWChar
?Хм, но если строка не копируется, а
P
указывает на S
, то почему-же при изменении S
не меняется P
?Ошибка в рассуждениях состоит в том, что предполагается, что операция изменения (в данном случае -
Delete
) произведёт операцию с самой строкой. Но на деле Delete
сделает копию строки и изменит её. Оригинальная строка при этом не изменится. Соответственно, P
продолжит указывать на оригинальную (не изменённую) строку.Как я узнал, что
Delete
делает копию? Отложим этот вопрос на потом, я подробно покажу это, а пока поверьте мне на слово.Постойте, но почему
Delete
делает копию? И что, в таком случае, будет являться оригиналом строки? В какой переменной он хранится, этот оригинал? Неужели, в P
и Михаил был прав?Наводящий вопрос: а где, собственно, хранится сам текст
'Hallo!'
? В строку он же должен попасть откуда-то.Возможно, я открою тайну, но: сложные константы хранятся в специальном сегменте данных программы. Поскольку константы не должны меняться, то этот сегмент имеет атрибуты "только для чтения" (read only). Это означает, что любая попытка записать что-то в этот участок памяти приведёт к возбуждению исключения
EAccessViolation
. Вот почему Delete
делает копию строки - потому что оригинальную строку изменить нельзя.Наверное, к этому моменту возникает много вопросов: не слишком ли много я делаю допущений? И что
Delete
делает копию? И что строка хранится в блоке констант, а не в переменной (т.е. не копируется при присваивании)?Ну, давайте посмотрим, как мы можем это подтвердить. Вспомним, что у строк типа
String
есть счётчик ссылок. Он увеличивается на 1 при копировании строк и уменьшается при выходе переменной за область видимости. Свежесозданные строки имеют счётчик ссылок равный 1, а удаляются из памяти, когда счётчик опускается до нуля. Соответственно, если счётчик равен 1, то на строку ссылается одна переменная (и тогда её можно спокойно модифицировать), а если счётчик больше 1, то на строку ссылается две или более переменных (и тогда строку модифицировать нельзя, надо сначала сделать копию).Как мы можем узнать счётчик ссылок?
Если у вас относительно новая Delphi, то в ней должна быть функция
StringRefCount
, а если очень старая - то нужно вспомнить внутреннее устройство строк. Строка является указателем, который указывает на начало данных строки (символов). Непосредственно перед этими данными лежит служебный заголовок - в котором, помимо всего прочего, хранится счётчик ссылок строки. Служебный заголовок представлен записью (record
) TStrRec
в модуле System
.Таким образом, если у нас есть строка
S
, то, чтобы узнать её счётчик ссылок, нужно преобразовать её в указатель (например: PAnsiChar(S)
в старых Delphi или PByte(S)
- в новых; PAnsiChar
/PByte
здесь нужен только чтобы иметь адресную арифметику), затем отступить на размер заголовка ( - SizeOf(TStrRec)
), разыменовать указатель (Pointer(...)^
), обозначить, что это - заголовок (TStrRec(...)
) и, наконец, прочитать счётчик ссылок (.refCnt
). Итого, вы можете добавить в Watches такое выражение:
TStrRec(Pointer(PByte(S) - SizeOf(TStrRec))^).refCntгде
S
- это ваша переменная типа String
.Примечание: если вы захотите использовать просто
StringRefCount
вместо этого страшного выражения, то вам нужно будет вставить вызов этой функции в любое место - этого будет достаточно, чтобы Delphi поместила машинный код функции в вашу программу. По умолчанию эта функция нигде не вызывается, её код в вашу программу не попадает, соответственно, вызывать то, чего нет, - нельзя. Ах, да, галку "Allow side effects and function calls" надо будет ещё установить в свойствах Watch-а.Посмотрим же, чему будет равен счётчик ссылок. Запустим программу. Сначала строка не инициализирована, т.е. указывает на
nil
, и счётчика ссылок у неё просто нет.Но после присвоения:
Счётчик ссылок стал равен... минус единице. Несложно сообразить, что -1 в данном случае является специальным флагом, который указывает, что память для строки не была выделена в куче (в противном случае счётчик ссылок имел бы значение просто 1), т.е. что строка является константой. Именно на этот признак смотрит
Delete
, когда принимает решение о необходимости копировать строку.Пройдёмся дальше по коду. До вызова
Delete
всё скучно, нет никаких изменений, счётчик ссылок по-прежнему равен -1. А вот после вызова Delete
:Счётчик становится равным 1 - именно потому, что
Delete
сделала копию строки. Т.е. выделила в куче новый блок памяти, счётчик ссылок которого будет равен 1. Чтобы он мог опуститься до 0, когда S
выйдет из поля видимости. И при этом память, которую выделила Delete
, будет освобождена.А что же
P
? А P
мы не меняли. Она по-прежнему указывает на "старую S
", которая, как мы помним является константой. Таким образом, в памяти программы будет две строки: одна - 'Hallo!'
(в области памяти для констант, на неё указывает P
), а другая - 'Hllo!'
(в динамической куче, на неё указывает S
). Как видим, никакого копирования памяти P
не выполняет. Михаил ошибся.Вот ещё пара экспериментов, которые можно провести: во-первых, попробуйте записать что-то в
P
:
var S: String; P: PChar; begin S := 'Hallo!'; P := PChar(S); ShowMessage(P); Delete(S, 2, 1); ShowMessage(P); P^ := 'A'; // - добавили; должно изменить первый символ строки ShowMessage(P); // - добавили end;Если вы запустите этот код, то получите исключение
EAccessViolation
при попытке записи в P
. Происходит это именно потому, что P
указывает на read-only область памяти констант, куда невозможно произвести запись. Если бы P
указывала на строку S
(которая размещена в read-write памяти кучи), то операция записи в P
была бы успешна.Во-вторых, вы можете зайти в
Delete
по F7:Как вы можете видеть,
Delete
первым действием проверяет, нужно ли копировать строку:И строка копируется, если её счётчик ссылок отличен от 1 (т.е. в том числе - когда он равен минус единице).
Вот, собственно, и всё. Правильно трактуйте результаты своих экспериментов.
P.S.
Возможно, если бы Михаил дочитал статью до конца, он бы и сам догадался:
Я не рассказал всё, что известно, и даже, возможно, немного переврал правду (например, не любая строка типаString
управляется счётчиком ссылок – к примеру, строковые константы всегда имеют счётчик ссылок равный –1), но эти мелкие детали не столь важны для общей, большой картины и не оказывают влияния на использование и взаимодействие междуString
-ми иPChar
-ми.
P.P.S.
Упражнение: чем отличается модифицирование строки указанными способами?
var S: String; P: PChar; begin S := { ... }; P := S; // Способ 1: S[1] := 'A'; // Способ 2: P^ := 'A'; end;
Александр, большое спасибо Ваш многолетний труд - хотя я уже давно не использовал Delphi, но с удовольствием продолжаю читать Ваш блог. Прекрасная разминка для ума, отличная подача материала, и очень корректный ответ на заносчивый комментарий в данном конкретном случае.
ОтветитьУдалить> счётчик ссылок равный –1
ОтветитьУдалить- одно печально, если передавать литерал в качестве параметра и параметр не имеет модификатора const, сразу выполняется копирование(в D7 как минимум).
А я так надеялся на транзитивность copy-on-write...
Так ведь при const же и не копируется?
Удалить