Однородные структуры данных - массивы и матрицы

1999, 2007, 2017, 20.08.2018

Массивы есть во всех современных языках программирования. За исключением экзотичных, предназначенных для узких областей применения. Массивы служат для реализации такого фундаментального математического объекта, как матрицы. После простых переменных это наиболее известное и используемое понятие. Массивы привычны, их используют не задумываясь. Ведь даже самый короткий курс по программированию или вычислительной технике обязательно включает их изучение. Но так ли просты массивы на самом деле? Ведь даже простые типы данных оказались не такими простыми.

Формат объявлений и обращений к массивам, конечно, зависят от конкретного языка программирования. Однако, как и всякое фундаментальное понятие, массивы подразумевают некие общие принципы внутренней организации, поведения и обращения. Определяют набор операций, общий для всех реализаций.

Давайте сначала посмотрим, как реализованы массивы в некоторых языках программирования. Будем рассматривать Fortran, Algol60, Basic, PL/I, C, Pascal. Выбор именно этих языков продиктован тем, что в них видны практически все варианты реализации понятия массива.

Реализация массивов в языках программирования

  • Язык Fortran. Один из первых ввел в обиход понятие массива. Первые версии языка позволяли организовывать только числовые массивы. Впрочем, и сам язык появился как средство описания вычислительных задач математики (FORTRAN=FORmula TRANslation). Более поздние версии языка значительно расширили понятие массива.

    Массивы в языке Fortran могут иметь произвольное количество индексов, или размерностей. Однако конкретные трансляторы могут накладывать ограничения. Изменить размер однажды созданного массива невозможно. Первый элемент массива всегда имеет индекс 1. Примеры описания массивов:

                DIMENSION A(30)
                REAL B(50)
                INTEGER C(10,20,30)

    Примеры обращения к массивам:

                DO 5 I=1,20
            5   C(5,I,10)=A(i)+B(I+10)*4

    Однако, этими простыми примерами не исчерпываются все возможности работы с массивами в языке Fortran. Дело в том, что Fortran, не допуская динамические массивы, тем не менее позволяет указывать размерность вычисляемую в ходе выполнения программы. Это возможно при передаче массива в процедуру или функцию. Пример:

                SUBROUTINE NEG(A,N)
                INTEGER A(N)
                DO 1 I=1,N
            1   A(I)=-A(I)
                END

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

                SUBROUTINE NEG(A,N)
                INTEGER A(1)
                DO 1 I=1,N
            1   A(I)=-A(I)
                END

    Странно, не правда ли? Объявляется массив из одного элемента, а обращаются к нескольким. Дело в том, что первые трансляторы не только не допускали указания переменной, задающей размер, при объявлении массива, но и не контролировали индексы. Поэтому объявление массива из одного элемента указывало транслятору, что в качестве параметра передается массив, и ни чего более. А раз это массив, то строка, в которой меняется знак элемента массива никаких претензий у транслятора не вызывала. Более современные трансляторы стали контролировать индексы, а для сохранения возможности написания процедур общего вида разрешили использование переменной при описании параметров процедур.

  • Язык PL/I. Задумывался как язык для решения задач ВСЕХ типов. Обладает огромной гибкостью. Но эта гибкость привела к существенному усложнению синтаксиса языка, что отразилось на времени компиляции. Кроме того поддержка времени выполнения оказалась очень громоздкой. Большое число неявных преобразований типов данных и соглашений затрудняло отладку и написание программ. Поэтому мало кто знал и тем более использовал все возможности этого языка. А возможности были впечатляющими, для своего времени. Динамические массивы, структуры, процедурный тип данных, перехват исключительных ситуаций, работа с битами, гибкий ввод-вывод, вырезки из массивов. Не удивительно, что он составил серьезную конкуренцию Fortran. Однако, с исчезновением, по крайней мере в нашей стране, компьютеров серии ЕС (IBM), этот язык канул в лету. Тогда как Fortran, безусловно утратив свои былые позиции, нашел свою нишу на персональных компьютерах. Нужно отметить, что язык PL/M, разработанный для операционной системы CP/M, не имеет никакого отношения к PL/I. Не смотря на созвучные названия это совершенно разные языки.

    Поскольку PL/I практически мертвый язык, я не буду сильно вдаваться в подробности работы с массивами в этом языке. Одной из уникальных возможностей, так и не появившихся в других языках, была возможность формирования вырезок из массивов. Насколько эта возможность была важна сказать трудно. Я не видел ни одной программы, где бы эта возможность использовалась. Разновидность вырезок есть в языке Algol68, но это тема отдельного и очень долгого разговора. Приведу пример описания массивов на языке PL/I:

                DECLARE A(50) FIXED(5);
                DECLARE B(30) FLOAT;
  • Язык Algol60. Язык легенда. Предназначенный больше для описания алгоритмов, чем для практического применения, этот язык опередил свое время по богатству возможностей и логичности построения. Основной проблемой этого языка было отсутствие стандартизованности ввода-вывода. Кроме издания огромного количества алгоритмов, язык нашел и практическое применение. Причем в некоторых странах (например, в ГДР) даже более широкое, чем Fortran.

    Наиболее важным и заметным, с точки зрения массивов, является возможность задания нижнего значения индекса. Массивы, передаваемые в качестве параметров в процедуры, можно описывать без указания размерности. Поскольку язык Algol60 ввел понятие блока begin..end, в котором можно описывать свои внутренние переменные, описание массивов с вычисляемыми границами индексов не вызывает затруднений. Приведу комплексный пример:

                real procedue pr(a,n);
                value n; integer n;
                array a;
                begin
                    real array b[-n:n];
                    real r;
                    integer i,j;
                    for i:=-n step 1 until n do
                        for j:=-n step1 until n do
                            b[i]:=b[i]+a[i,j];
                        end j;
                    end i;
                    for i=-n step 1 until n do
                        if b[i]>0 then r:=r+b[i];
                    end i;
                    pr:=r;
                end pr;

    Этот пример не более чем способ показать основные правила работы с массивами в языке Algol60.

  • Язык Pascal. Был разработан Н.Виртом как язык для обучения программированию. Базовым языком послужил Algol60. Все сложные и неоднозначные конструкции Algol были выброшены. Были добавлены недостающие элементы, такие как пользовательские типы данных. Процедуры и методы ввода-вывода стали частью стандарта языка. В результате получился компактный, наглядный и надежный язык программирования. Достаточно быстро Pascal стал популярен не только как язык обучения, но и как язык прикладного программирования. Дальнейшим развитием языка Pascal стали языки Modula и Oberon. Эти языки менее известны, в первую очередь из-за того, что Pascal был поддержан фирмой Borland. Кроме трансляторов Turbo Pascal и Borland Pascal эта фирма выпустила продукт, который стал бестселлером. Речь идет о Delphi. Правда Pascal в продуктах Borland давно перестал быть Pascal Вирта, теперь это Object Pascal, но основные идеи остались прежними. Modula и Oberon не получили такой поддержки от крупных разработчиков трансляторов, а жаль.

                program test;
                const
                    Size=30;
                type
                    Color=(Red,Green,Blue);
                var
                    CrtPixels : array [0..Size] of Color;
                    i : integer;
                begin
                    for i:=0 to Size do begin
                        case i mod 3 of
                            0: CrtPixels[i]:=Red;
                            1: CrtPixels[i]:=Green;
                            2: CrtPixels[i]:=Blue;
                        end;
                    end;
                end.

    Этот пример очень простой. Да сложнее и не требуется. Видно, что работа с массивами в языке Pascal очень похожа на работу с массивами в языке Algol60. Есть и отличия. Это первый пример, в котором массив не числовой, а определяемого пользователем перечислимого типа. И тем не менее работа с таким массивом не отличается от работы с обычным массивом целых чисел. Это важный момент.

    Стандарт языка, предложенный Виртом, не допускал создания динамических массивов. Это было сделано намеренно. Однако массивы переменного, или, по крайней мере вычисляемого размера, были необходимы при написании серьезных программ. Естественно это было реализовано в коммерческих трансляторах. Так в Borland Pascal можно объявить массив, при описании функции, как array of без указания размера, но с обязательным указанием типа. Получить значения нижнего и верхнего индексов можно функциями Low и High. Более того, можно указать факт передачи массива без указания не только размеров, но и типа. Использовать такой параметр в функции можно только с помощью явного приведения типа.

    Для работы с динамическими массива стали использовать тип ptr of, т.е. указатель. Вместе с функциями работы с памятью, предоставляемыми, например, операционной системой, это дает возможность создавать динамический массив, изменять размер или удалять его.

  • Язык Basic. Язык для начинающих и простых программ, так он по крайней мере задумывался. В оригинале, простой язык программирования, интерпретируемый а не транслируемый, требующий минимальной поддержки и минимальных ресурсов, соответственно предоставляющий минимум средств для написания программы. Любимый язык фирмы Microsoft и самого Билла Гейтса. Ведь именно с интерпретатора этого языка начинал Билл, успешная реализация которого и послужила причиной создания фирмы Microsoft.

    В понимании Microsoft язык Basic вовсе не простейший язык, а мощный современный язык программирования, включающий в себя поддержку объектноориентированного подхода, OLE, средств визуального проектирования. Это Visual Basic.

    Оригинальный Basic допускал только однобуквенные переменные. Массивы были только статические, и только одномерные. Причем если объявлялся массив без указания размерности, то считается, что массив состоит из 10 элементов. При развитии языка первыми ликвидированными ограничениями стали длинна имен переменных и количество индексов массива. В дальнейшем, при активном участии Microsoft стали допустимы и динамические массивы. Нижнее значение индекса фиксировано, и равно 0. В качестве лирического отступления можно сказать, что в первоначальном варианте Basic каждый оператор (строка) должен был начинаться с "глагола". То есть, в слежующем примере программы два оператора присваивания в цикле должны были начинаться с "LET". Кроме того, все строки программы нумеровались с шагом, по умолчанию, 10, который можно было изменять. И операторы GOTO указывали именно на номер строки, на которую осуществлялся переход.

                DIM Arr[30,30]
                DIM Matrix()
                REDIM Matrix(30,10,30)
                FOR I=30 TO 0 STEP -1
                    Matrix[I,0,I]=I
                    Arr[I,I]=Matrix[I,I,I]
                NEXT

    Как видно, возможно не только указывать, что массив динамический, но еще и на ходу менять количество индексов.

  • Язык С. Один из самых популярных языков программирования, особенно в варианте С++. Большую свободу самовыражения дает только Assembler. Несмотря на то, что им пугают начинающих программистов, достаточно простой язык. К тому же чрезвычайно мощный, компактный и выразительный. Не зря его используют даже для написания операционных систем, например Unix и Windows. Позволяет программисту контролировать абсолютно все. Однако именно эта возможность и требует учитывать все тонкости и детали, например управления памятью. Небрежность оборачивается трудноуловимыми ошибками.

    В языке С, работа с массивом во многом похожа на работу с указателем. Фактически, при объявлении массива производится выделение памяти, которое может быть и динамическим, и объявление указателя на выделенную область памяти. Разумеется указатель и массив не эквивалентны. Семантическая разница между ними огромна. Однако возможность работы с массивом как с указателем, а с указателем как с массивом, является важной особенностью языка С.

    Чем же различаются массивы и указатели, кроме семантики? Вот далеко не полный список отличий. Для многомерных массивов нельзя просто объявить указатель, а затем использовать его с индексами. Почему, станет понятным после знакомства с реализацией массивов. Нельзя освободить память из-под массива, определенного с явным указанием размерностей. В языке С++ для массива, при создании, вызываются конструкторы для всех элементов, а при уничтожении, деструкторы для всех элементов. Для указателя таких вызовов не производится.

    Позволю себе не углубляться дальше в тонкости языков С и С++, так как эта статья совсем о другом. Скажу лишь, что нижняя граница индексов массивов в языке С всегда 0.

                #include <stdlib.h>
                void func(int* a) {
                    int*    b;
                    int     c[10,5];
                    int     d[]={1,2,3,4,5};
                    int     i;
                    b=malloc(10*sizeof(int));
                    for(i=0; i<10; i++) {
                        a[i]=d[i/2]*i;
                        *(b+i)=a[i];
                        c[i,i]=a[i]+d[i/2];
                    }
                    free(b);
                }

    Обратите внимание на *(b+i), это эквивалент b[i], и демонстрирует возможность смешивания указателей и массивов.

Физическая реализация массивов

Мы рассмотрели как реализованы массивы в некоторых языках программирования. Если абстрагироваться от деталей, то становится видно, что массивы действительно практически одинаково выглядят в любом языке. Что же можно сделать с массивами? Создать , уничтожить, получить доступ к конкретному элементу, получить размерность, получить нижнее и верхнее значения конкретного индекса, изменить размер массива.

Не во всех языках возможно выполнение всех этих операций, не все операции кажутся очевидными. Например, создание и уничтожение массива. В языке Fortran эти операции нельзя выполнить явно. Но массивы тем не менее создаются и уничтожаются, автоматически, при входе в процедуру и при выходе из нее. Собственно, в Fortran IV массивы, как впрочем и локальные переменные процедур и функций, зачастую размещаются на этапе трансляции программы в статических областях памяти. При этом о создании и уничтожении массивов речи идти, естественно, не может. Однако, если компилятор размещает локальные переменные в стеке, то он просто обязан создавать и уничтожать массивы во время выполнения программы.

Все элементы массива имеют один и тот же тип. Это очень важное замечание. Именно поэтому массивы относятся к однородным типам данных, в отличии от записей, объединений, деревьев, файлов и прочих типов данных, элементы которых могут быть различных типов.

Основой работы с массивами является доступ к отдельным элементам. Физически массивы располагаются в оперативной памяти. Значит для доступа к элементу массива достаточно получить адрес этого элемента в памяти. Оперативная память представляет собой набор последовательно пронумерованных байт. По существу это одномерный массив байт. Элементы массива в языках высоко уровня могут занимать более одного байта. Кроме того массивы могут быть многомерными. Таким образом, для доступа к конкретному элементу массива в языке высоко уровня, необходимо решить задачу отображения этого массива на одномерный массив байт. Поверьте, это звучит страшнее, чем есть на самом деле.

Одномерные массивы

Сначала определимся с понятиями. В оперативной памяти компьютера, кроме массивов, находятся и другие данные, а так же код программы и код операционной системы. Иначе говоря, массив занимает только часть памяти. Адрес памяти, с которого начинается массив, называется базовым адресом массива. Мы будем обозначать это base. Размер элемента массива будем обозначать как size. На рисунке изображен массив, элементы которого занимают по два байта. Нижняя граница индекса будет обозначаться low(), например нижняя граница индекса i будет выглядеть так, low(i). Соответственно верхняя граница будет обозначаться как high(). Сам массив будем обозначать как array.

Начнем разбираться с одномерных массивов с нижней границей индекса равной 0. Очевидно, что array[0] будет располагаться по адресу base. Следующий элемент, array[1], будет иметь адрес base+size. И так далее, array[2] расположиться по адресу base+size+size=base+2*size. В общем виде это можно записать так:

                    array[i]=base+i*size

Если допустить, что нижняя граница индекса не 0, то формула немного усложнится, оставаясь тем не менее простой и очевидной:

                    array[i]=base+(i-low(i))*size

Для неверящих формулам выполним проверку. Пусть у нас есть массив, нижняя граница индекса которого -5, а верхняя 5. Итак:

                    array[-5]=base+(-5+5)*size=base
                    array[-4]=base+(-4+5)*size=base+size

Видно, что формулы дают правильные результаты.

Теперь попробуем разобраться с двумерными массивами. При этом мы сразу наталкиваемся на вопрос "какой индекс изменяется первым?". Порядок изменения индексов не важен с точки зрения языка высокого уровня, но важен с точки зрения вычисления адреса элемента. Те из Вас, кто изучал в институте язык Fortran, наверняка помнят задачи, в которых требовалось вывести на печать содержимое массива. Хитрость заключалась в том, что первый индекс соответствовал номеру строки, а второй номеру столбца. В языке Fortran при указании имени массива в операторе ввода-вывода, например в операторе PRINT, первым изменяется первый индекс. В результате массив (его еще называли матрицей) печатается повернутым на 90 градусов. Решение было простое, использовать неявные циклы. Другие решения не принимались. Несмотря на явно учебный характер тех задач они показывали одну важную, и не всегда очевидную, проблему. Проблему переносимости данных через внешние носители. Если Ваша программа, на Вашем любимом языке программирования, выводит многомерный массив на диск, а другая программа, возможно даже на машине с другой архитектурой, читает этот массив, то Вы должны или явно указывать последовательность вывода каждого элемента массива, или быть уверены, что правила изменения индексов многомерного массива одинаковы в обоих случаях. Кстати, проблема последовательности передачи элементов информации очень важна при работе в сетях.

Размещение массива в машинной памяти

Однако вернемся к массивам. На рисунке показана разница в размещении элементов массива [3,2] в зависимости от того, какой индекс изменяется первым. Вычисление адреса элемента в обоих случаях выполняется одинаково. Для наглядности рассуждений примем, что первым изменяется второй индекс, то есть сначала в памяти размещаются элементы первой строки, затем второй, и так далее. Будем рассматривать массив array[i,j]. Адрес каждого элемента в строке будет вычисляться по тем же формулам, что и для одномерного массива. Для вычисления адреса начала каждой строки нам потребуется знать, сколько памяти занимает одна строка. Очевидно, что каждая строка является одномерным массивом (это кстати объясняет, почему многомерные массивы часто рассматриваются как массивы массивов) и занимает

                    (high(j)-low(j)+1)*size

байт оперативной памяти. Первая строка двумерного массива начинается с адреса base, вторая с адреса base+(high(j)-low(j)+1)*size. Значит формулу для вычисления адреса элемента двумерного массива можно получить из формулы для одномерного массива. Нужно только заменить base на приведенное выше выражение, с учетом номера строки

                    array[i,j]=base+(i-low(i))*(high(j)-low(j)+1)*size+(j-low(j))*size

Желающие могут упростить выражение вынеся size за скобки. Проверим формулу для массива [2,3]. При этом индекс i будет изменяться от 0 до 1, а j от 0 до 2, т.е. low(i)=0, high(i)=1, low(j)=0, high(j)=2. Пусть каждый элемент занимает по два байта, т.е. size=2.

                    a[0,0]=base+(0-0)*(2-0+1)*2+(0-0)*2=base
                    a[0,1]=base+(0-0)*(2-0+1)*2+(1-0)*2=base+2
                    a[0,2]=base+(0-0)*(2-0+1)*2+(2-0)*2=base+4
                    a[1,0]=base+(1-0)*(2-0+1)*2+(0-0)*2=base+6
                    a[1,1]=base+(1-0)*(2-0+1)*2+(1-0)*2=base+8
                    a[1,2]=base+(1-0)*(2-0+1)*2+(2-0)*2=base+10

Как видно, результаты получились правильными. Для случая, когда первым изменяется первый индекс, формула сохраняется, поменяются лишь переменные i и j

                    array[i,j]=base+(j-low(j))*(high(i)-low(i)+1)*size+(i-low(i))*size

Массивы размерностью 3 ничего нового не привносят. Формула остается прежней, только base заменяется на выражение, учитывающее количество двумерных массивов и их размер. Напомню, что трехмерный массив можно рассматривать как одномерный массив двумерных массивов. Аналогично и четырехмерный массив можно рассматривать как одномерный массив трехмерных массивов. И так далее. Формулы приводить не буду. Попробуйте написать их сами.

Описанные формулы позволяют выполнить основную операцию - доступ к отдельным элементам массива. Операция создания массива сводится к определению требуемого объема памяти и собственно выделению памяти. Операция уничтожения массива просто освобождает ранее выделенную память. Операция изменения размера массива допускается достаточно редко. Если размер массива уменьшается, то просто освобождается часть занимаемой им памяти (при этом возможно потребуется перемещение части элементов для многомерных массивов). Если размер массива увеличивается, что бывает гораздо чаще, то выделяется новый блок памяти соответствующего размера, после чего содержимое массива копируется во вновь выделенную память, а старый блок памяти освобождается. Так что изменение размера массива операция достаточно дорогостоящая и медленная.

Информационные функции, такие как получение размерностей, доступны в достаточно малом количестве языков. Для их реализации требуется ведение специальных информационных структур, в которых хранятся: количество индексов, верхняя и нижняя граница каждого индекса. Разумеется, можно хранить и дополнительную информацию, такую как размер массива в байтах. Эти же структуры данных используются и для контроля выхода индексов за границы массива.

На контроле индексов остановлюсь подробнее. Эта возможность есть далеко не во всех языках и компиляторах. Иногда контроль можно включить или выключить, для всей программы или ее части. Контроль индексов может сильно замедлить Вашу программу. Действительно, для статических массивов эта операция требует дополнительно по два сравнения на каждый индекс. Для динамических массивов, кроме того, требуется обращение к массиву с границами индексов. Так что может иметь смысл использовать контроль индексов только на этапе отладки программы. Можно так же запретить контроль индексов для критичной по времени части программы, например для внутренних циклов.

Коснусь еще одной темы - ускорения доступа к элементам многомерных массивов. Как видно из формул, вычисление адреса элемента многомерного массива в памяти требует достаточно болльшого количества операций среди которых и умножение (довольно медленная операция). Если вспомнить, что многомерный массив можно рассматривать как массив массивов, то можно ускорить доступ храня в памяти адреса начала каждого подмассива. Это позволит, за счет использования большего объема памяти, экономить время вычисления адреса. Чем выше размерность массива, тем большее количество дополнительной памяти потребуется, но и экономия времени будет больше. Этот подход называется "векторизацией массивов". Например транслятор Fortran в операционной системе Рафос (RT-11) позволял включать и отключать такую оптимизацию. Умеют это делать и современные компиляторы.

Использование идеологии массивов для организации баз данных

И в заключение хочу продемонстрировать Вам универсальность описанных здесь подходов, методов и формул к, казалось бы, совершенно посторонним темам. В качестве примера возьмем файлы dBase III (dbf) и применим к ним методы работы с массивами. Думаете не возможно? Вы ошибаетесь!

Не буду подробно описывать формат файлов dbf, он достаточно широко известен, а сами файлы очень широко распространены. Напомню только, что такие файлы состоят из заголовка, содержащего информацию о базе в целом и о каждом поле, после которого идут собственно записи базы. Причем все записи имеют одинаковую длину, это для нас важно.

Длина заголовка может быть разной, но ее можно определить прочитав файл базы. Из заголовка так же определяется длина записи. Первая запись в базе имеет номер 1, то есть low=1. Этих данных для нашего примера достаточно. Забудем, на время, об удаленных записях, о необходимости корректировки полей в заголовке, при изменении или добавлении записей в базе. Осталось напомнить, что мы можем прочитать дисковый файл с любого байта, и любое количество байт. А теперь пример:

            int        dBase;            /* Дескриптор файла базы */
            int        base;             /* Длина заголовка базы в байтах */
            int        size;             /* Длина записи базы в байтах */
            int        i;                /* Номер записи */
            int        addr;             /* Адрес записи в файле */
            char*      record;           /* Собственно запись базы */


            /*
                Открываем базу и получаем дескриптор файла, длину заголовка
                и длину записи.
            */

                . . . . .

            /*
                Выделяем память для хранения записи в оперативной памяти
            */

            record=malloc(size);

            /*
                Считываем пятую запись базы
            */

            i=5;
            addr=base+(i-1)*size;
            lseek(dBase,addr,SEEK_SET);
            read(dBase,record,size);

            /*
                Изменяем запись
            */

                . . . . .

            /*
                Записываем измененую запись обратно
            */

            lseek(dBase,addr,SEEK_SET);
            write(dBase,record,size);

            /*
                Освобождаем занимаемую записью оперативную память
            */

            free(record);

            /*
                Закрываем базу
            */

                . . . . .

Видите, мы нашли начало пятой записи базы так же, как вычисляли адрес элемента массива в памяти. Не правда ли, все очень просто. Если добавить коррекцию заголовка базы при ее изменении и контроль ошибок, то получится вполне работоспособный набор функций для работы с dbf файлами. Если приложить еще немного усилий, то можно написать набор классов С++ для работы с базами dBase III. Причем это будет работать гораздо быстрее, чем обработка таких баз через ODBC, а места занимать гораздо меньше. Кстати, у меня был набор таких классов. Он написан году так в 1994, и предназначался для программистов переходящих на С++ с CLIPPERа. Как показала практика, эти классы существенно облегчили такой переход. Причем они использовались вплоть до 2009 года. Исходные тексты этих классов не привожу сознательно. Написать их не сложно, достаточно пары дней, а в качестве упражнения еще и очень полезно.

На этом небольшой рассказ о массивах и их внутреннем устройстве заканчивается. Надеюсь он получился не очень запутанным и окажется интересным для многих. Я не стал касаться использования для массивов таких классов C++, как vector. Я не стал касаться тонкостей обработки массивов в таких языках без строгой типизации, как Python. Я не стал касаться вопроса работы с массивами очень большого размера (разряженные массивы). Тема низкоуровневой работы с данными вообще, и массивами, в частности, слишком велика для небольшого цикла статей для начинающих. Она очень интересна, но это вопрос отдельных статей, требующих от читателя гораздо более глубоких знаний.