Это никакая не проблема, так оно и должно работать. Вопрос является следствием непонимания того, как устроены динамические массивы и что происходит при использовании нетипизированных 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). Ну, либо используйте динамический массив динамических массивов, но при сериализации нужно будет записывать каждый динамический массив отдельно, чтобы гарантировать порядок, — одним махом не получится.