Нередко возникает необходимость работы не с одиночными данными, а с наборами данных. И для этого в языке Си применяются массивы. Массив представляет набор однотипных значений. Объявление массива выглядит следующим образом:
тип_переменной название_массива [длина_массива]
После типа переменной идет название массива, а затем в квадратных скобках его размер. Например, определим массив из 4 чисел:
int main(void)
{
int numbers[4];
return 0;
}
Используя номера элементов в массиве, которые называются индексами, мы можем обратиться к отдельным элементам. Например:
#include <stdio.h>
int main(void)
{
int numbers[4];
numbers[0] = 1;
numbers[1] = 2;
numbers[2] = 3;
numbers[3] = 4;
printf("numbers[0] = %d \n", numbers[0]); // 1 - первый элемент
printf("numbers[2] = %d \n", numbers[2]); // 3 - третий элемент
return 0;
}
Индексы указываются в квадратных скобках после названия массива и начинаются с нуля, поэтому для обращения к первому элементу необходимо использовать выражение numbers[0].
Также мы можем сразу объявить и инициализировать массив значениями. Для этого применяется инициализатор - набор значений в фигурных скобках:
int numbers[4] = { 1, 2, 3, 5 }; // инициализация массива
printf("numbers[2] = %d", numbers[2]); // 3
Внутри инициализатора через запятую перечисляются значения, которые последовательно присваиваются элементам массива. То есть в данном случае у нас будет следующее соответствие между значениями элементов и их индексами:
| Значение | 1 | 2 | 3 | 5 |
| Индекс | 0 | 1 | 2 | 3 |
При инициализации массива можно явно не указывать его длину, в этом случае длина массива будет вычисляться исходя из количества его элементов в инициализаторе:
int numbers[] = { 1, 2, 3, 5 };
При этом необязательно инициализировать массив значениями для всех его элементов:
int numbers[5] = { 10, 12}; // 10, 12, 0, 0, 0
В данном случае в рамках инициализации предоставляются значения для двух первых элементов, остальные элементы по умолчанию получают значение 0.
Также Си позволяет частично инициализировать элементы массива не по порядку:
int numbers[5] = { [1]=11, [3] = 13 };
В данном случае инициализируются только два элемента - с индексами 1 и 3. Остальные получают значение по умолчанию - 0. То есть в итоге подобный массив будет анологичен следующему:
int numbers[5] = { 0, 11, 0, 13, 0 };
Не всегда в программе может быть известен размер массива. В этом случае можно использовать оператор sizeof, который возвращает размер массива в байтах в виде значения типа size_t:
#include <stdio.h>
int main(void)
{
int numbers[] = { 5, 6, 7};
size_t size = sizeof(numbers);
printf("numbers size: %zu \n", size); // numbers size: 12
return 0;
}
В этом примере оператор sizeof() для массива { 5, 6, 7} возвращает 12 байт (так как массив содержит 3 значения типа int, которое обычно занимает 4
байта). Тип результата оператора sizeof - size_t фактически является псевдонимом для типа unsigned long long, то есть
64-разрядное положительное число. Для его вывода на консоль применяется спецификатор %zu.
Используя размер типа, мы можем получить количество элементов в массиве:
#include <stdio.h>
int main(void)
{
int numbers[] = { 5, 6, 7};
size_t size = sizeof(numbers);
size_t count = sizeof(numbers) / sizeof(int);
printf("numbers size: %zu \n", size); // numbers size: 12
printf("numbers count: %zu \n", count); // numbers count: 3
return 0;
}
Также можно получить количество элементов в массиве, разделив его размер на размер первого элемента:
#include <stdio.h>
int main(void)
{
int numbers[] = { 5, 6, 7};
size_t size = sizeof(numbers);
size_t count = sizeof(numbers) / sizeof(numbers[0]);
printf("numbers size: %zu \n", size); // numbers size: 12
printf("numbers count: %zu \n", count); // numbers count: 3
return 0;
}
Также можно отметить, что элементы массива могут динамически вычисляться. Например:
int sizes[] = {sizeof(int), sizeof(long), sizeof(long long)}; // numbers = {4, 4 (Windows) или 8 (Linux/MacOS), 8}
sizes[1] = sizeof(char); // sizes[1] = 1
В данном случае каждый элемент массива вычисляется динамически с помощью выше рассмотренной функции sizeof(), которая возвращает размер.
Используя циклические конструкции, можно перебрать массив:
#include <stdio.h>
int main(void)
{
int numbers[] = { 10, 12, 13, 54, 43 };
size_t count = sizeof(numbers) / sizeof(numbers[0]);
for(size_t i =0; i < count; i++)
{
printf("numbers[%zu] = %d \n", i, numbers[i]);
}
return 0;
}
Стоит отметить, что в качестве индекса используется значение типа size_t. В реальности часто встречается и мы могли бы использовать тип int или
unsigned int:
#include <stdio.h>
int main(void)
{
int numbers[] = { 10, 12, 13, 54, 43 };
size_t count = sizeof(numbers) / sizeof(numbers[0]);
// int в качестве индекса
for(int i =0; i < count; i++)
{
printf("numbers[%d] = %d \n", i, numbers[i]);
}
return 0;
}
В целом мы получим тот же результат. Но в общем рекомендуемым способом все таки является использование size_t, поскольку этот тип допускает количество,
которое может не вписаться в допустимый диапазон чисел типов int или unsigned int.
Размер массива можно установить динамически с помощью переменной/константы:
#include <stdio.h>
int main(void)
{
int maxSize = 3;
int array[maxSize];
array[0] = 1;
array[1] = 2;
array[2] = 3;
for (int i = 0; i < maxSize; i++)
{
printf("%d", array[i]);
}
return 0;
}
Стоит отметить, что при динамической установке нельзя при определении инициализировать массив:
int maxSize = 3;
int array[maxSize] = {1, 2, 3}; // ! Ошибка, так нельзя
При необходимости после инициализации мы можем многократно изменять значения элементов массива:
#include <stdio.h>
int main(void)
{
int numbers[3] = {11, 12, 13};
numbers[1] = 22; // изменяем второй элемент
printf("numbers[1] = %d", numbers[1]); // numbers[1] = 22
return 0;
}
Однако иногда, наоборот, не требуется или даже нежелательно изменять элементы массива. В этом случае мы можем определить массив как константный:
#include <stdio.h>
int main(void)
{
const int numbers[3] = {11, 12, 13};
// numbers[1] = 22; // Нельзя изменить - массив константный
printf("numbers[1] = %d", numbers[1]); // numbers[1] = 22
return 0;
}
При попытке изменить элемент константного массива мы уже на этапе компиляции столкнемся с ошибкой.
Массивы могут быть многомерными. Элементы таких массивов сами в свою очередь являются массивами, в которых также элементы могут быть массивами. В большинстве случаев многмерные массивы представляют двухмерные массивы, которые можно представить в виде таблицы. Например, определим двухмерный массив чисел:
int numbers[3][2] = { {1, 2}, {4, 5}, {7, 8} };
Здесь массив numbers имеет три элемента (3 строки), но каждый из этих элементов сам представляет массив из двух элементов (2 столбцов). Такой массив еще можно представить в виде таблицы:
| 1 | 2 |
| 4 | 5 |
| 7 | 8 |
И чтобы обратиться к элементам вложенного массива, потребуется два индекса:
int numbers[3][2] = { {1, 2}, {4, 5}, {7, 8} };
printf("numbers[1][0] = %d \n", numbers[1][0]); // 4
другой пример, двухмерный массив с двумя строками и тремя столбцами:
int numbers[2][3] = { {1, 2, 4}, {5, 7, 8} };
Такой массив графически можно представить следующим образом:
| 1 | 2 | 4 |
| 5 | 7 | 8 |
Для перебора двухмерного массива применются вложенные циклы:
#include <stdio.h>
int main(void)
{
int numbers[3][2] = { {1, 2}, {4, 5}, {7, 8} };
// проходим по 3 строкам таблицы
for(int i =0; i < 3; i++)
{
// проходим по 2 столбцам каждой строки
for(int j =0; j<2; j++)
{
printf("numbers[%d][%d] = %d \n", i, j, numbers[i][j]);
}
}
return 0;
}
Как и в одномерных массивах, мы можем применить оператор sizeof для поиска длины массива и даже его подмассивов:
#include <stdio.h>
int main(void)
{
int numbers[3][2] = { {1, 2}, {4, 5}, {7, 8} };
size_t rows_count = sizeof(numbers) / sizeof(numbers[0]); // 3
size_t columns_count = sizeof(numbers[0]) / sizeof(numbers[0][0]); // 2
printf("rows count = %zu \n", rows_count);
printf("columns count = %zu \n", columns_count);
// проходим по 3 строкам таблицы
for(size_t i =0; i < rows_count; i++)
{
// проходим по 2 столбцам каждой строки
for(size_t j =0; j<columns_count; j++)
{
printf("numbers[%zu][%zu] = %d \n", i, j, numbers[i][j]);
}
}
return 0;
}
Консольный вывод:
rows count = 3 columns count = 2 numbers[0][0] = 1 numbers[0][1] = 2 numbers[1][0] = 4 numbers[1][1] = 5 numbers[2][0] = 7 numbers[2][1] = 8
Стоит отметить, что двухмерные и прочие многомерные массивы - это фактически абстракция, которая в реальности не существует. В реальности все массивы являются одномерными и представляют сплошной блок памяти. Возможно, гораздо удобнее работать, скажем, с табличными данными как с двухмерных массивом. Однако мы могли бы инициализировать многомерные массивы как одномерные:
int numbers[3][2] = { 1, 2, 4, 5, 7, 8 };
Выше рассматривались массивы чисел, но с массивами остальных типов данных все будет аналогично. Но отдельно стоит остановиться на массивах символов. В различных языках программирования есть специальные типы данных для представления строк. В языке программирования Си для представления строк используются массивы символов, ведь по сути строка - это и есть набор символов. Например, определим строку:
#include <stdio.h>
int main(void)
{
char message[] = "Hello";
printf("message: %s \n", message); // message: Hello
return 0;
}
Строки определяются в двойных кавычках. И если нам в программе нужны строки, то как раз можно использовать массивы символов.
Но стоит отметить, что кроме самих символов, которые заключены двойные кавычки, каждая строка в качестве завершающего символа содержит символ \0или нулевой символ (нулевой байт). Он же самый первый символ из таблицы ASCII. В Си нулевой байт служит признаком окончания строки. Поэтому в строке "Hello" на самом деле будет не 5 символов, а 6.
| H | e | l | l | o | \0 |
К примеру, переберем все символы строки и выведем их десятичный код ASCII:
char message[] = "Hello";
size_t length = sizeof(message)/sizeof(char); // 6 символов
for(size_t i=0; i<length; i++)
{
printf("%d ", message[i]);
}
На консоли при запуске программы мы сможем увидеть в конце нулевой символ:
72 101 108 108 111 0
Если бы мы определяли массив message не как строку, а именно как массив символов, то последним элементом должен был бы идти нулевой символ:
char message[] = {'H', 'e', 'l', 'l', 'o', '\0'};
Рассмотрим работу с массивами на примере умножения матриц:
#include <stdio.h>
int main(void)
{
const int r1 = 3, c1r2=2, c2=1;
int matrix1[3][2] = {{1, 2},{3, 4},{5, 6}};
int matrix2[2][1] = {{10},{20}};
int matmult[r1][c2];
// инициализируем результирующую матрицу
for(int i=0;i<r1;i++)
{
for(int j=0;j<c2;j++)
{
matmult[i][j]=0;
}
}
for(int i=0;i<r1;i++)
{
for(int j=0;j<c2;j++)
{
for(int k=0;k<c1r2;k++)
{
matmult[i][j] = matmult[i][j] + matrix1[i][k] * matrix2[k][j];
}
}
}
printf("Result \n");
for(int i=0;i<r1;i++)
{
for(int j=0;j<c2;j++)
{
printf("%d ",matmult[i][j]);
}
printf("\n");
}
}
Здесь у нас определены две матрицы. Матрица matrix1[3][2] имеет три строки и два столбца:
| 1 | 2 |
| 3 | 4 |
| 5 | 6 |
Вторая матрица фактически состоит из одно столбца:
| 10 |
| 20 |
Для хранения размера столбцов и строк определены переменные
const int r1 = 3, // число строк в 1-й матрице
c1r2=2, // число столбцов в 1-й и число строк во 2-й матрице
c2=1; // число столбцов во 2-й матрице
Также определяем результирующую матрицу - результат произведения - matmul:
int matmult[r1][c2];
И инициализируем ее нулями.
При произведении матриц мы получаем матрицу, где количество строк равно количеству строк первой матрицы, а количество столбцов - количеству столбцов второй матрицы. А элемент результирующей матрицы на i-й строке j-м столбце равен сумме произведений элементов на i-й строке первой матрицы на соответствующие элементы j-го столбца второй матрицы.
c2,1 = a2,1 * b1,1 + a2,2 * b2,1
Соответственно при вычислении произведения в цикле по i проходим по всем строкам первой матрицы:
for(int i=0;i<r1;i++)
Далее в цикле по j проходим по всем столбцам второй матрицы:
for(int j=0;j<c2;j++)
В цикле по k умножаем значения из k-столбца первой матрицы на значения k-строки второй матрицы и прибавляем к результату:
for(int k=0;k<c1r2;k++)
{
matmult[i][j] = matmult[i][j] + matrix1[i][k] * matrix2[k][j];
}
В результате мы получим матрицу из трех строк и одного столбца:
Result 50 110 170