basic-c/sections/09-arrays.tex

377 lines
46 KiB
TeX
Raw Normal View History

2021-09-29 14:11:47 +03:00
\section{Массивы}
В этом разделе нас с вами ждут массивы. Много массивов. И ещё пара слов о директивах компилятору, иногда также называемых директивами препроцессора. С них и начнём.
\subsection{Директива \code{\#define}}
2021-09-30 16:01:28 +03:00
\label{text:define}
2021-09-29 14:11:47 +03:00
Помимо уже хорошо знакомой вам директивы \code{\#include}, частично описанной в разделе \hyperref[text:directive]{\ref{text:directive}}, естественно, существуют и другие. Некоторые из них ограничивают импорт описанных в заголовочном файле функций, некоторые <<\textbf{описывают}>> какие-то константы и даже действия. Вот, директиву \textbf{описать} мы и рассмотрим подробнее. Она не зря называется директивой препроцессора, поскольку даёт указание не процессору во время выполнения программы выделить память, присвоить значения, а непосредственно компилятору: заменить в тексте программы одни слова на другие. Таким образом можно задавать константы проекта, и даже делать сокращённые записи целых действий. Например, написав \code{\#define ARRAY\_LENGTH 50} мы предпишем компилятору, перед запуском трансляции нашего кода заменить все слова \code{ARRAY\_LENGTH} на цифру 50. В такой записи, слово \code{ARRAY\_LENGTH} будет называться \textit{макроконстантой}.
\frm{Обратите внимание, что директива пишется немного не так, как обычный оператор языка, хоть и может находиться в любом месте кода. В конце директивы не ставится точка с запятой. Это важно именно потому что директивы работают с текстом программы, то есть если точка с запятой всё же будет поставлена, текст программы будет всегда содержать вместо макроконстанты число и точку с запятой, что может в корне изменить смысл программы.}
Весьма удобно, но этим можно не ограничиваться, мы можем попросить компилятор заменить вызовы функций и операторы на короткие, удобные нам слова. Важно помнить, что директивы препроцессора работают с текстом программы, поэтому не осуществляют никаких дополнительных проверок. Это сложный и мощный инструмент, который чаще всего используется для решения нетривиальных задач, например, выбор кода, который попадёт в компиляцию в зависимости от операционной системы. Иногда в программах можно встретить описание недостающего, но такого привычного булева типа при помощи директив препроцессора:
\begin{lstlisting}[language=C,style=CCodeStyle]
#define bool int
#define true 1
#define false 0
\end{lstlisting}
Но нам пока что достаточно умения создать глобальную именованную константу. Код ниже демонстрирует, что директивы не обязательно группировать именно в начале файла, а можно использовать там, где это удобно и уместно, так мы можем объявить константу с длиной массива в начале файла, а можем прямо внутри функции \code{int main (int argc, char *argv[])}.
\begin{lstlisting}[language=C,style=CCodeStyle]
int main(int argc, char* argv[]) {
#define ARRAY_LENGTH 50
int a = ARRAY_LENGTH;
printf("a = %d", a);
return 0;
}
\end{lstlisting}
Результатом работы этой функции будет ожидаемое:
\begin{verbatim}
$ ./program
a = 50
\end{verbatim}
\subsection{Массивы}
Вступление про директивы препроцессора напрямую не связано с темой массивов, но директива \code{\#define} для объявления размера массива применяется чрезвычайно часто. Рассмотрим природу этого явления чуть позже.
\frm{Массив это множество данных одного типа, расположенных в памяти подряд.}
Язык С строго типизирован, поэтому невозможно создать массив из разных типов данных. На данном этапе мы рассматриваем только простые типы данных, поэтому и массивы будем рассматривать статические. Статическим массивом называют массив, количество элементов которого заранее известно и не изменяется за время работы программы. Альтернативой статическому массиву является динамический, таких массивов в языке С не существует, но всегда можно самостоятельно описать такую структуру данных, которая будет хранить значения, динамически расширяясь и сужаясь. Также для начала ограничим нашу беседу одномерными массивами, то есть такими, которые можно записать в виде значений через запятую. Статические одномерные массивы принято объявлять двумя способами:
\begin{itemize}
\item простое объявление с указанием размера;
\item объявление, совмещённое с инициализацией
\end{itemize}
Для примера объявим массив, содержащий элементы типа \code{int}, дадим ему идентификатор или имя массива \code{arr} (сокращённо от англ array), укажем максимальное количество элементов которые может вместить в себя массив, например, пять.
\begin{figure}[h!]
\begin{lstlisting}[language=C,style=CCodeStyle]
int arr[5];
arr[0] = 10;
arr[1] = 20;
arr[2] = 30;
\end{lstlisting}
\end{figure}
Как уже говорилось массив это множество данных или элементов. К каждому элементу массива можно обратиться по его номеру, который принято называть индексом. Индексация элементов начинается с нуля. Давайте заполним наш массив значениями типа \code{int}. Для этого последовательно обратимся к каждому элементу и присвоим значение. Обратите внимание, что язык С не гарантирует что инициализационное значение элементов массива будет равно нулю, если это не указано явно, поэтому выведя на экран содержимое массива, мы можем гарантировать значения только первых трёх элементов, которые мы указали в коде. Второй способ объявления, совмещённый с инициализацией массива используют, если массив сравнительно небольшой и его значения заранее известны, например:
\begin{lstlisting}[language=C,style=CCodeStyle]
int arr[6] = {1, 1, 2, 3, 5, 8};
\end{lstlisting}
При этом, если сразу заполняются все элементы, размерность можно не указывать. Итак, мы научились создавать и заполнять значениями массивы. Теперь общее правило объявления массивов в С: при объявлении массива нужно указать его имя, тип элементов, количество элементов, опционально - указать сами эти элементы. Количество элементов есть натуральное число, то есть целое положительное, ноль не может быть количеством элементов. Нельзя задавать переменное количество элементов массива.
\begin{figure}[h!]
\begin{lstlisting}[language=C,style=CCodeStyle]
int nArr[100]; // An array for 100 int's;
float fArr[5]; // An array for 5 float's;
char cArr[2]; // An array for 2 char's;
int varElem;
int nArr[varElem]; // Compile error! Number of elements must be constant;
\end{lstlisting}
\end{figure}
Так мы обязаны создавать массивы только с точно указанным числом элементов. Для языка С это позволено сделать объявлением константы времени исполнения \code{const int elements; int arr[elements]}, но, например, в С++ такая запись вызовет ошибку компиляции, поэтому там необходимо строго указывать размер числовым литералом или объявив его директивой \code{\#define}, что, фактически, одно и тоже.
\frm{В более поздних стандартах С++ появилось ключевое слово \code{constexpr}, позволяющее объявлять константы времени компиляции и отказаться от объявления размеров массива только литералом}
Теперь давайте научимся получать доступ к элементам массива. Нет ничего проще, тем более, что мы это уже делали объявляли массив и для примера его заполняли. Для доступа к конкретному элементу массива нужно указать имя массива и индекс элемента в квадратных скобках. Квадратные скобки - это тоже оператор языка, он называется оператором индексного доступа:
\begin{figure}[h!]
\begin{lstlisting}[language=C,style=CCodeStyle]
int a = arr[0];
printf("lets see whats in 0-th element: %d", a);
\end{lstlisting}
\end{figure}
При помощи массивов решают множество задач, таких как поиск, сортировка, составление таблиц соответствия, создание частотных диаграмм. На основе массивов создают более сложные структуры данных. Для короткого минимального примера, давайте напишем программу, которая будет печатать наш массив в консоль.
\begin{figure}[h!]
\begin{lstlisting}[language=C,style=CCodeStyle]
#include <stdio.h>
int main(int argc, char *argv[]) {
int arr[5];
int i;
printf("Your array is: ");
for (i = 0; i < 5; i++) {
printf("%d ", arr[i]);
}
return 0;
}
\end{lstlisting}
\end{figure}
Такая несложная программа даст нам следующий результат (Обратите внимание, что при такой инициализации, а точнее её отсутствии, значения внутри массива не гарантируются. В результате запуска программы на компьютере автора первые четыре индекса оказались равными нулю, а пятый принял странное отрицательное целочисленное значение):
\begin{verbatim}
$ ./program
Your array is 0 0 0 0 -497067408
\end{verbatim}
Мы научились создавать, инициализировать массивы и обращаться к его элементам. Теперь решим задачу посложнее: напишем программу, которая проверит насколько статистически хорош описанный в стандартной библиотеке (языка С) генератор псевдо-случайных чисел (функция \code{rand();}). Для такой статистической проверки нам понадобится сформировать так называемый \textit{частотный массив}, массив, в котором будет содержаться информация о том, сколько раз то или иное число появилось во множестве значений, полученном при помощи генератора псевдослучайных чисел, частота вхождения значений. Сама генерация псевдослучайных чисел происходит при помощий функции \code{rand();} которая создаёт целое число типа \code{int}. Но, поскольку целое число в таком диапазоне нам не нужно, мы его сократим при помощи оператора получения остатка от деления.
\begin{figure}[h!]
\begin{lstlisting}[language=C,style=CCodeStyle]
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define ARRAY_LENGTH 10
#define NUMBERS_AMOUNT 1000000
int main( int argc, char *argv[]){
srand(time(NULL)); // initialize PRNG
int frequency[ARRAY_LENGTH] = {0};
int a;
int i;
for (i = 0; i < NUMBERS_AMOUNT; i++) {
a = rand() % ARRAY_LENGTH;
frequency[a]++;
}
for (i = 0; i < ARRAY_LENGTH; i++) {
printf("Number %d generated %6d (%5.2f%%) times\n",
i,
frequency[i],
((float)frequency[i] / NUMBERS_AMOUNT * 100));
}
return 0;
}
\end{lstlisting}
\end{figure}
Обратите внимание на 14ю строку: для сгенерированного на 13й строке числа \code{0} увеличим значение в 0-й ячейке массива, для числа \code{1} - в 1-й, и т.д. Данная программа наглядно демонстрирует не только работу с массивами, но и то, что генератор псевдослучайных чисел в языке С генерирует статистически верную последовательность случайных чисел. Инициализация генератора псевдослучайных чисел на восьмой строке \code{srand(time(NULL));} происходит значением текущего времени системы, то есть мы гарантируем, что начальное значение генератора будет отличаться от запуска к запуску, а значит наше исследование будет чуть более достоверным.
\begin{figure}[h!]
\begin{verbatim}
$ ./program
Number 0 generated 99955 (10.00%) times
Number 1 generated 99977 (10.00%) times
Number 2 generated 100156 (10.02%) times
Number 3 generated 99952 (10.00%) times
Number 4 generated 100212 (10.02%) times
Number 5 generated 100713 (10.07%) times
Number 6 generated 99418 ( 9.94%) times
Number 7 generated 99768 ( 9.98%) times
Number 8 generated 99918 ( 9.99%) times
Number 9 generated 99931 ( 9.99%) times
\end{verbatim}
\end{figure}
Запуск программы (даже несколько запусков) показывает, что всех значений получилось около десяти процентов, что говорит о том, что последовательность псевдослучайных чисел статистически верна.
\subsection{Идентификатор массива}
Это будет непросто, но мы поговорим о том, что из себя представляет идентификатор массива, чем чреват выход за пределы массива, затронем тему арифметики указателей и научимся передавать указатели на массивы в функции. Как упоминалось ранее, массив - это ссылочный тип данных. То есть в идентификаторе хранится адрес, ссылка на первый байт первого элемента массива, дальнейший доступ к элементам осуществляется посредством \textit{смещения относительно этого байта}. Таким образом запись вида \code{array[0]} говорит нам о том, что нужно взять адрес массива и сместить указатель на \code{0} элементов того типа, из которых состоит массив. Отсюда становится ясно, почему \textbf{индексирование массивов начинается с нуля}.
Давайте попробуем визуализировать положение вещей в массивах. На рисунке со стр. \hyperref[pic:arrays]{\pageref{pic:arrays}} можно увидеть создание массива на первой строке, далее доступ к двум его ячейкам для записи в них значений.
\begin{figure}[h!]
\begin{multicols}{2}
\begin{lstlisting}[language=C,style=CCodeStyle]
int arr[ARRAY_LENGTH];
arr[0] = 20;
arr[1] = 50;
\end{lstlisting}
\columnbreak
\begin{tikzpicture}
\draw[step=0.5cm,gray,very thin] (0,-0.5) grid (5, 2);
\draw(0, -0.5) -- (5, -0.5) -- (5, 0) -- (0, 0) -- cycle;
\draw(0, 0.5) -- (5, 0.5) -- (5, 1) -- (0, 1) -- cycle;
\draw(0, 1.5) -- (5, 1.5) -- (5, 2) -- (0, 2) -- cycle;
\fill[blue!40!white] (0, 0.5) rectangle (0.5, 1);
\fill[blue!40!white] (0.5, -0.5) rectangle (1, 0);
\end{tikzpicture}
\end{multicols}
\caption{Визуализация создания и индексации массива}
\label{pic:arrays}
\end{figure}
Соответственно, когда мы берем элемент с нулевым смещением, то есть по нулевому индексу, это будет ячейка памяти, находящаяся ровно по адресу массива. Все остальные ячейки мы просто игнорируем. Далее, берем следующий элемент массива. Для этого возьмем смещение в единицу, допустим, это будет число 50. Соответственно смещение будет на одну ячейку типа \code{int}. То есть мы игнорируем нулевой элемент и берем первый. Становится очевидно, что если мы в своём коде напишем такой индекс, который находится за пределами описанного массива - мы просто получим какое-то значение, которое никак не можем прогнозировать.
Относительно выхода за пределы массива надо сказать, что ни компилятор, ни тем более операционная система никаких проверок не делают, поэтому такие проверки (чаще всего самопроверки на этапе написания кода) полностью ложатся на плечи программиста. Язык С не предоставляет никаких сред виртуализации, никаких подсистем исключений, только выдача случайных данных, которые могут попасться нашей программе. Или запись в ячейки, которые совершенно не гарантированно останутся пустыми.
\frm{В связи с тем, индекс массива - это значение смещения, относительно его начала, и, как следствие, индексы массива всегда отсчитываются с нуля, важно помнить, что при создании массива из, например, десяти элементов десятый индекс будет находиться за пределами массива.}
Надо сказать, что всё-таки б\'{o}льшая часть значений за пределами массива будет равна нулю, но всё равно лишний раз экспериментировать не стоит.
\begin{figure}[h!]
\begin{multicols}{2}
\begin{lstlisting}[language=C,style=CCodeStyle]
arr[11] = 60;
\end{lstlisting}
\columnbreak
\begin{tikzpicture}
\draw[step=0.5cm,gray,very thin] (0,-0.5) grid (6, 0);
\draw(0, -0.5) -- (5, -0.5) -- (5, 0) -- (0, 0) -- cycle;
\fill[red!40!white] (5.5, -0.5) rectangle (6, 0);
\end{tikzpicture}
\end{multicols}
\caption{Выход за пределы массива}
\label{pic:outofbounds}
\end{figure}
Как мы уже знаем, в идентификаторе массива хранится ссылка на первый байт первого элемента массива, т.е. идентификатор является, по сути, указателем. Но существует несколько отличий: указатель - это переменная, к ней применимы, например, операции инкремента и декремента, чего конечно нельзя делать с идентификатором массива.
\frm{Идентификатор массива \textbf{не является} lvalue, Но один элемент массива является lvalue. Так, записи вида \code{arr++;} или \code{arr = 5} будут являться ошибочными, поскольку в них происходит обращение не к конкретному элементу с целью его изменения, а к массиву целиком.}
Обратившись к идентификатору массива мы можем получить доступ к элементам массива не только при помощи записи индекса в квадратных скобках, но и при помощи так называемой арифметики указателей. Мы знаем, что массив - это единая область памяти, и значения в нём располагаются подряд по очереди, значит, отсчитав от указателя на первый индекс нужное количество байт - мы получим указатель на второй индекс. Давайте для примера подсчитаем среднее арифметическое всех чисел в массиве, с использованием арифметики указателей.
Будем запрашивать значения для расчётов у пользователя. Создадим вспомогательную переменную \code{float result;}, для хранения результата и в цикле будем запрашивать у пользователя числа. Количество введенных цифр должно соответствовать количеству элементов массива, поэтому условием выхода из цикла будет равенство счётчика и последнего индекса массива, то есть, длины массива минус единица.
\frm{Обратите внимание, что в коде мы указали условием выхода из цикла \textbf{строгое неравенство}, то есть со значением \code{i} равным длине массива в тело цикла мы не попадём.}
Помним, что индексация массива начинается с нуля, поэтому длина массива всегда на единицу больше последнего индекса. Выведем в консоль надпись <<введите значение>>, при помощи функции \code{scanf();} считаем его и сразу привычным образом, оператором индексного доступа, положим в массив.
\begin{figure}[h!]
\begin{lstlisting}[language=C,style=CCodeStyle]
int arr[ARRAY_LENGTH];
int i = 0;
float result = 0;
while (i < ARRAY_LENGTH) {
printf("Enter value %d: ", i);
scanf("%d", arr[i]);
i++;
}
\end{lstlisting}
\end{figure}
Выведем в консоль получившийся массив при помощи цикла \code{for(;;)} и привычной нам функции \code{printf();}. Следом напишем ещё один цикл в котором подсчитаем среднее арифметическое. Для этого к результату будем прибавлять существующий результат и значение массива на которое указывает уже не такая привычная, как квадратные скобки конструкция \code{*(arr + i)}, которую сразу и разберём.
\begin{figure}[h!]
\begin{lstlisting}[language=C,style=CCodeStyle]
printf("Your array is: ");
for (i = 0; i < ARRAY_LENGTH; i++)
printf("%d ", arr[i]);
printf("\nAnd the average is: ");
for (i = 0; i < ARRAY_LENGTH; i++)
result += *(arr + i);
printf("%f\n", result / ARRAY_LENGTH);
\end{lstlisting}
\end{figure}
Как вы видите, некоторые подсчеты программа выполняет за нас - мы прибавляем к указателю единицу, двойку, тройку и т.д, а программа понимает, что надо взять не следующий по счёту байт, а следующий указатель. Так как в данном примере мы используем массив в котором хранятся значения типа \code{int}, а как вы помните \code{int} в подавляющем большинстве случаев - это четыре байта, то при увеличении указателя на единицу, мы обратимся к области памяти находящейся на четыре байта дальше идентификатора, при увеличении на двойку на восемь байт и так далее. Подсчитать среднее арифметическое не составит труда, этот алгоритм нам знаком со средней школы. Далее при помощи функции \code{printf();} выведем в консоль среднее арифметическое. Запустим, повводим цифры и убедимся что все работает.
\begin{verbatim}
$ ./program
Enter value 0: 1
Enter value 1: 2
Enter value 2: 3
Enter value 3: 4
Enter value 4: 5
Enter value 5: 6
Enter value 6: 7
Enter value 7: 8
Enter value 8: 9
Enter value 9: 10
Your array is: 1 2 3 4 5 6 7 8 9 10
And the average is: 5.500000
\end{verbatim}
Внимательный читатель мог заметить, что мы применяем операцию \textit{разыменования}. Что происходит, когда мы таким образом обращаемся к массиву? Операция разыменования получает доступ к значению, находящемуся по адресу. Адресс массива - это адрес его первого элемента, поэтому конструкция \code{*arr} вернёт значение нулевого элемента массива. А прибавление значений к этому указателю будет смещать его также, как это делает оператор квадратных скобок.
Как уже упоминалось, идентификатор массива - это не обычный указатель. Обычный указатель хранит в себе адрес какой-то другой переменной, и сам где-то хранится. Указатель на начало массива хранит в себе адрес массива, то есть адрес его нулевого элемента, и сам этот указатель находится в этом самом месте. На первый взгляд сложновато? Но пусть Вас это не сбивает с толку, на деле всё не так жутко. На деле это означает, что при передаче массива (читай идентификатора массива) в функцию в качестве аргумента, мы не должны использовать оператор взятия адреса, поскольку идентификатор массива сам по себе является указателем на собственное начало. Это открывает для нас широкие возможности по написанию функций, работающих с массивами данных. В только что написанной нами программе оформим вывод массива на экран и поиск среднего арифметического в виде функции. Опишем функции \code{printArray()} и \code{average()} в которые передадим указатель на массив и его длину, т.к. в массиве не содержится сведений о его размере.
Поскольку мы передаём в функцию указатель, то все действия которые описаны в этой функции будут происходить с массивом который мы создали в основной части программы через этот указатель, который мы передали, никакого копирования значений или чего то подобного. Для корректной работы наших функций объявим в них счётчик и изменим названия переменных на названия параметров.
\begin{figure}[h!]
\begin{lstlisting}[language=C,style=CCodeStyle]
void printArray(int* array, int length) {
int i;
for (i = 0; i < length; i++)
printf("%d ", array[i]);
}
float average(int* array, int length) {
float result = 0;
int i;
for (i = 0; i < length; i++)
result += *(array + i);
return result / length;
}
\end{lstlisting}
\end{figure}
2021-09-30 16:01:28 +03:00
Так, полный листинг этого примера в приложении \hyperref[appendix:arrayaverage]{\ref{appendix:arrayaverage}}
2021-09-29 14:11:47 +03:00
\newpage
\subsection{Многомерные массивы}
Массив в языке С может иметь сколько угодно измерений. Все массивы, с которыми мы имели дело до этого момента - одномерные, их легко визуализировать в виде простого перечисления элементов, возможно, как строки или как таблицы, состоящей из одной строки. Самые распространённые многомерные массивы - это двумерные и трёхмерные, которые легко себе представить в виде таблицы или куба соответственно. Итак, массив это структура, содержащая элементы. Двумерный массив - это массив из массивов, содержащих элементы. Трёхмерный - это массив из массивов, содержащих массивы, которые содержат элементы. И так далее. В массиве могут находиться любые типы данных, мы, для удобства, будем рассматривать работу массивов с числами.
Попробуем визуализировать двумерный массив. Создадим двумерный массив в коде, например, 5х5 элементов. Массив 5х5 это 5 столбцов и 5 строчек. Соответственно, \textit{каждая строчка это будет у нас младший индекс, а каждый столбец старший индекс}. Трехмерный массив может быть, например, 3х3х3 его можно визулизировать как всем известный кубик Рубика то есть, это три стоящих друг за другом таблицы 3х3. Также опишем его в коде ниже. Получается, что мы к таблице (ширине и высоте) добавили третье \textbf{измерение}, поэтому и массив получается \textbf{многомерным}, в данном случае, \textbf{трёхмерным}. Массивы б\'{о}льших размерностей тоже можно встретить в программах, но значительно реже, только лишь потому, что их действительно немного сложнее представить себе.
\begin{figure}[h!]
\begin{multicols}{2}
\begin{lstlisting}[language=C,style=CCodeStyle]
int twoDimensional[5][5];
\end{lstlisting}
\begin{center}
\begin{tabular}{||c|c|c|c|c||}
\hline
\hline
0,0 & 0,1 & 0,2 & 0,3 & 0,4 \\
\hline
1,0 & 1,1 & 1,2 & 1,3 & 1,4 \\
\hline
2,0 & 2,1 & 2,2 & 2,3 & 2,4 \\
\hline
3,0 & 3,1 & 3,2 & 3,3 & 3,4 \\
\hline
4,0 & 4,1 & 4,2 & 4,3 & 4,4 \\
\hline
\hline
\end{tabular}
\end{center}
\columnbreak
\begin{lstlisting}[language=C,style=CCodeStyle]
int threeDimensional[3][3][3];
\end{lstlisting}
\begin{tikzpicture}[every node/.style={minimum size=1cm},on grid]
\begin{scope}[every node/.append style={yslant=-0.5},yslant=-0.5]
\node at (0.5,2.5) {2,0,0};
\node at (1.5,2.5) {1,0,0};
\node at (2.5,2.5) {0,0,0};
\node at (0.5,1.5) {2,1,0};
\node at (1.5,1.5) {1,1,0};
\node at (2.5,1.5) {0,1,0};
\node at (0.5,0.5) {2,2,0};
\node at (1.5,0.5) {1,2,0};
\node at (2.5,0.5) {0,2,0};
\draw (0,0) grid (3,3);
\end{scope}
\begin{scope}[every node/.append style={yslant=0.5},yslant=0.5]
\node at (3.5,-0.5) {0,0,0};
\node at (4.5,-0.5) {0,0,1};
\node at (5.5,-0.5) {0,0,2};
\node at (3.5,-1.5) {0,1,0};
\node at (4.5,-1.5) {0,1,1};
\node at (5.5,-1.5) {0,1,2};
\node at (3.5,-2.5) {0,2,0};
\node at (4.5,-2.5) {0,2,1};
\node at (5.5,-2.5) {0,2,2};
\draw (3,-3) grid (6,0);
\end{scope}
\begin{scope}[every node/.append style={yslant=0.5,xslant=-1},yslant=0.5,xslant=-1]
\node at (3.5,2.5) {2,0,0};
\node at (3.5,1.5) {1,0,0};
\node at (3.5,0.5) {0,0,0};
\node at (4.5,2.5) {2,0,1};
\node at (4.5,1.5) {1,0,1};
\node at (4.5,0.5) {0,0,1};
\node at (5.5,2.5) {2,1,2};
\node at (5.5,1.5) {1,1,2};
\node at (5.5,0.5) {0,1,2};
\draw (3,0) grid (6,3);
\end{scope}
\end{tikzpicture}
\end{multicols}
\end{figure}
Как работать с многомерными массивами мы рассмотрим на примере двумерного массива. Поставим для себя задачу: сформировать таблицу Пифагора (раньше такие на тетрадях в клетку печатали на обратной стороне). Таблица подразумевает наличие двух измерений: строк и колонок. Для этого объявим константы rows и cols и присвоим им значения 10, \code{rows} это количество строк, а \code{cols}, соответственно, столбцов. Создадим двумерный массив, \code{table[rows][cols]}. Таким образом, мы создали массив размером rows, в каждом элементе которого содержится ссылка на массив размером cols, т.е. массив массивов содержащих непосредственные значения.
\frm{Важно помнить, что описанное строение массива является понятийным, и физически в памяти элементы хранятся иначе, но на основе этого понятия можно изучить работу оператора разыменования указателей в чати работы с многомерными массивами, так для получения доступа к значению с индексом \code{cube[1][1][1]} нужно трижды разыменовать \code{cube}:\newline \code{*( *( *(cube + 1) + 1) + 1)}}
Таблица Пифагора представляет собой таблицу, где строки и столбцы озаглавлены множителями, а в ячейках таблицы находится их произведение. Вот это самое произведение мы и будем сначала считать и записывать в массив, а затем выводить на экран. Заполнение многомерных массивов значениями ничем не отличается от заполнения одномерных массивов. Заполним нашу матрицу поэлементно: напишем двойной цикл который будет заполнять таблицу. Объявим переменные-итераторы. И с помощью внешнего цикла \code{for(;;)} пройдемся по всем строкам массива, а с помощью вложенного - по всем столбцам каждой строки массива, при этом будем записывать в каждый элемент (ячейку таблицы) результат умножения. Формула \code{(r + 1) * (c + 1)} позволяет исключить нули из нашей таблицы умножения.
\begin{figure}[h!]
\begin{lstlisting}[language=C,style=CCodeStyle]
const int rows = 10, cols = 10;
int table[rows][cols];
int r, c;
for (r = 0; r < rows; r++) {
for (c = 0; c < cols; c++)
table[r][c] = (r + 1) * (c + 1);
}
\end{lstlisting}
\end{figure}
И ещё раз: внешний цикл при каждой итерации перемещает нас на одну строчку вниз. Вложенный, при каждой итерации, перемещает нас на одно значение вправо. Важно понять, что на одну итерацию внешнего цикла приходится \code{cols} итераций вложенного. Т.е. с помощью такой конструкции мы всегда поочередно переберём все элементы массива.
Сразу отметим, что помимо такого, поэлементного способа, также есть способ заполнять многомерные массивы посредством конструкции в фигурных скобках, этот подход часто используется когда мы заранее знаем все значения и их не очень много. Например, вот так объявляем и инициализируем двумерный массив размером 3х4:
\begin{lstlisting}[language=C,style=CCodeStyle]
int arr[3][4] = {
{0, 1, 2, 3},
{4, 5, 6, 7},
{8, 9, 10, 11}
};
\end{lstlisting}
Как и в случае такой инициализации одномерного массива, указание размерности в квадратных скобках не обязательно. Обратите внимание, что при такой инициализации удобно писать каждую строку массива отдельной строкой в коде, соответвтующим образом расставляя фигурные скобки и запятые, такое оформление кода значительно улучшает его читаемость. Вернёмся к таблице Пифагора и напишем такой же (как и для заполнения) двойной цикл, который будет выводить на экран наши двумерные массивы в удобном для нас виде.
\begin{lstlisting}[language=C,style=CCodeStyle]
for (r = 0; r < rows; r++) {
for (c = 0; c < cols; c++)
printf("%3d ", table[r][c]);
printf("\n");
}
\end{lstlisting}
Запустив нашу программу с формированием таблицы умножения увидим, что все отлично работает. Полный листинг программы приводить не целесообразно, поскольку это цикл с заполнением и цикл с выводом, полностью привед§нные выше. К тому же, такой код носит исключительно академический характер, и в случае действительной необходимости формирования таблицы Пифагора на экране промежуточное заполнение массива будет излишним, результат умножения целесообразнее сразу выводить на экран.
Как уже говорилось, все массивы могут содержать данные любых типов, в том числе и указатели. Именно это позволяет массиву хранить другие массивы, строки и прочие ссылочные типы данных. Используя массивы указателей, мы можем создать, например, массив строк.
\begin{lstlisting}[language=C,style=CCodeStyle]
char* stringArray[3] = {"Hello", "C", "World"};
int r;
for (r = 0; r < 3; r++)
printf("%s ", stringArray[r]);
\end{lstlisting}
Это указатели на строки, а точнее, на строковые литералы. Такой тип данных (строковый литерал) является указателем. И мы можем создать из этих указателей массив. Используя массивы указателей, мы можем создать, например, двумерный массив, где каждый элемент не обязан быть того же размера, что и остальные (но обязан быть того же типа, как мы помним). Но строки и сложно составленные указатели - это темы, которые очень сильно выходят за рамки Основ языка, хотя, конечно, это не помешает нам немного подробнее разобраться со строками в следующем разделе.