Нередко возникает необходимость работы не с одиночными данными, а с наборами данных. И для этого в языке Си применяются массивы. Массив представляет набор однотипных значений. Объявление массива выглядит следующим образом:
тип_переменной название_массива [длина_массива]
После типа переменной идет название массива, а затем в квадратных скобках его размер. Например, определим массив из 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; }
Используя циклические конструкции, можно перебрать массив:
#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
Выше рассматривались массивы чисел, но с массивами остальных типов данных все будет аналогично. Но отдельно стоит остановиться на массивах символов. В различных языках программирования есть специальные типы данных для представления строк. В языке программирования Си для представления строк используются массивы символов, ведь по сути строка - это и есть набор символов. Например, определим строку:
#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