Расположение указателей на сложные записи

У меня есть список указателей на некоторые сложные записи. Иногда, когда я пытаюсь избавиться от них, я получаю неверную ошибку указателя. Я не совсем уверен, правильно ли я их создаю и утилизирую. Запись выглядит так:

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 ().

FWIW, я бы порекомендовал эту и эту мою статью.

Я рекомендую вам избегать использования динамического массива 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;
Другие вопросы по тегам