Откуда в PChar "мусор"

версия для печати

Неудачное сочетание двух функций в Delphi 7 стоило мне нескольких часов жизни и много нервов. Зато мне удалось наконец поймать непредсказуемый PChar за хвост. Я и раньше сталкивался с необъяснимыми косяками, корнями идущими из бардака в pchar-переменных. Но сколько я об этом ни читал, сколько не копался - смысл не доходил. Теперь же я на 90% уверен, что точно знаю, чего ожидать от этого типа.

Теория

Я полагаю, что перед этой статьей вы уже почитали справочник по программированию и может даже пару других эпосов, посвященных PChar. Позвольте же и мне вставить "свои пять копеек". Итак, pchar - типизированный указатель. Переменная такого типа хранит адрес памяти, в котором начинается строка. Но строки-то бывают разной длины. Как прога узнает, сколько байт считать из памяти начиная с указанного адреса? А она просто читает подряд все байты, пока не найдет символ с кодом 0 (#0). Отсюда и название - "null-terminated string". Наличие такого «концевика» позволяет легко определить реальный размер строки на которую указывает значение pChar.

Присвоение значения PChar
Пример присвоения значения переменной типа PChar

Поясню рисунок: в память последовательно пришется текст с завершающим символом #0, в переменную возвращается адрес первого записанного символа. Конечно, память адресуется не так, как я нарисовал, и значения в нее пишутся не в десятичной СС, а в двоичной, но суть ясна. Кстати, числа в примере - это ACSII-код символов текста. Тоже важный момент.

Ноль-терминатор (#0) - это краеугольный камень всех проблем с PChar :). Из-за его отсутствия в переменной появляется всевозможный мусор. Хуже всего, когда там появляются вполне читаемые символы. Это конкретно заводит в тупик. Обычно функции, работающие с PChar, контролируют наличие ноль-терминатора. Однако при "правильном" подходе можно пробиться за пределы.

Еще следует кое-что знать о диспетчере памяти Delphi. Я и сам не до конца с этим разобрался, но как понял, рассказываю. При запуске программы для нее выделяется оперативная память. Она резервируется 1Мб-ыми секциями и выделяется блоками по 16 кБ по мере надобности. Память для глобальных переменных выделяется в сегменте данных программы и освобождается при ее завершении. Локальные переменные размещаются в стеке. Каждый раз при вызове функций/процедур для них выделяется память, а при выходе из функции/процедуры она освобождается, хотя оптимизация компилятора может их уничтожить еще во время выполнения подпрограммы. Размер стека программы ограничен минимальным и максимальным значениями. Эти значения задаются директивами компилятора $MINSTACKSIZE (по умолчанию - 16 384 байта=16 кБ) и $MAXSTACKSIZE (по умолчанию - 1 048 576 байт=1 Мб).

А теперь самое интересное: когда память особождается от локальной переменной, по умолчанию из памяти не удаляются данные. Всего лишь разрывается связь между переменной и адресом памяти. Но пока не закрыта программа, отведенная ей под стек память все еще доступна вместе с данными в ней. Вполне может быть, что при создании очередной переменной ей будет назначен адрес памяти, содержащей что-то отличное от нуля. Это не критично, т.к. переменные инициализируются, так же после присвоения значений в память попадут новые данные. Однако объявляя указатель на память все же можно заглянуть за кулисы.

Ловля на живца

Весь проект в zip-архиве. Исходники Delphi 7.

//Возвращает длину строки. Для упрощения основного кода используем доп. функцию
function len(s:PChar):ShortString; begin result:=IntToStr(StrLen(s)); end;

procedure
TForm1.Button1Click(Sender: TObject);
var s,p:PChar;
begin
  s:='Hello, world'; //Пишем в память текст, запоминаем адрес начала строки
  Canvas.TextOut(10,10,s+' '+len(s));
  GetMem(p,1024);    //Занимаем для переменной 1kБ памяти в стеке
  Canvas.TextOut(10,30, p+' '+len(p)+'     ');
  strMove(p,s,4);    //Копируем 4 символа в переменную p
//  p[4]:=#0;
  Canvas.TextOut(10,50,'What a '+p+' '+len(p)+'     ');
//  FreeMem(p);
end;

Что происходит? Нажимаем на кнопу, в переменную p должно быть скопировано "hell". Строки 8, 10 и 13 выдают текст и его длину на канву формы. Несколько нажатий кнопы дают более впечатляющий по глючности результат. В зависимости от версии Delphi возможно вы не сможете получить что-то не читабельное, поэтому приложил скиншот.

Пример работы программы
Пример работы программы

Сразу же раскажу, как правильно: раскомментировав строку (14) мы избавимся от меняющихся значений в конце строки. Если бы код сразу был верным (с особождением памяти переменной p), вряд ли бы наглядно удалось увидеть проблему, хотя она все равно остается. Раскоментировав строку (12) избавимся от глючности вообще. Этой строкой в конец скопированного текста добавляется ноль-терминатор.

Разбираемся: строка (9) занимает память в стеке для переменной. Функция GetMem() при этом не чистит занятую память, только связывает с переменной адрес нужного объема памяти. Поэтому следующая строка (10) выдает на форму символьный мусор. Далее копирование 4-символов и опять в конце строки мусор на форме! Почему? Потому, что функция StrMove() является редким примером функции, не следящей за наличием нуль-терминатора.

занимаем память
занимаем память
копируем текст
копируем текст
выдаем все до #0
выдаем все до #0

Надеюсь, из рисунков понятно: сначала StrMove() не пишет в конце текста #0, затем текст читается из памяти по указателю, пока не будет найден #0. Т.е. еще несколько байт. Кстати, функция чтения пытается и эти байты превратить в буквы, поэтому получается "мусор". Просто их значение не соответствует буквам в кодировке ASCII.

Вскрытие
function StrMove(Dest: PChar; const Source: PChar; Count: Cardinal): PChar;

Справка Delphi: "StrMove копирует точное количество символов из Source в Dest и возвращает Dest. Source и Dest могут перекрываться." И ни слова про отсутствие терминатора! Его наличие подразумевается типом возвращаемого значения, PChar. Однако это не так :( С тем же успехом она могла бы возвращать нетипизированный указатель Pointer, но по скольку в памяти все таки текст, как-то уточнили. На самом деле все еще интереснее. Функция StrMove() в основе своей использует другую функцию из модуля System.

//Модуль SysUtils
function StrMove(Dest: PChar; const Source: PChar; Count: Cardinal): PChar;
begin
  Result := Dest;
  Move(Source^, Dest^, Count);
end;

//Модуль System
procedure  Move( const Source; var Dest; count : Integer );
{$IFDEF PUREPASCAL}
var
  S, D: PChar;
  I: Integer;
begin
  S := PChar(@Source);
  D := PChar(@Dest);
  if S = D then Exit;
  if Cardinal(D) > Cardinal(S) then
    for I := count-1 downto 0 do
      D[I] := S[I]
  else
    for I := 0 to count-1 do
      D[I] := S[I];
end;
{$ELSE}
...

Далее по коду еще реализация Move() на ассемблере, но суть та же. Нигде не добавляется #0. Из-за этого допущения я тупил несколько часов, пытаясь понять, почему в возвращаемой строке то мусор, то вообще текст из предыдущих значений переменных.

Есть верный способ избавить себя от головной боли в этом случае. Можно использовать функцию AllocMem() из модуля SysUtils. Благодаря очистке памяти в конце текста будет предостаточно нулей :) Исходный код функции следующий:

function AllocMem(Size: Cardinal): Pointer;
begin
  GetMem(Result, Size);      //Занимаем память
  FillChar(Result^, Size, 0);//Чистим ее, забивая нулями 
end;
Все, что осталось не понятно мне

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

Опять возвращаясь к понятию PChar и #0. Допустим, есть такой код:

procedure TForm1.Button2Click(Sender: TObject);
var p:PChar;
    c:Char;
begin
  p:=AllocMem(100);//Явно больше, чем нужно
  p[0]:='c'; p[1]:='u'; p[2]:='t';
  p[3]:=' '; p[4]:='i'; p[5]:='t';
  p[6]:=#0;

//  p:=PChar('cut it');
  c:=p[4];
  p[1]:=c;
  p[3]:=#0;

  Button2.Caption:=p+len(p);
end;

Этот код демонстрирует некоторые возможности PChar. Можно рассматривать PChar, как массив, читать и изменять его элементы. Спокойно переходить от "указателя на Char" к собственно типу Char (строка 11). Непонятно мне следующее: если раскомментировать строку 10 (все до нее можно убрать), то получается "Access Violation.." в строках 12 и 13. Почему?

Post Scriptum

Описанная здесь проблема - не единственный случай загадочного проявления PChar. Я уже боролся с багами своих программ при использовании ресурсных и функциональных dll-ок, использующих PChar. Но это другая история :)

Я допускаю, что мои суждения могут быть ошибочны. Если вы знаете лучше меня, и есть что сказать - скажите :). Вот еще пара полезных ссылок по теме:
статья: "Строковые типы в Delphi. Особенности реализации и использования"
статья: "Управление памятью в Delphi 5.0: диспетчер памяти"

[1oo%, EoF]

Понравилась статья? Расскажите о ней друзьям:

Метки: Delphi, кодинг

Комментарии
Для работы модуля комментариев включите javaScript


Показать/скрыть правила
Имя
[i] [b] [u] [s] [url]
:-) ;-) :D *lol* 8-) :-* :-| :-( *cry* :o :-? *unsure* *oops* :-x *shocked* *zzz* :P *evil*

Осталось 1000 символов.
Код защиты от спама Обновить код
Каждый комментарий проходит ручную модерацию. 100% фильтрация спама.
Продвижение
Время
Метки