arrays 01-03 + minor fix of variables (frame about int size added)

This commit is contained in:
ivan-igorevich 2021-09-17 14:03:47 +03:00
parent 0328efacc2
commit 3b18f47af2
4 changed files with 177 additions and 125 deletions

Binary file not shown.

265
main.tex
View File

@ -29,14 +29,15 @@
\section{Массивы} \section{Массивы}
В этом разделе нас с вами ждут массивы. Много массивов. И ещё пара слов о директивах компилятору, иногда также называемых директивами препроцессора. С них и начнём. В этом разделе нас с вами ждут массивы. Много массивов. И ещё пара слов о директивах компилятору, иногда также называемых директивами препроцессора. С них и начнём.
\subsection{Директива \code{\#define}} \subsection{Директива \code{\#define}}
Помимо уже хорошо знакомой вам директивы \code{\#include}, частично описанной в разделе \hyperref[text:directive]{\ref{text:directive}}, естественно, существуют и другие. Некоторые из них ограничивают импорт описанных в заголовочном файле функций, некоторые <<\textbf{описывают}>> какие-то константы и даже действия. Вот, директиву \textbf{описать} мы и рассмотрим подробнее. Она не зря называется директивой препроцессора, поскольку даёт указание не процессору во время выполнения программы выделить память, присвоить значения, а непосредственно компилятору: заменить в тексте программы одни слова на другие. Таким образом можно задавать константы проекта, и даже делать сокращённые записи целых действий. Например, написав \code{\#define ARRAY\_LENGTH 50} мы предпишем компилятору, перед запуском трансляции нашего кода заменить все слова \code{ARRAY\_LENGTH} на цифру 50. Весьма удобно, но этим можно не ограничиваться, мы можем попросить компилятор заменить вызовы функций и операторы на короткие, удобные нам слова. Важно помнить, что директивы препроцессора работают с текстом программы, поэтому не осуществляют никаких дополнительных проверок. Это сложный и мощный инструмент, который чаще всего используется для решения нетривиальных задач, например, выбор кода, который попадёт в компиляцию в зависимости от операционной системы. Иногда в программах можно встретить описание недостающего но такого привычного булева типа при помощи директив препроцессора: Помимо уже хорошо знакомой вам директивы \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] \begin{lstlisting}[language=C,style=CCodeStyle]
#define bool int #define bool int
#define true 1 #define true 1
#define false 0 #define false 0
\end{lstlisting} \end{lstlisting}
Но нам пока что достаточно умения создать глобальную именованную константу. Код ниже демонстрирует, что директивы не обязательно группировать именно в начале файла, а можно использовать там, где это удобно и уместно, так мы можем объявить константу с длиной массива в начале файла, а можем прямо внутри функции \code{int main (int argc, char *argv[])}. Но нам пока что достаточно умения создать глобальную именованную константу. Код ниже демонстрирует, что директивы не обязательно группировать именно в начале файла, а можно использовать там, где это удобно и уместно, так мы можем объявить константу с длиной массива в начале файла, а можем прямо внутри функции \code{int main (int argc, char *argv[])}.
\begin{figure}[h!] \begin{figure}[h!]
\begin{lstlisting}[language=C,style=CCodeStyle] \begin{lstlisting}[language=C,style=CCodeStyle]
int main(int argc, char* argv[]) { int main(int argc, char* argv[]) {
@ -56,7 +57,6 @@
\item объявление, совмещённое с инициализацией \item объявление, совмещённое с инициализацией
\end{itemize} \end{itemize}
Для примера объявим массив, содержащий элементы типа \code{int}, дадим ему идентификатор или имя массива \code{arr} (сокращённо от англ array), укажем максимальное количество элементов которые может вместить в себя массив, например, пять. Для примера объявим массив, содержащий элементы типа \code{int}, дадим ему идентификатор или имя массива \code{arr} (сокращённо от англ array), укажем максимальное количество элементов которые может вместить в себя массив, например, пять.
\begin{figure}[h!] \begin{figure}[h!]
\begin{lstlisting}[language=C,style=CCodeStyle] \begin{lstlisting}[language=C,style=CCodeStyle]
int arr[5]; int arr[5];
@ -71,7 +71,6 @@
int arr[6] = {1, 1, 2, 3, 5, 8}; int arr[6] = {1, 1, 2, 3, 5, 8};
\end{lstlisting} \end{lstlisting}
При этом, если сразу заполняются все элементы, размерность можно не указывать. Итак, мы научились создавать и заполнять значениями массивы. Теперь общее правило объявления массивов в С: при объявлении массива нужно указать его имя, тип элементов, количество элементов, опционально - указать сами эти элементы. Количество элементов есть натуральное число, то есть целое положительное, ноль не может быть количеством элементов. Нельзя задавать переменное количество элементов массива. При этом, если сразу заполняются все элементы, размерность можно не указывать. Итак, мы научились создавать и заполнять значениями массивы. Теперь общее правило объявления массивов в С: при объявлении массива нужно указать его имя, тип элементов, количество элементов, опционально - указать сами эти элементы. Количество элементов есть натуральное число, то есть целое положительное, ноль не может быть количеством элементов. Нельзя задавать переменное количество элементов массива.
\begin{figure}[h!] \begin{figure}[h!]
\begin{lstlisting}[language=C,style=CCodeStyle] \begin{lstlisting}[language=C,style=CCodeStyle]
int nArr[100]; // An array for 100 int's; int nArr[100]; // An array for 100 int's;
@ -84,148 +83,166 @@
Так мы обязаны создавать массивы только с точно указанным числом элементов. Для языка С это позволено сделать объявлением константы времени исполнения \code{const int elements; int arr[elements]}, но, например, в С++ такая запись вызовет ошибку компиляции, поэтому там необходимо строго указывать размер числовым литералом или объявив его директивой \code{\#define}, что, фактически, одно и тоже. Так мы обязаны создавать массивы только с точно указанным числом элементов. Для языка С это позволено сделать объявлением константы времени исполнения \code{const int elements; int arr[elements]}, но, например, в С++ такая запись вызовет ошибку компиляции, поэтому там необходимо строго указывать размер числовым литералом или объявив его директивой \code{\#define}, что, фактически, одно и тоже.
\frm{В более поздних стандартах С++ появилось ключевое слово \code{constexpr}, позволяющее объявлять константы времени компиляции и отказаться от объявления размеров массива только литералом} \frm{В более поздних стандартах С++ появилось ключевое слово \code{constexpr}, позволяющее объявлять константы времени компиляции и отказаться от объявления размеров массива только литералом}
Теперь давайте научимся получать доступ к элементам массива. Нет ничего проще, тем более, что мы это уже делали объявляли массив и для примера его заполняли. Для доступа к конкретному элементу массива нужно указать имя массива и индекс элемента в квадратных скобках. Квадратные скобки - это тоже оператор языка, он называется оператором индексного доступа: Теперь давайте научимся получать доступ к элементам массива. Нет ничего проще, тем более, что мы это уже делали объявляли массив и для примера его заполняли. Для доступа к конкретному элементу массива нужно указать имя массива и индекс элемента в квадратных скобках. Квадратные скобки - это тоже оператор языка, он называется оператором индексного доступа:
\begin{figure}[h!] \begin{figure}[h!]
\begin{lstlisting}[language=C,style=CCodeStyle] \begin{lstlisting}[language=C,style=CCodeStyle]
int a = arr[0]; int a = arr[0];
printf("lets see whats in 0-th element: %d", a) printf("lets see whats in 0-th element: %d", a);
\end{lstlisting} \end{lstlisting}
\end{figure} \end{figure}
При помощи массивов решают множество задач, таких как поиск, сортировка, составление таблиц соответствия, создание частотных диаграмм. На основе массивов создают более сложные структуры данных. Для короткого минимального примера, давайте напишем программу, которая будет печатать наш массив в консоль. При помощи массивов решают множество задач, таких как поиск, сортировка, составление таблиц соответствия, создание частотных диаграмм. На основе массивов создают более сложные структуры данных. Для короткого минимального примера, давайте напишем программу, которая будет печатать наш массив в консоль.
% #include <stdio.h> \begin{figure}[h!]
\begin{lstlisting}[language=C,style=CCodeStyle]
#include <stdio.h>
% int main( int argc, char *argv[]) int main(int argc, char *argv[]) {
% { int i;
% int i; printf("Your array is: ");
% printf("Your array is: "); for (i = 0; i < 5; i++) {
% for (i = 0; i < 5; i++) printf("%d ", arr[i]);
% { }
% printf("%d ", arr[i]); // получаем значение i элемента массива и выводим в консоль. return 0;
% } }
% return 0;
% }
% Мы научились создавать, инициализировать массивы и обращаться к его элементам. Теперь решим задачу посложнее - напишем программу, которая проверит насколько статистически хорош описанный в стандартной библиотеке генератор псевдо-случайных чисел. Для такой статистической проверки нам понадобится сформировать так называемый частотный массив. Массив, в котором будет содержаться информация о том, сколько раз то или иное число появилось во множестве, полученном при помощи генератора псевдослучайных чисел.
\end{lstlisting}\end{figure}
Мы научились создавать, инициализировать массивы и обращаться к его элементам. Теперь решим задачу посложнее: напишем программу, которая проверит насколько статистически хорош описанный в стандартной библиотеке (языка С) генератор псевдо-случайных чисел (функция \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-й, и т.д. Данная программа наглядно демонстрирует не только работу с массивами, но и то, что генератор псевдослучайных чисел в языке С генерирует статистически верную последовательность случайных чисел.
\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 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);
\end{lstlisting}
\end{figure}
Как вы видите, некоторые подсчеты программа выполняет за нас - мы прибавляем к указателю единицу, двойку, тройку и т.д, а программа понимает, что надо взять не следующий по счёту байт, а следующий указатель. Так как в данном примере мы используем массив в котором хранятся значения типа \code{int}, а как вы помните \code{int} в подавляющем большинстве случаев - это четыре байта, то при увеличении указателя на единицу, мы обратимся к области памяти находящейся на четыре байта дальше идентификатора, при увеличении на двойку на восемь байт и так далее. Подсчитать среднее арифметическое не составит труда, этот алгоритм нам знаком со средней школы. Далее при помощи функции \code{printf();} выведем в консоль среднее арифметическое. Запустим, повводим цифры и убедимся что все работает.
Внимательный читатель мог заметить, что мы применяем операцию \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}
Так, полный листинг этого примера на стр. \hyperref[code:arrayaverage]{\pageref{code:arrayaverage}}.
\begin{figure}[h!]
\lstinputlisting[language=C,style=CCodeStyle]{../sources/arrayaverage.c}
\label{code:arrayaverage}
\end{figure}
% Данная программа наглядно демонстрирует не только работу с массивами, но и то, что генератор псевдослучайных чисел в языке С генерирует статистически верную последовательность случайных чисел. \subsection{Многомерные массивы}
Массив в языке С может иметь сколько угодно измерений. Все массивы, с которыми мы имели дело до этого момента - одномерные, их легко визуализировать в виде простого перечисления элементов, возможно, как строки или как таблицы, состоящей из одной строки. Самые распространённые многомерные массивы - это двумерные и трёхмерные, которые легко себе представить в виде таблицы или куба соответственно. Итак, массив это структура, содержащая элементы. Двумерный массив - это массив из массивов, содержащих элементы. Трёхмерный - это массив из массивов, содержащих массивы, которые содержат элементы. И так далее. В массиве могут находиться любые типы данных, мы, для удобства, будем рассматривать работу массивов с числами.
% #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)); // зададим начальное значение ГПСЧ
% int frequency[ARRAY_LENGTH] = {0}; // объявим и обнулим
% int a;
% int i;
% for (i = 0; i < NUMBERS_AMOUNT; i++) // заполним
% {
% a = rand() % ARRAY_LENGTH; // сгенерируем
% frequency[a]++; // для числа 0 увеличим значение в 0-й ячейке массива, для числа 1 - в 1-й, и т.д.
% }
% 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;
% }
% На этом занятии мы познакомились с понятием массива, научились создавать и заполнять массивы. Применили массив по назначению. До встречи на следующем занятии.
\section{Массивы 2}
% Здравствуйте, коллеги. На предыдущем занятии мы начали говорить о массивах, давайте продолжим. Это будет непросто, но мы поговорим о том, что из себя представляет идентификатор массива, чем чреват выход за пределы массива, затронем тему арифметики указателей и научимся передавать ссылки на массивы в функции.
% Как упоминалось ранее - массив - это ссылочный тип данных. То есть в идентификаторе хранится адрес, ссылка на первый байт первого элемента массива, дальнейший доступ к элементам осуществляется посредством смещения относительно этого байта. Таким образом запись вида array[0] говорит нам о том, что нужно взять адрес массива и сместить указатель на 0 элементов того типа, из которых состоит массив. Отсюда становится ясно, ПОЧЕМУ индексирование массивов начинается с нуля.
% int arr[ARRAY_LENGTH];
% arr[0] = 20;
% Давайте я попробую в комментариях к коду визуализировать положение вещей в массивах. Вот в квадратных скобках наш массив, квадратики это элементы нашего массива. Соответственно, если мы берем элемент с нулевым смещением, то есть по нулевому индексу, это будет вот этот вот индекс, квадратными скобочками я его нарисую. Все остальные индексы мы просто игнорируем. Далее, берем следующий элемент массива. Для этого возьмем смещение в единицу, допустим, это будет число 50. Соответственно в комментариях опять рисуем наш массив, и уже смещение будет на единицу. То есть мы игнорируем нулевой элемент и берем первый. Надеюсь, стало понятнее.
% int arr[ARRAY_LENGTH]; // [ [][][][][][][][][][] ]
% arr[0] = 20; // [ [] ]
% arr[1] = 50; // [ [] ]
% МОЖЕТ ПЕРЕДЕЛАТЬ НА СЛАЙДЫ?
% Относительно выхода за пределы массива надо сказать, что ни компилятор, ни тем более операционная система никаких проверок не делают, поэтому это полностью ложится на плечи программиста. Никаких сред виртуализации, никаких исключений, только случайные данные, которые могут попасться нашей программе. Надо сказать, что всё-таки бОльшая часть значений за пределами массива будет равна нулю, но всё равно лишний раз экспериментировать не стоит.
% arr[10] = 60; // [ ] []
% И ЭТО ТОЖЕ
% Как мы уже знаем в идентификаторе массива хранится ссылка на первый байт первого элемента массива, т.е. идентификатор является по сути - указателем. Но существует несколько отличий: указатель - это переменная, к ней применимы, например, операции инкремента и декремента, чего конечно нельзя делать с идентификатором массива без должной подготовки, о которой и поговорим. Обратившись к идентификатору массива мы можем получить доступ к элементам массива не только при помощи записи индекса в квадратных скобках, но и при помощи так называемой арифметики указателей. Мы знаем, что массив - это единая область памяти, и значения в нём располагаются подряд по очереди, значит, отсчитав от указателя на первый индекс нужное количество байт - мы получим указатель на второй индекс. Давайте для примера подсчитаем среднее арифметическое всех чисел в массиве, с использованием арифметики указателей. Заполним этот массив с клавиатуры.
% Создадим вспомогательную переменную float result, для хранения результата и в цикле будем запрашивать у пользователя цифры. Количество введенных цифр должно соответствовать количеству элементов массива, поэтому условием выхода из цикла будет равенство итератора и длины массива1, т.к. индексация массива начинается с 0, длина массива на 1 больше последнего индекса. Выведем в консоль надпись «введите значение», при помощи функции scanf считаем его и сразу положим в массив. Здесь мы к указателю на первый элемент массива прибавляем значение итератора и получаем индекс массива, в который будет положено значение. Выведем в консоль получившийся массив при помощи цикла for и привычной нам функции printf. Следом напишем ещё один цикл в котором подсчитаем среднее арифметическое. Для этого к результату будем прибавлять существующий результат и значение массива на которую указывает конструкция *(average + i).Как вы видите, некоторые подсчеты программа выполняет за нас - мы прибавляем к указателю единицу, двойку, тройку и т.д, а программа понимает, что надо взять не следующий по счёту байт, а следующий указатель. Т.к. в данном примере мы используем массив в котором хранятся значения типа int, а как вы помните int = 4 байта, то при увеличении указателя на 1 мы обратимся к области памяти находящейся на 4 байта дальше идентификатора, при увеличении на 2 на 8 байт и т.д. Подсчитать среднее арифметическое не составит труда.
% Далее при помощи функции printf выведем в консоль среднее арифметическое. Запустим, повводим цифры и убедимся что все работает.
% int i = 0;
% float result = 0;
% while (i < ARRAY_LENGTH){
% printf("Enter value %d:", i);
% scanf("%d", arr + i);
% i++;
% }
% 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);
% Как мы уже говорили, идентификатор массива - это не обычный указатель. Обычный указатель хранит в себе адрес какой-то другой переменной, и сам где-то хранится. Указатель на начало массива хранит в себе адрес массива, то есть адрес его нулевого элемента, и сам этот указатель находится в этом самом месте. На первый взгляд сложновато? Но пусть Вас это не сбивает с толку, на деле всё не так жутко. На деле это означает, что при передаче массива в функцию мы не должны использовать оператор взятия адреса, поскольку идентификатор массива сам по себе является указателем на собственное начало.
% В только что написанной нами программе оформим вывод массива на экран и поиск среднего арифметического в виде функции.
% Опишем функции printArray и average в которые передадим указатель на массив и его длину, т.к. в массиве не содержится сведений о его размере. Т.к. мы передаем в функцию указатель, то все действия которые описаны в этой функции будут происходить с массивом который мы создали в основной части программы через этот указатель, который мы передали, никакого копирования значений или чего то подобного. Для корректной работы наших функций объявим в них итератор и изменим названия переменных на название аргументов.
% 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;
% }
% Напишем вызов наших функций в основной части программы. Запустим и убедимся, что все также работает корректно. Вот, собственно, и вся магия передачи массивов в функции.
% printf(“Our array is: ”);
% printArray(arr, ARRAY_LENGTH);
% printf(“\n And the average is: ”);;
% printf(“%f \n”, average(arr, ARRAY_LENGTH));
% На этом уроке мы познакомились с арифметикой указателей и способом изменить массив в сторонней функции, это открыло нам массу возможностей для написания функций, реализующих те или иные алгоритмы работы с массивами. До встречи на следующем уроке
\section{Массивы 3}
% Коллеги, здравствуйте. Рад, вас приветствовать на десятом, юбилейном уроке курса основы языка С. На этом уроке нас ждёт продолжение разговора о массивах.
% Массив в языке С может иметь сколько угодно измерений. Самые распространённые это двумерные и трёхмерные, которые легко себе представить в виде таблицы или куба соответственно. Итак, массив это структура, содержащая элементы. Двумерный массив - это массив из массивов, содержащих элементы. Трёхмерный - это массив из массивов, содержащих массивы. И так далее.
% В массиве могут находиться любые типы данных, мы, для удобства, будем рассматривать работу массивов с числами.
% Давайте для удобства попробуем визуализировать двумерный массив. Создадим двумерный массив, например 5*5, и вот здесь в комментариях я попробую его нарисовать с помощью псевдографики. Массив 5*5 это 5 столбцов и 5 строчек. Соответственно, каждая строчка это будет у нас младший индекс, а каждый столбец старший индекс. % Давайте для удобства попробуем визуализировать двумерный массив. Создадим двумерный массив, например 5*5, и вот здесь в комментариях я попробую его нарисовать с помощью псевдографики. Массив 5*5 это 5 столбцов и 5 строчек. Соответственно, каждая строчка это будет у нас младший индекс, а каждый столбец старший индекс.
% Трехмерный массив… Боюсь, что псевдографики в комментариях для этого не хватит. Но он может быть 3*3*3 это всем известный кубик Рубика. У него есть 6 граней, в каждой из которых 3*3 квадратика. % Трехмерный массив… Боюсь, что псевдографики в комментариях для этого не хватит. Но он может быть 3*3*3 это всем известный кубик Рубика. У него есть 6 граней, в каждой из которых 3*3 квадратика.
% СЛАЙД О МАССИВЕ, ПОКАЗАТЬ ЧТО ЭТО МАССИВ МАССИВОВ % СЛАЙД О МАССИВЕ, ПОКАЗАТЬ ЧТО ЭТО МАССИВ МАССИВОВ

View File

@ -25,7 +25,7 @@
long double & Тип вещественного числа с плавающей запятой, ставящийся в соответствие формату повышенной точности с плавающей запятой & \%Lf \%LF \%Lg\newline\%LG \%Le \%LE \\ long double & Тип вещественного числа с плавающей запятой, ставящийся в соответствие формату повышенной точности с плавающей запятой & \%Lf \%LF \%Lg\newline\%LG \%Le \%LE \\
\hline \hline
\end{tabular} \end{tabular}
\caption{Основные типоы данных в языке С} \caption{Основные типы данных в языке С}
\label{tab:types} \label{tab:types}
\end{figure} \end{figure}
\newpage \newpage
@ -73,6 +73,8 @@ $
\end{verbatim} \end{verbatim}
\end{figure} \end{figure}
\label{text:pointers} \label{text:pointers}
Иногда бывает важным отметить, что рассмотрение языка С для решения задач на уровне персональных компьютеров может отличаться от рассмотрения языка С для решения прикладных задач аппаратуры (микроконтроллеров и микропроцессорных операционных систем). Так может оказаться важным, что тип данных \code{int} не всегда является тридцатидвухразрядным, а его размер аппаратно-зависимый. Для разрешения подобных неточностей в более новых стандартах языка вводятся и принимаются гарантированно фиксированные по размеру переменные
\paragraph{Тип данных - указатель.} Как было сказано - переменная это именованный контейнер. У каждого такого контейнера есть свой собственный адрес в оперативной памяти. Язык С позволяет узнать этот адрес и работать с ним. Оператор взятия адреса это знак амперсанд (\&), написанный перед именем переменной. То есть у любой переменной всегда есть значение и адрес где это значение хранится (немного подробнее на стр. \pageref{fig:dereference}). Для вывода в консоль адреса используется специальный заполнитель - \code{\%p}. \paragraph{Тип данных - указатель.} Как было сказано - переменная это именованный контейнер. У каждого такого контейнера есть свой собственный адрес в оперативной памяти. Язык С позволяет узнать этот адрес и работать с ним. Оператор взятия адреса это знак амперсанд (\&), написанный перед именем переменной. То есть у любой переменной всегда есть значение и адрес где это значение хранится (немного подробнее на стр. \pageref{fig:dereference}). Для вывода в консоль адреса используется специальный заполнитель - \code{\%p}.
\begin{figure}[h!] \begin{figure}[h!]

33
sources/arrayaverage.c Normal file
View File

@ -0,0 +1,33 @@
#include <stdio.h>
#include <stdlib.h>
void printArray(int* array, int length) {
int i;
for (i = 0; i < length; i++)
printf("%d ", array[i]);
printf("\n");
}
float average(int* array, int length) {
float result = 0;
int i;
for (i = 0; i < length; i++)
result += *(array + i);
return result / length;
}
int main(int argc, const char** argv) {
#define ARRAY_LENGTH 10
int i = 0;
float result = 0;
while (i < ARRAY_LENGTH) {
printf("Enter value %d:", i);
scanf("%d", arr[i]);
i++;
}
printf(Our array is: );
printArray(arr, ARRAY_LENGTH);
printf(And the average is: %f \n,
average(arr, ARRAY_LENGTH));
return 0;
}