Страница 1 из 1

Запись в поток двухмерных динамических массивов

СообщениеДобавлено: 12.03.2016 14:01:43
shyub
Этот вопрос является продолжением темы http://freepascal.ru/forum/viewtopic.php?f=13&t=10973 (Как организовать запись в файл больших объёмов?).
При запси вот так:
Код: Выделить всё
BufStr.Write(M0, 75000);
содержимое простого массива записывается, а при записи из динамического в файле получается тарабарщина.
При записи вот так
Код: Выделить всё
BufStr.Write(M0[0][0], 75000);
при каждом изменении первого индекса массива (M0[0][x] становится M0[1][0] и т.д.) в файл записываются несколько непонятных знакав, а дальше, вроде, правильно.
При вот такой записи:
Код: Выделить всё
    for y:=0 to 49 do begin
      BufStr.Write(M0[y][0], 1500); // Отправить в буфер.
    end;
всё пишется правильно.
Почему возникает такая проблема?

Re: Запись в поток двухмерных динамических массивов

СообщениеДобавлено: 14.03.2016 23:43:21
Дож
Это никакая не проблема, так оно и должно работать. Вопрос является следствием непонимания того, как устроены динамические массивы и что происходит при использовании нетипизированных var-аргументов.

Массивы

Массив — это набор данных одного типа, последовательно хранящийся в памяти.

Пример:
Код: Выделить всё
var
  A: array[0..9] of LongInt;

После этого объявления переменная A указвает на последовательность данных:
| A[0] | A[1] | A[2] | A[3] | A[4] | A[5] | A[6] | A[7] | A[8] | A[9] |


Двухмерный массив

Разберём подробно вот такую вот конструкцию:
Код: Выделить всё
var
  A: array[0..2] of Array[0..2] of LongInt;


Для того, чтобы эту конструкцию было легче понять, предлагаю переписать код так:
Код: Выделить всё
type
  TArray = Array[0..2] of LongInt;
var
  A: array[0..2] of TArray;


A у нас имеет тот же тип, что и раньше, но теперь мы видим, что у нас последовательно в памяти хранятся значения типа TArray:
Код: Выделить всё
| A[0] | A[1] | A[2] |


Если раскрыть каждый из типов, то получим такую последовательность LongInt'ов в памяти:
Код: Выделить всё
| A[0][0] | A[0][1] | A[0][2] | A[1][0] | A[1][1] | A[1][2] | A[2][0] | A[2][1] | A[2][2] |


Отлично, мы разобрали на простом примере конструирование сложного массива и его упаковку в памяти. Едем дальше.

Указатели

Указатель — это значение с адресом в оперативной памяти. Указатель переменной можно получить оператором @.

Вернёмся к примеру A из прошлого раздела и посмотрим как действует указатель @ в различных ситуациях
Код: Выделить всё
@A       указатель на переменную A
@A[0]    указатель на первый элемент массива A
@A[0][0] указатель на первый элемент первого элемента массива A
@A[2][2] указатель на последний элемент последнего элемента массива A


Указатели можно сохранять в переменные. А от адреса этих переменных тоже можно брать указатели
Код: Выделить всё
var
  P: Pointer;

...
  P := @A[0][0]; // мы взяли указатель на первый элемент двухмерного массива A

  @P // указатель на переменную P, в которой хранится указатель на первый элемент двухмерного массива A


Нетипизированные var-аргументы

Язык паскаль позволяет объявлять нетипизированные аргументы функций с var модификатором. Примером может служить первый аргумент метода BufStr.Write.

Рассмотрим такой учебный пример двух функций:
Код: Выделить всё
// печатаем LongInt, находящийся в переменной V
procedure PrintV(var V);
begin
  Writeln(LongInt(V));
end;

// печатаем LongInt, находящийся по адресу P
procedure PrintP(P: Pointer);
begin
  Writeln(LongInt(P^));
end;


В чём разница этих двух функций? По большому счёту — её нет. Они делают одно и то же, просто синтаксис вызова немного отличается:
Код: Выделить всё
// печать элемента массива — отличие только в синтаксисе
PrintV(A[1][1]); // тут неявно будет передан указатель
PrintP(@A[1][1]); // берём указатель на элемент явно

// печать значение по заданному указателю — отличие только в синтаксисе
P := A[1][1];
PrintV(P^); // разыменовываем указатель, после чего он будет неявно передан
PrintP(P);


Динамическая память

Иногда может возникнуть ситуация, когда требуется хранить данные, размер которых заранее неизвестен. Проблему можно решать разными способами. Например, можно объявить массив с большим количеством элементов и его размер:
Код: Выделить всё
var
  A: array[0..100500] of LongInt; // объявляем с запасом
  Count: LongInt; // реальное число элементов, которое в данный момент храним в массиве


Подобное решение не всегда приемлемо. Во-первых, вдруг программе потребуется хранить гораздо меньше, чем 100501 чисел? Тогда программа будет держать занятой много неиспользуемой памяти. Во-вторых, вдруг программе потребуется хранить более 100501 чисел? Тогда корректность работы программы с таким решением обеспечить невозможно. Есть вариант лучше. Называется «динамическая память».

Для её использования есть две полезных функции:
Код: Выделить всё
function GetMem(size: PtrUInt):pointer;
function FreeMem(p: pointer):PtrUInt;


Первая фукнция позволяет выделить непрерывную последовательность в Size байт. Вторая — освободить выделенную ранее таким способом последовательность.

Допустим, что мы при помощи GetMem(Count * SizeOf(LongInt)) уже получили память под массив и активно его используем. Как нам расширить массив на один элемент и добавить в конец число 5? Ведь после массива в памяти могут хранится какие-то полезные данные, записывать 5 туда опасно.

Добавить элемент очень просто. Сперва выделяем память под новый массив при помощи GetMem((Count + 1) * SizeOf(LongInt)). После этого копируем Count * SizeOf(LongInt) байт из первого массива в новую область памяти. В последние SizeOf(LongInt) байт нового массива записываем 5. Освобождаем ссылку на старый массив вызовом FreeMem. Всё.

Привожу код этого простого действия:
Код: Выделить всё
type
  PLongInt = ^LongInt;
var
  P: PLongInt; // указатель на массив
  Count: LongInt; // текущее количество элементов в массиве

...

Count := 10;
P := GetMem(Count * SizeOf(LongInt); // выделяем память по массив
P^ := 3; // первый элемент — 3


Чтобы не проделывать всего перечисленного в предыдущем абзаце вручную, в паскаль (или уже в делфи) были введены динамические массивы.

Динамические массивы

Скорее всего, всё описанное выше является очевидным, — это основы программирования на паскале. Из этих основ легко достроить понимание того, как устроены динамические массивы.

Как уже было сказано, динамический массив — это конструкция, упрощающая работу с массивами переменной длины. Кроме того, элементы динамического массива хранятся последовательно в динамической памяти.

Код: Выделить всё
var
  A: array of LongInt;
...
  SetLength(A, 10);
  A[0] := 5;
  // Теперь в переменной A хранится указатель на динамическую память с элементами
  // PrintV(A); напечатает этот указатель
  // PrintV(A[0]); напечатает первый элемент 5
  SetLength(A, 100);
  // PrintV(A); возможно, распечатает другое значение, потому что указатель поменялся
  // PrintV(A[0]); всё также печатает 5


Окей — рассмотрим двухмерный динамический массив

Код: Выделить всё
var
  A: array of array of LongInt;


Как и ранее, перепишем код с сохранением типа A, но с декомпозицей составного типа на простые:
Код: Выделить всё
type
  TArray = array of LongInt;
var
  A: array of TArray;

...
  SetLength(A, 3, 3);


Что хранится в A? Указатель на динамический массив. Что хранится в A[0]? Правильно, указатель на динамический массив. Что можно сказать про компоновку всех элементов A[I][J] в памяти? Только то, что элементы A[I][0]..A[I][2] хранятся последовательно, при этом сами эти пачки могут быть раскиданы друг относительно друга разными способами.

Я устал думать что писать, теперь просто приведу словесные описания кода, что он делает
Код: Выделить всё
BufStr.Write(M0, 75000);

«записать указатель в переменной M0 и ещё 74996 байт рандомного мусора»

Код: Выделить всё
BufStr.Write(M0[0][0], 75000);

«записать элементы M0[0][0]..M0[0][1499] и ещё 73500 байт рандомного мусора»

Попытайтесь при помощи PrintV узнать что в каких значениях лежит. Writeln(PtrUInt(@M0[0][0])), Writeln(PtrUInt(@M0[1][0])) и т.д. позволит увидеть, что строки матрицы не всегда хранятся последовательно.

Дальнейшие рекомендации

1) Читать доки
2) Не передавать переменные типа динамический массив как нетипизированную var-переменную. Не брать указатель на переменную типа динамический массив. Вместо этого всегда явно использовать элемент под номером ноль, с которого последовательно в памяти идут все остальные элементы.
3) Чтобы гарантировать последовательное хранение строк динамической матрицы в памяти, не пользуйтесь динамическим массивом динамических массивов. Используйте A: array of T и SetLength(A, M*N); вместо A: array of array of T и SetLength(A, M, N). Ну, либо используйте динамический массив динамических массивов, но при сериализации нужно будет записывать каждый динамический массив отдельно, чтобы гарантировать порядок, — одним махом не получится.