Вычисление размера массива. Инициализация массивов

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

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

Операция sizeof с массивами

В примере программы из прошлого занятия для определения размера массива используется константа TOTAL_MARKS, определенная директивой define:

#include <stdio.h>
 
#define TOTAL_MARKS                13
 
int main(void)
{
         int marks[TOTAL_MARKS];
 
         marks[2] = 4;
         marks[0] = 2 * 3;
         marks[12] = 7 - 2;
//       marks[13] = 1;
 
         for(int i = 0; i < TOTAL_MARKS; ++i)
                   printf("%d ", marks[i]);
 
         return 0;
}

Однако не всегда удается так просто определить размер массива и, соответственно, перебрать его элементы. В общем случае язык Си предоставляет возможность вычисления числа элементов массива с помощью уже знакомой вам операции sizeof. Делается это следующим образом. Как вы помните, sizeof возвращает размер переменной или типа данных в байтах. Но, применительно к массиву, она возвращает размер области памяти (также в байтах), которая под него выделена. Например:

size_t bytes_marks = sizeof(marks);
printf("%zu\n", bytes_marks);

Получим значение 52 в переменной bytes_marks. Это, как раз, величина:

13 ∙ 4 = 52 байта

Обратите внимание на тип переменной size_t. В данном случае – это переопределение целочисленного типа unsigned long long. По сути, имя size_t введено для универсальной записи программы при использовании функций или операций подобной sizeof. В разных реализациях языках и на разных платформах имя size_t может быть связано с разными целочисленными типами. Но всегда является целочисленной и беззнаковой. Далее, мы выводим значение переменной bytes_marks, используя спецификатор %zu, специально введенный для типа size_t.

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

size_t size_marks = sizeof(marks) / sizeof(marks[0]);
printf("%zu\n", size_marks);

Увидим значение 13, как раз столько элементов в массиве marks.

Конечно, вместо sizeof(marks[0]) в данном конкретном случае можно было бы прописать тип int:

size_t size_marks = sizeof(marks) / sizeof(int);

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

size_t size_marks = sizeof(marks) / sizeof(*marks);

Забегая вперед, скажу, что *marks – это, по сути, то же самое, что и marks[0].

Инициализация массивов

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

int marks[TOTAL_MARKS] = {1, 2, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0};

То есть, после объявления массива прописывается знак равно и далее в фигурных скобках через запятую перечисляются значения, которые будут записаны в соответствующие элементы массива. В данном примере в первые четыре элемента (по порядку) будут занесены значения 1, 2, 3 и 4 соответственно. А в остальные – числа 0.

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

int marks[TOTAL_MARKS];
marks = {1, 2, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0};

то получим синтаксическую ошибку. Операция присваивания не поддерживает такое выражение. А вот при инициализации оно вполне допустимо. Поэтому нужно четко различать инициализацию и присваивание.

Итак, мы теперь знаем, как можно инициализировать элементы массива начальными значениями. Но, что если нам нужно записать данные только в первые несколько элементов, а остальные пусть принимают любые значения. Сделать это очень просто. Указываем только нужные данные, остальные опускаем:

int marks[TOTAL_MARKS] = {1, 2, 3};

Тогда в первые три элемента будут занесены числа 1, 2 и 3, а вот остальные становятся равными 0. Да, так работает инициализатор. Если мы указываем хотя бы одно начальное значение, то все остальные элементы массива становятся равными нулю. Но, обратите внимание, мы в инициализаторе можем указывать меньшее количество данных, но большее (больше числа элементов массива) задавать нельзя. Если в начальном примере добавить хотя бы один ноль:

int marks[TOTAL_MARKS] = {1, 2, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};

то получим ошибку при компиляции программы.

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

int coords[] = {10, -2, 30};

Массив coords будет состоять из трех элементов, т.к. в инициализаторе указано три значения.

Наконец, в стандарте C99 появился еще один способ инициализации с указанием индекса инициализируемого элемента. Например:

short digits[10] = {-1, [2] = 5, 18, [9] = -1};
for(int i = 0 ;i < sizeof(digits) / sizeof(digits[0]); ++i)
         printf("%d ", digits[i]);

В результате увидим строку из значений:

-1 0 5 18 0 0 0 0 0 -1

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

Все эти варианты инициализации вполне можно использовать в программах в зависимости от их удобства.

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

#include <stdio.h>
 
#define TOTAL_MARKS                13
 
int month[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
 
int main(void)
{
         int marks[TOTAL_MARKS] = {1, 2, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0};
 
         for(int i = 0; i < TOTAL_MARKS; ++i)
                   printf("%d ", marks[i]);
 
         return 0;
}

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

#include <stdio.h>
#include <stdlib.h>
 
#define TOTAL_MARKS                13
 
int month[] = {31, 28, 31 * 2, 30, 31, 30, 31, 31, 30, 31, 30, 31};
 
int main(void)
{
         int val = -5;
         int marks[TOTAL_MARKS] = {1, 2, 3, 4, val, abs(val), 0, 0, 0, 0, 0, 0, 0};
 
         for(int i = 0; i < TOTAL_MARKS; ++i)
                   printf("%d ", marks[i]);
 
         return 0;
}

Но, как я уже отмечал, инициализацию массивов внутри функций следует избегать, кроме, может быть, функции main(), т.к. она, как правило, вызывается только один раз в процессе работы программы.

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

Видео по теме