Расположение указателей на сложные записи
У меня есть список указателей на некоторые сложные записи. Иногда, когда я пытаюсь избавиться от них, я получаю неверную ошибку указателя. Я не совсем уверен, правильно ли я их создаю и утилизирую. Запись выглядит так:
type
PFILEDATA = ^TFILEDATA;
TFILEDATA = record
Description80: TFileType80; // that's array[0..80] of WideChar
pFullPath: PVeryLongPath; // this is pointer to array of WideChar
pNext: PFILEDATA; // this is pointer to the next TFILEDATA record
end;
Как я понимаю, когда я хочу указатель на такую запись, мне нужно инициализировать указатель и динамические массивы следующим образом:
function GimmeNewData(): PFILEDATA;
begin
New(Result);
New(Result^.pFullPath);
end;
Теперь, чтобы избавиться от ряда этих записей, я написал это:
procedure DisposeData(var pData: PFILEDATA);
var pNextData: PFILEDATA;
begin
while pData^.pNext <> nil do begin
pNextData := pData^.pNext; // Save pointer to the next record
Finalize(pData^.pFullPath); // Free dynamic array
Dispose(pData); // Free the record
pData := pNextData;
end;
Finalize(pData^.pFullPath);
Dispose(pData);
pData := nil;
end;
Когда я запускаю свою программу в режиме отладки (F9) в Delphi 2010 IDE, происходит что-то странное. Когда я делаю код DisposeData с помощью F8, кажется, что программа пропускает строку Finalize(pData^.pFullPath) и переходит к Dispose(pData). Это нормально? Также при выполнении Dispose (pData) окно Local variable, в котором отображается содержимое указателей, не изменяется. Значит ли это, что утилизировать не удастся?
Изменить: PVeryLongPath это:
type
TVeryLongPath = array of WideChar;
PVeryLongPath = ^TVeryLongPath;
Edit2
Поэтому я создаю 2 записи TFILEDATA, а затем уничтожаю их. Затем я снова создаю те же 2 записи. Почему-то на этот раз pNext во второй записи не равно нулю. Это указывает на 1-ую запись. Уничтожение этой странной вещи приводит к ошибке операции с недействительным указателем. Случайно я вставил pData^.pNext:= nil в процедуру DisposeData. Теперь код выглядит так:
procedure DisposeData(var pData: PFILEDATA);
var pNextData: PFILEDATA;
begin
while pData <> nil do begin
pNextData := pData^.pNext;
pData^.pNext := nil; // <----
Dispose(pData^.pFullPath);
Dispose(pData);
pData := pNextData;
end;
end;
Ошибка ушла. Я постараюсь изменить PVeryLongPath на TVeryLongPath.
5 ответов
Тот факт, что вы приняли ответ Серга, указывает на то, что с кодом создания вашего узла что-то не так. Ваш комментарий к этому ответу подтверждает это.
Я добавляю это как новый ответ, потому что изменения в вопросе существенно меняют его.
Код связанного списка должен выглядеть так:
var
Head: PNode=nil;
//this may be a global variable, or better, a field in a class,
//in which case it would be initialized to nil on creation
function AddNode(var Head: PNode): PNode;
begin
New(Result);
Result.Next := Head;
Head := Result;
end;
Обратите внимание, что мы добавляем узел в начало списка. Нам не нужно инициализировать Next
в nil
в любом месте, потому что мы всегда назначаем другой указатель узла Next
, Это правило важно.
Я написал это как функцию, которая возвращает новый узел. Поскольку новый узел всегда добавляется в заголовке, это несколько избыточно. Поскольку вы можете игнорировать возвращаемые значения функции, это не причинит никакого вреда.
Иногда вы можете захотеть инициализировать содержимое узла при добавлении новых узлов. Например:
function AddNode(var Head: PNode; const Caption: string): PNode;
begin
New(Result);
Result.Caption := Caption;
Result.Next := Head;
Head := Result;
end;
Я очень предпочитаю такой подход. Всегда убедитесь, что ваши поля инициализированы. Если нулевая инициализация вам подходит, вы можете использовать AllocMem
создать свой узел.
Вот более конкретный пример использования такого метода:
type
PNode = ^TNode;
TNode = record
Caption: string;
Next: PNode;
end;
procedure PopulateList(Items: TStrings);
var
Item: string;
begin
for Item in Items do
AddNode(Head, Item);
end;
Чтобы уничтожить список, код работает так:
procedure DestroyList(var Head: PNode);
var
Next: PNode;
begin
while Assigned(Head) do begin
Next := Head.Next;
Dispose(Head);
Head := Next;
end;
end;
Вы можете ясно видеть, что этот метод может вернуться только тогда, когда Head
является nil
,
Если вы инкапсулируете свой связанный список в класс, то вы можете сделать указатель на голову членом класса и избежать необходимости передавать его.
Главное, что я хотел бы отметить, - это то, что код ручного распределения памяти деликатный. Легко сделать небольшие ошибки в деталях. В подобных ситуациях выгодно помещать тонкий код во вспомогательные функции или методы, поэтому вам нужно написать его только один раз. Связанные списки являются отличным примером проблемы, которую нравится решать с помощью дженериков. Вы можете написать код управления памятью один раз и использовать его для всех типов узлов разных типов.
Во-первых, если вы что-то освобождаете, содержимое указателей на это не меняется. Вот почему вы не видите изменений в отображении локальных переменных.
РЕДАКТИРОВАТЬ: объявить pFullPath
как TVeryLongPath
, Это уже ссылочный тип, и вы не должны использовать указатель на такой тип. New() не делает то, что вы думаете, в таком случае.
Вероятно, было бы лучше, если бы вы объявили его как UnicodeString или если ваш Delphi не имеет его, WideString.
Если pFullPath объявлен как динамический "массив WideChar", то вы не должны использовать New() для него. Для динамических массивов используйте SetLength() и ничего больше. Dispose() правильно удалит все элементы в вашей записи, так что просто сделайте:
New(Result);
SetLength(Result^.pFullPath, size_you_need);
и позже:
Dispose(pData);
В обычном коде вам никогда не придется вызывать Finalize(). Об этом заботится Dispose, пока вы передаете указатель правильного типа в Dispose ().
Я рекомендую вам избегать использования динамического массива WideChar, с которым вообще не удобно работать. Вместо этого используйте string
если у вас Delphi 2009 или более поздняя версия, или WideString
для более ранних версий Delphi. Оба они являются динамическими строковыми типами с WideChar
элементы. Вы можете назначить им, и Delphi решает все задачи.
Итак, предположим, что теперь у вас есть следующая запись:
TFILEDATA = record
Description80: TFileType80;
pFullPath: WideString;
pNext: PFILEDATA;
end;
Вы можете значительно упростить вещи.
function GimmeNewData(): PFILEDATA;
begin
New(Result);
end;
procedure DisposeData(var pData: PFILEDATA);
var pNextData: PFILEDATA;
begin
while pData <> nil do begin
pNextData := pData^.pNext;
Dispose(pData);
pData := pNextData;
end;
end;
Я думаю, что большинство ваших проблем вызвано предположением, что New()
дает вам память, которая обнуляется. Я почти уверен (и я также уверен, что кто-то исправит меня, если я ошибаюсь), но Delphi не гарантирует, что это так. Это можно исправить, изменив свой код на это:
function GimmeNewData(): PFILEDATA;
begin
New(Result);
ZeroMemory(Result, SizeOf(TFILEDATA));
end;
Вы всегда должны либо обнулять память, выделенную для записи, либо, по крайней мере, заполнять все поля чем-то другим. Это поведение отличается от объектов, которые гарантированно обнуляются при распределении.
Надеюсь это поможет.
Вы также должны инициализировать pNext
поле для nil
- без этого вы, наконец, получите нарушение доступа. Принимая во внимание то, что уже было сказано в предыдущих ответах, вы можете изменить свой код как
type
TFileType80 = array[0..80] of WideChar;
PFILEDATA = ^TFILEDATA;
TFILEDATA = record
Description80: TFileType80;
FullPath: WideString;
pNext: PFILEDATA;
end;
function GimmeNewData: PFILEDATA;
begin
New(Result);
Result^.pNext:= nil;
end;