Введение в массивы

Практический курс по C/C++: https://stepik.org/course/193691

На этом занятии речь пойдет о массивах. Что это такое и зачем они нужны? Давайте представим, что нам в программе нужно хранить значения функции синуса sin(x) в отдельных дискретных отсчетах x:

f(x) = sin(x),    x = 0; 0,1; …, 2π

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

Причем, тип данных у всех значений единый. Например, все числа можно определить как вещественные типа double, или типа int и так далее. Смешанных типов здесь нет. Для определенности положим, что все значения имеют тип double и занимают по size = 8 байт в памяти. Соответственно, чтобы получить первый элемент этой последовательности, нужно взять первые 8 байт, для второго – следующие 8 байт и так далее. Математически операцию вычисления первого байта произвольного элемента можно записать в виде:

addres_k = f + k ∙ size,   k = 0, 1, 2, …, n-1

Здесь f – номер первого байта первого элемента в памяти устройства; size – размер одного элемента в памяти (в нашем примере size = 8). Значения k в такой структуре данных получили название индексы. То есть, если всего имеется n элементов, то индекс первого элемента всегда равен 0, а индекс последнего равен n-1.

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

<тип элементов массива> <имя массива>[<число элементов массива>];

Например, в нашем случае с функцией синус массив можно объявить так:

double f[30];

В итоге получаем массив, состоящий из элементов типа double, с именем f и общим числом элементов, равным 30. Имя массива мы, конечно же, придумываем сами, так же как и имена переменных. В результате такого объявления в памяти устройства автоматически выделяется непрерывная область памяти для хранения 30 чисел типа double общего размера:

30 ∙ 8 = 240 байт.

А теперь, внимание! Строго говоря, в языке Си нет структуры данных под названием массив! То, что здесь понимается под массивом – это просто фрагмент области памяти, в которую можно записывать произвольные данные, в нашем случае через переменную f. То есть, здесь нет программной оболочки над этими данными, которая бы контролировала корректность индексов, или позволяла бы присваивать один массив другому и так далее. Все что здесь мы имеем – это набор элементов в непрерывной области памяти. Больше ничего! Вся ответственность за правильное использование этого фрагмента с данными лежит на плечах программиста. Сделано это было не случайно. Вспомним, что язык Си был создан как заменитель языка Ассемблер, так, чтобы текст программы можно было эффективно переводить в машинные коды без ущерба в производительности. Поэтому никаких лишних надстроек, посредников между данными в памяти и их представлением на уровне языка Си здесь нет. С одной стороны, это ускоряет работу программ, а с другой – возлагает на программиста всю полноту ответственности за использование ресурсов вычислительной техники.

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

Способы объявлений массивов

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

#include <stdio.h>
 
#define SIZE_BUFFER          1024
 
int main(void)
{
         // Корректные объявления
         double f[30];
         char buffer[SIZE_BUFFER];
         int marks[13];
         short ar[8 * 5];
         char bytes[sizeof(double)];
 
         // Некорректные объявления
         int n = 5;
 
         float func[21.5];   // вещественное количество элементов
         int array[n];       // нельзя было до стандарта C99
         int null_ar[0];     // размер должен быть больше нуля
 
         return 0;
}

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

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

Запись и чтение значений в массивы

Итак, в самом простом варианте мы научились объявлять массивы. Спрашивается, как теперь в него можно занести какие-либо значения и прочитать их оттуда? Давайте для определенности представим, что в программе объявлен целочисленный массив из 13 элементов для хранения оценок учащегося:

#include <stdio.h>
#define TOTAL_MARKS                13
 
int main(void)
{
         int marks[TOTAL_MARKS];
         return 0;
}

Чтобы занести в массив marks какое-либо целочисленное значение, необходимо прописать имя массива, поставить квадратные скобки, в них указать индекс элемента, в который заносится число и с помощью операции присваивания занести нужное значение. Например:

marks[2] = 4;

Здесь мы в 3-й элемент записали значение 4. Обратите внимание, индекс 2 означает 3-й элемент массива, а не второй, так как первый элемент всегда имеет индекс 0, например:

marks[0] = 2 * 3;

Теперь, мы в первый элемент массива занесли значение 6 = 2 * 3. И так можно делать со всеми тринадцатью элементами, то есть, индексы в нашем примере с массивом marks должны меняться в диапазоне от 0 до 12 включительно. Последнее значение:

marks[12] = 7 - 2;

И вот здесь начинающие программисты часто делают ошибку. Для записи данных в последний элемент указывают индекс на единицу больше:

marks[13] = 1;

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

[0; SIZE_ARRAY-1]

где SIZE_ARRAY – общее число элементов в массиве. Казалось бы, простое правило. Но не представляете, как бывает непросто его соблюсти. И все из-за того, что массивы в языке Си – это не структура данных, а просто непрерывная область памяти и корректное ее использование целиком и полностью лежит на плечах программиста.

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

int x = marks[2];

Здесь читается значение 3-го элемента массива с индексом 2 и копируется в целочисленную переменную x. Выведем это значение на экран:

printf("x = %d\n", x);

После запуска программы увидим строчку:

x = 4

То есть, мы прочитали значение 4, которое ранее записывали в 3-й элемент. И так можно читать из любого элемента массива. Здесь также нужно контролировать корректность индексов и не допускать ситуаций вроде следующей:

x = marks[13];

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

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

#include <stdio.h>
 
#define TOTAL_MARKS                13
 
int main(void)
{
         int marks[TOTAL_MARKS];
         marks[2] = 4;
         marks[0] = 2;
         marks[12] = 5;
//       marks[13] = 1;               // никогда так не делайте!!!
 
         for(int i = 0; i < TOTAL_MARKS; ++i)
                   printf("%d ", marks[i]);
 
         return 0;
}

После запуска у меня на экране отобразилась следующая строчка:

2 6488012 4 1424847605 -2 6487816 2001366829 4200976 6487936 4201088 4200976 4199120 5

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

Практический курс по C/C++: https://stepik.org/course/193691

Видео по теме