#3 — Массивы данных в Си
Массивы данных позволяют хранить большой объем информации в одном месте. В языке Си они представляют возможности для работы со строками. За урок мы научимся работать с массивами и познакомимся с созданием строк.
Видеоурок
Массивы позволяют хранить большой объем информации в одном месте. В языке Си можно найти несколько основных типов массивов.
Одномерный массив
Чтобы создать массив необходимо указать тип данных, прописать название и поставить квадратные скобки. Это очень схоже с созданием обычных переменных, но здесь после названию идут ещё квадратные скобки.
В массивах отсчет начинается с 0, поэтому первый элемент по индексу будет равен 0, второй — 1 и так далее.
Примеры создания массива:
char stroka[2]; // Создание пустого массива int[] numbers; // Будет считаться ошибкой int nums[4]; nums[0] = 1; // Добавление элементов в массив из 4 элементов int nums2[] = < 1, 2, 3, 5 >; // Присвоение всех значений сразу
Работать с элементами массива можно точно как с переменными. Мы можем их выводить или же устанавливать для них новые значения.
Многомерный массив
Многомерный массив — это массив, в котором каждый элемент является другим массивом. На практике очень редко используются массивы с более чем третим уровнем вложенности. То есть массивы, в которых все элементы являются другими массивами и в котором все элементы также другие массивы встречаются очень и очень редко на практике.
Мы не будем изучать подобные массивы, так как принцип их построения точно такой же как при работе с двумерными массивами.
Для создания двумерных массивов необходимо использовать двойные квадратные скобки после названия. Выглядит такой массив как матрица, а записывается следующим образом:
int x[2][3] = < < 0, 34, 2 >, < 3, 4, 5 >>; printf("%d", x[0][1]); // Выведет 34 // Можно их сразу не присваивать char symbols[5][2]; symbols [0][1] = 'A';
Мы видим, что элементы первого массива являются другими массивами. Чтобы выбрать какой-либо объект используйте ту же структуру что и для одномерных массивов, вот только теперь указывайте индекс как первого массива, так и второго:
x[0][1] = 1; // Вместо 34 теперь будет 1
Весь код будет доступен после подписки на проект!
Работа с массивами в языке Си
На этой странице относительно подробно рассказывается о статических и динамических массивах. Краткое изложение основных моментов и описание методов поиска ошибок доступны при нажатии на кнопки выше. Двумерные массивы описаны на этой странице.
Массив – это линейно упорядоченная совокупность однотипных элементов. Массив определяется типом элементов (int, double, . ) и длиной. Доступ к элементам осуществляется по индексу – порядковому номеру элемента массива. Логически первый элемент массива имеет индекс ноль. В языке Си существуют статические массивы, число элементов в которых должно быть известно в момент компиляции программы, и динамические массивы, размер которых задается в процессе выполнения программы, то есть может зависеть от входных данных. Эти два типа отличаются только методом создания массива, поэтому сначала рассмотрим статические массивы.
Статические массивы
Способы объявления статических массивов
Объявление статического массива отличается от объявления обычной переменной только указанием количества элементов массива. Например, следующее объявление означает, что именем points называется массив из 100 действительных чисел.
double points[100];
В некотором смысле можно считать, что такое объявление переменной points создает 100 переменных, которые называются points[0], points[1], . points[99]. Плюс к этому, «имена» этих переменных можно вычислять: points[1], points[0+1] или points[k-1] имеют одно значение (если k=2).
В реальных программах следует избегать явного использования числовых констант в объявлениях массива (и других частях программы). Если нам нужно объявить два массива, которые теоретически могут иметь разный размер, например,
double points[100]; int students[100];
то в дальнейшем, если возникнет необходимость увеличить один из массивов, будет сложно отличить одну константу от другой. Особенно это верно при обработке элементов массива (см. ниже). Правильным считается использование директив препроцессора для присвоения константам «говорящих» имен. Например:
#define NPOINTS 100 #define NSTUDENTS 100 . double points[NPOINTS]; int students[NSTUDENTS];
Объявление массива может быть совмещено с присвоением значений его элементам. Например,
double points[] = ;
создает массив из четырех действительных чисел с указанными значениями. Заметим, что в данном случае число элементов массива в квадратных скобках не указывается. Компилятор самостоятельно вычисляет длину по списку начальных значений. В программе можно вычислить длину такого массива, разделив его размер на размер одного элемента (пример ниже).
Работа с элементами массива
Для доступа к элементу массива достаточно знать его имя и порядковый номер элемента. В языке Си элементы массива индексируются начиная с нуля, то есть в массиве из двух элементов корректными являются индексы 0 и 1. Если массив имеет имя array, то его k -й элемент записывается как array[k] . Это выражение может использоваться как для получения значения элемента массива, так и для его изменения, если оно стоит в левой части оператора присваивания. Рассмотрим для примера следующую программу.
#define NPOINTS 100 int main() < double points[NPOINTS]; int k; points[0] = 0.1; for(k=1; k < NPOINTS; k++) < points[k] = 0.1 + points[k-1]; >return 0; >
Эта программа заполняет массив действительных чисел значениями 0, 0.1, 0.2 и так далее. Отметим, что макропеременная NPOINTS используется как при объявлении массива, так и в качестве верхней границы цикла по всем его элементам. Если размер массива нужно будет изменить, то достаточно исправить одну строчку в программе (#define).
Пример работы с массивом, который задан с начальными значениями:
int main() < double points[] = ; int k; int npoints = sizeof(points)/sizeof(points[0]); for(k=0; k < npoints; k++) < printf("points[%d] = %lf\n", k, points[k]); >return 0; >
Типичная ошибка при работе с массивами состоит в указании неправильного индекса. Если в приведенной выше программе переменная цикла k будет пробегать значения от 0 до npoints включительно, то поведение программы, вообще говоря, может быть любым. Наиболее вероятным поведением является вывод на экран какого-то значения, но может возникнуть и критическая ошибка, которая приведет к аварийной остановке программы.
Представление массива в памяти и адресная арифметика
В памяти ЭВМ элементы массива записаны последовательно без пропусков. Имя массива является указателем на его начальный элемент (с индексом 0). Поскольку в массиве все элементы имеют одинаковый тип, то зная адрес начала массива (A), размер одного элемента (size) и индекс k можно вычислить адрес размещения k-ого элемента: A + k*size. Если требуется получить значение k-ого элемента массива, то достаточно выполнить одно умножение (k*size), одно сложение (A + k*size) и загрузить значение из памяти по только что вычисленному адресу. Таким образом, обращение к элементу массива очень эффективно и сложность этой операции не зависит от величины индекса k: получение (или изменение) значения нулевого элемента столь же эффективно, как и миллионного.
Хорошо, адрес начала массива мы знаем — это его имя, индекс нам известен, но как узнать size (размер одного элемента)? Чуть ниже мы узнаем как это сделать, но для работы с указателями на элементы массива это не требуется! В языке Си к указателям можно прибавлять целые числа. Например, если есть указатель double *a; , то значением выражения a+9 будет адрес десятого (еще раз вспомним, что массивы индексируются с нуля!) элемента массива, который начинается с адреса a . Компилятор сам понимает, что a является указателем на double и прибавляет нужное значение.
Обратной стороной последовательно хранения элементов в памяти является сложность вставки нового значения с сохранением порядка следования элементов. Например, если в массив нужно добавить новое значение по индексу 0, то чтобы «освободить» место все элементы массива придется сдвинуть на одну позицию. Ясно, что сложность этой операции зависит от длины массива. Чем больше длина, тем дольше выполняется это действие.
Передача массива в функцию
Функция может получать на вход массив. В действительности в функцию передается адрес начала массива и его длина. Прототип функции может быть оформлен либо так:
int print_array(double x[], int len);
int print_array(double *x, int len);
Эти варианты являются эквивалентными. Некоторые программисты предпочитают первый (квадратные скобки показывают, что формальный параметр функции является массивом), другие — второй (имя массива является указателем на нулевой элемент). Естественно, что функция может иметь и другие параметры, в том числе, другие массивы. Это только пример.
Рассмотрим возможную реализацию функции распечатывания массива.
#include int print_array(double x[], int len) < int k; for(k = 0; k < len; k++) < printf("x[%d] = %lf\n", k, x[k]); >return 0; >
При вызове функции в качестве аргумента нужно передавать имя массива и его длину.
int main() < double points[] = ; int npoints = sizeof(points)/sizeof(points[0]); print_array(points, npoints); return 0; >
Внимание! Если функция print_array изменит значение элемента массива x (например, в цикле будет написано x[k]=0; ), то изменятся значения и в массиве points функции main. Элементы массива при вызове функций не копируются! Функция получает на вход адрес памяти, где записаны элементы массива. Эта память «общая» для вызывающей и вызываемой функции.
Динамические массивы: malloc и free
Статические массивы имеют одно существенное ограничение: размер массива должен быть известен в момент компиляции программы. В большинстве задач размер данных становится известным только в момент выполнения программы. Например, вы написали программу для обработки списка друзей или подписчиков в социальной сети. У одного пользователя друзей мало, а у другого — очень много. Какое значение выбрать для длины массива друзей? 200? 1000? Миллион? Если константа будет очень большой, чтобы «заведомо» (посмотрите как росло число пользователей Интернет) устраивать всех пользователей, то для подавляющего числа пользователей это приведет к излишним затратам памяти. Захотите ли Вы поставить на свой телефон программу, которая при запуске займет всю его память с сообщением: «А вдруг у тебя миллион друзей. Нет? Всего 12?! Неплохо, прямо как у Oушена! А y Трампа миллион. «? [Друзей не должно и не может быть так много, но это к делу не относится.] Чтобы избежать таких ситуаций нужно уметь выделять минимально необходимое количество памяти.
- выделение памяти под массив;
- освобождение памяти, когда она больше не требуется.
Стандартная библиотека языка Си содержит несколько функций для работы с динамической памятью. Нам понадобятся две: malloc (memory allocation — выделение памяти) и free (освобождение). Для использования этих удивительных функций нужно в программе подключить заголовочный файл . Пример программы приведен ниже. Сначала посмотрим, что делают эти функции.
malloc: динамическое выделение памяти
Прототип: void *malloc(size_t size); Параметры: size — беззнаковое целое число, размер запрашиваемой памяти в байтах. Возвращает: Адрес начала выделенной памяти или NULL, если не удалось выделить память. Функция malloc возвращает указатель типа void * — это «абстрактный» указатель на память, который может быть приведен к указателю на любой тип. Функция malloc не может сразу возвращать указатель нужного типа, так как она используется для создания разных массивов, а в прототипе нужно указать конкретный тип возвращаемого значения.
Для выделения памяти под массив из n элементов типа T, где в T могут быть стандартные типы int , double и т.п., необходимо знать размер значения T в байтах. Для определения этой величины в языке Си есть специальный оператор sizeof , который в момент компиляции программы вычисляет нужное значение. Например, массив из n целых чисел будет занимать n*sizeof(int) байт памяти.
Таким образом, для создания динамического массива некоторого типа, например с массива целых чисел, нужно использовать команду вида:
int length; int *points; // . получили значение length (длина массива) points = (int *)malloc(length * sizeof(int));
Если нужен другой тип данных, допустим double , то int заменяется на нужное имя ( double ) в трех местах (кроме первой строки, так как длина массива всегда является целым числом).
free: освобождение памяти
Функция free позволяет освободить область памяти, которая ранее была выделена программе при вызове malloc .
Прототип void free(void *ptr); Параметры: ptr — указатель, который был получен в результат вызова malloc.
В качестве аргумента функции free может использоваться только тот адрес, который был получен в результате вызова malloc. Нельзя создать статический массив и «освободить» его функцией free. Адрес может быть освобожден только один раз. Если два раза подряд вызвать функцию free с одним и тем же аргументом, то это приведет к аварийному завершению программы.
Пример программы с динамическим массивом
В качестве иллюстрации описанных методов рассмотрим программу, которая динамически выделяет память под массив и считывает его.
#include #include int main() < int npoints; double *points; int k; scanf("%d", &npoints); /* npoints получает значение в момент выполнения программы */ points = (double *)malloc(npoints*sizeof(double)); /* Выдели память для хранения npoints элементов, каждый размера sizeof(double) */ if(points == NULL) < printf("Произошла ошибка. Запросили слишком много памяти??\n"); return -1; >/* Считываем данные с использованием адресной арифметики */ k = 0; while(k < npoints && scanf("%lf", points+k) == 1) < k++; >/* Работаем с points как с обычным массивом */ /* Например, вызываем функцию print_array(points, npoints) */ free(points); /* Освободили память */ return 0; >
Функции, которые возвращают массив
Иногда бывает удобно сделать функцию, которая возвращает динамически созданный массив. Примером может служить функция считывания массива из файла. Такая функция может получать на вход файловую переменную ( FILE * ) и должна вернуть в вызывающую функцию массив значений. Например, массив действительнах чисел. Попробуем ее реализовать.
Во-первых, нужно понять, какой прототип должна иметь такая функция. Она должна вернуть два значения: адрес выделенной памяти и длину массива. Как мы уже знаем, несколько значений можно вернуть используя указатели. Длина массива имеет тип int . Значит параметр функции будет иметь тип int * (адрес, по которому нужно записать значение). Массив — это адрес нулевого элемента, то есть double * . Значит параметр будет иметь тип double ** — «указатель на указатель». Мы должны передать адрес (одна звездочка), по которому нужно записать результат вызова malloc, который имеет тип double * . В результате получаем следующий прототип:
int read_array(FILE *input, double **array, int *length);
Собственно возвращаемое значение функции ( int ) может быть кодом ошибки. Если функция вернет 0, то это означает успешное выполнение. Любое ненулевое значение означает ошибку.
Теперь можно рассмотреть структуру тела функции (для наглядности в приведенном ниже коде отсутствуют проверки успешности считывания и корректности данных).
int read_array(FILE *input, double **array, int *length) < double *arr; int arr_length, k; /* Считываем массив: сначала длину, потом элементы */ fscanf("%d", &arr_length); arr = (double *)malloc(arr_length * sizeof(double)); for(k = 0; k < arr_length; k++) fscanf("%lf", arr + k); /* Копируем результат по заданным адресам */ *length = arr_length; *array = arr; return 0; >
Как задать массив в си
Нередко возникает необходимость работы не с одиночными данными, а с наборами данных. И для этого в языке Си применяются массивы. Массив представляет набор однотипных значений. Объявление массива выглядит следующим образом:
тип_переменной название_массива [длина_массива]
После типа переменной идет название массива, а затем в квадратных скобках его размер. Например, определим массив из 4 чисел:
int main(void)
Используя номера элементов массиве, которые называются индексами, мы можем обратиться к отдельным элементам. Например:
#include 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 >;
Используя циклические конструкции, можно перебрать массив:
#include int main(void) < int numbers[] = < 10, 12, 13, 54, 43 >; for(int i =0; i < 5; i++) < printf("numbers[%d] = %d \n", i, numbers[i]); >return 0; >
При этом необязательно инициализировать массив значениями для всех его элементов:
int numbers[5] = < 10, 12>; // 10, 12, 0, 0, 0
В данном случае в рамках инициализации предоставляются значения для двух первых элементов, остальные элементы по умолчанию получают значение 0.
Многомерные массивы
Массивы могут быть многомерными. Элементы таких массивов сами в свою очередь являются массивами, в которых также элементы могут быть массивами. В большинстве случаев многмерные массивы представляют двухмерные массивы, которые можно представить в виде таблицы. Например, определим двухмерный массив чисел:
int numbers[3][2] = < , , >;
Здесь массив numbers имеет три элемента (3 строки), но каждый из этих элементов сам представляет массив из двух элементов (2 столбцов). Такой массив еще можно представить в виде таблицы:
1 | 2 |
4 | 5 |
7 | 8 |
И чтобы обратиться к элементам вложенного массива, потребуется два индекса:
int numbers[3][2] = < , , >; printf("numbers[1][0] = %d \n", numbers[1][0]); // 4
другой пример, двухмерный массив с двумя строками и тремя столбцами:
int numbers[2][3] = < , >;
Такой массив графически можно представить следующим образом:
1 | 2 | 4 |
5 | 7 | 8 |
Для перебора двухмерного массива применются вложенные циклы:
#include int main(void) < int numbers[3][2] = < , , >; // проходим по 3 строкам таблицы for(int i =0; i < 3; i++) < // проходим по 2 столбцам каждой строки for(int j =0; j> return 0; >
Строки
Выше рассматривались массивы чисел, но с массивами остальных типов данных все будет аналогично. Но отдельно стоит остановиться на массивах символов. В различных языках программирования есть специальные типы данных для представления строк. В языке программирования Си для представления строк используются массивы символов, ведь по сути строка — это и есть набор символов. Например, определим строку:
#include 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»; for(int i=0; i
На консоли при запуске программы мы сможем увидеть в конце нулевой символ:
72 101 108 108 111 0
Если бы мы определяли массив message не как строку, а именно как массив символов, то последним элементом должен был бы идти нулевой символ:
char message[] = ;
Размер и количество элементов массива
Не всегда в программе может быть известен размер массива. В этом случае можно использовать оператор sizeof , который возвращает размер массива в байтах.
#include int main(void) < int numbers[] = < 5, 6, 7>; int size = sizeof(numbers); printf("numbers size: %d \n", size); // numbers size: 12 return 0; >
В этом примере оператор sizeof() для массива < 5, 6, 7>возвращает 12 байт (так как массив содержит 3 значения типа int, которое обычно занимает 4 байта).
Используя размер типа, мы можем получить количество элементов в массиве:
#include int main(void) < int numbers[] = < 5, 6, 7>; int size = sizeof(numbers); int count = sizeof(numbers) / sizeof(int); printf("numbers size: %d \n", size); // numbers size: 12 printf("numbers count: %d \n", count); // numbers count: 3 return 0; >
Также можно получить количество элементов в массиве, разделив его размер на размер первого элемента:
#include int main(void) < int numbers[] = < 5, 6, 7>; int size = sizeof(numbers); int count = sizeof(numbers) / sizeof(numbers[0]); printf("numbers size: %d \n", size); // numbers size: 12 printf("numbers count: %d \n", count); // numbers count: 3 return 0; >
Динамическая установка размера массива
Размер массива можно установить динамически с помощью переменной/константы:
#include 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] = ; // ! Ошибка, так нельзя
Задачи с массивы
Рассмотрим работу с массивами на примере умножения матриц:
#include int main(void) < const int r1 = 3, c1r2=2, c2=1; int matrix1[3][2] = ,,>; int matrix2[2][1] = ,>; int matmult[r1][c2]; // инициализируем результирующую матрицу for(int i=0;i> for(int i=0;i > > printf("Result \n"); for(int i=0;i 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Далее в цикле по j проходим по всем столбцам второй матрицы:
for(int j=0;jВ цикле по k умножаем значения из k-столбца первой матрицы на значения k-строки второй матрицы и прибавляем к результату:
for(int k=0;k
В результате мы получим матрицу из трех строк и одного столбца:
Массивы в C++
Продолжаем серию «C++, копаем вглубь». Цель этой серии — рассказать максимально подробно о разных особенностях языка, возможно довольно специальных. Это четвертая статья из серии, первые три, посвященные перегрузке в C++, находятся здесь, здесь и здесь.
Эта статья посвящена массивам. Массивы можно отнести к наиболее древним слоям C++, они пришли из первых версий C. Тем не менее, массивы вошли в объектно-ориентированную систему типов C++, хотя и с определенными оговорками. Программисту важно знать об этих особенностях, чтобы избежать потенциальных ошибок. В статье также рассмотрено другое наследие C – тривиальные типы и неинициализированные переменные. Часть нововведений C++11, С++14, С++17 затрагивают работу с массивами, все эти новые возможности также подробно описаны. Итак, попробуем рассказать о массивах все.
Оглавление
Оглавление
1. Общие положения
Массив является простейшим агрегатным типом. Он моделирует набор однотипных элементов, расположенных подряд в непрерывном отрезке памяти. Массивы в той или иной форме поддерживаются практически всеми языками программирования и неудивительно, что они появились в первых версиях C и затем стали частью C++.
1.1. Объявление массивов
Если T некоторый тип, N константа или выражение, вычисляемое во время компиляции, то инструкция
T a[N];
объявляет переменную a типа «массив из N элементов типа T » (array of N elements of the type T ). Тип N должен иметь неявное приведение к типу std::size_t , а его значение, называемое размером массива, должно быть больше нуля. Массив располагается в непрерывном отрезке памяти, под каждый элемент массива выделяется sizeof(T) байт, соответственно размер памяти, необходимой для размещения всего массива, равен N*sizeof(T) байт. Эта величина ограничена сверху платформой и компилятором. Тип массива обозначается как T[N] , то есть он включает тип элементов и размер массива. Таким образом, массивы, имеющие одинаковый тип элементов, но разный размер, будут иметь разный тип.
Такие массивы еще называют встроенными массивами (regular arrays), чтобы подчеркнуть отличие от других вариантов массивов, термин «массив» используется в программировании и в том числе в C++ достаточно широко.
Вот примеры правильных объявлений массивов:const int N = 8; constexpr int Square(int n) < return n * n; >int a1[1]; int a2[N]; int a3['Q']; int a4[Square(2)];
А вот примеры некорректных объявлений массивов:
int n; int b1[0]; // нулевой размер int b2[n]; // размер нельзя определить во время компиляции int b3["Q"]; // размер нельзя привести к size_t
Доступ к элементам массива осуществляется через индексатор, значения индекса от 0 до N-1 . Вот пример:
int a[4]; a[0] = 42; int t = a[3];
Выход за границы массива не контролируется, ошибка может привести к неопределенному поведению.
В одной инструкции можно объявить несколько массивов, но размер должен быть указан для каждого.
int a[4], b[8];
Для типов массивов можно вводить псевдонимы. Можно использовать традиционный вариант с ключевым словом typedef :
typedef int I4[4];
или более современный (C++11) с ключевым словом using :
using I4 = int[4];
После этого массивы объявляются как простые переменные:
I4 a, b;
Это будет то же самое, что
int a[4], b[4];
1.2. Операторы и стандартные функции для работы с массивами
Для работы с массивами можно использовать оператор sizeof и несколько стандартных функций и макросов.
Оператор sizeof возвращает полный размер массива в байтах, то есть размер элемента умноженный на размер массива.
Макрос _countof() (в MSVS заголовочный файл ) возвращает размер массива, то есть количество элементов. В С++17 появился стандартный шаблон функции std::size() , которая делает то же самое (а еще имеет перегруженную версию, которая определяет размер стандартного контейнера).
int a[4]; std::cout
16 4В C++11 в стандартной библиотеке появились свободные (не члены) шаблоны функций std::begin() и std::end() . Вызванная для массива std::begin() возвращает указатель на первый элемент массива, std::end() на past-the-last элемент. (Есть также константные версии: std::cbegin() , std::cend() .) Это позволяет использовать массивы в диапазонном for .
int a[4]< 4, 3, 2, 1 >; for (auto t : a)А также в стандартных алгоритмах:
std::sort(std::begin(a), std::end(a));
1.3. Размещение в памяти
Если массив объявлен статически, то есть в глобальной области видимости, в области видимости пространства имен или в качестве статического члена класса, то он размещается в статической памяти. Массивам, объявленным локально, память выделяется на стеке. (Естественно, надо учитывать ограниченный размер стека при выборе размера локальных массивов.) Нестатические члены класса размещаются в границах экземпляра класса. Динамические массивы (см. раздел 6) размещаются в динамической памяти.
1.4. Ограничения на типы элементов массивов
Нельзя объявить массив, элементы которого имеют тип void .
Нельзя объявить массив ссылок.
int u, v; int &rr[2] = < u, v >; // ошибка
Вместо этого можно использовать массив константных указателей.
int * const rr[2] = < &u, &v >;
(Синтаксис инициализации массивов будет обсуждаться в разделе 3.2.)
В C++11 появился шаблон std::reference_wrapper<> . Он эмулирует интерфейс ссылки, но экземпляры конкретизации можно хранить в контейнерах и встроенных массивах. Но все же эмуляция интерфейса ссылки не совсем полная, иногда приходится использовать функцию-член get() . Вот пример.
int u = 42, v = 5; std::reference_wrapper rr[2] = < u, v >; std::cout
Нельзя объявить массив функций.
int ff[2](double); // ошибка
Вместо этого можно использовать массив указателей на функцию.
int (*ff[2])(double);
Шаблон std::reference_wrapper<> можно конкретизировать типом функции, но преимуществ перед указателем практически нет — функцию и так можно вызвать через указатель без разыменования, а инициализировать указатель именем функции без оператора & . Есть еще вариант эмулирования массива функций — это использование шаблона std::function<> , но этот шаблон является темой отдельного разговора.
Массив нельзя объявить с помощью ключевого слова auto .
auto x[2] = // ошибка
Квалификатор const не применим к типу массива, а только к типам его элементов.
using I4 = int[4]; const I4 ci; // то же, что и const int ci[4];
2. Сведение и копирование массивов
В данном разделе рассматриваются особенности массивов, которые выделяют их из общей системы типов C++.
2.1. Сведение
Как было сказано выше, размер массива является составной частью типа массива, но в определенных ситуациях она теряется и это делает тип массива в некотором смысле «неполноценным». Эта потеря называется сведение (decay, array-to-pointer decay). (Для перевода термина «decay» еще используется слово «низведение», также можно встретить «разложение».) Суть сведения заключается в том, что почти в любом контексте массив преобразуется к указателю на первый элемент и информация о размере теряется. Исключениями являются оператор sizeof , оператор & (взятия адреса) и инициализация ссылки на массив. Оператор sizeof рассматривался в разделе 1.2, указатели и ссылки на массивы будут подробно рассмотрены в разделе 4. Объявление с помощью ключевого слова decltype также правильно определяет тип массива, без сведения.
Конечно, тесную связь массивов и указателей отрицать нельзя. Вот стандартный (в стиле C) способ обработать все элементы массива:
const int N = 100; int a[N]; for (int *d = a, *end = d + N; dНо все же сведение можно отнести к сишным архаизмам и с ним надо быть внимательным и аккуратным, иначе можно столкнуться с не самыми приятными неожиданностями.
Вот как сведение влияет на объявления функций. Функции
void Foo(int a[4]); void Foo(int a[]); void Foo(int *a);
не являются перегруженными функциями — это одно и то же. Размер надо передавать дополнительным параметром или использовать специальное соглашение для определения размера (например, завершающий ноль для строк).
При внешнем связывании массива также происходит сведение.
// file 1 int A[4]; // file 2 extern int A[];
Для размера также надо использовать дополнительную переменную или использовать специальное соглашение для определения размера.
При объявлении переменной с помощью ключевого слова auto также происходит сведение.
int a[4]; auto da = a; // тип da выводится как int*
При конкретизации шаблона функции
template void Foo(T t);
тип параметра шаблонной функции также будет выведен как указатель, если аргумент является массивом.
Сведение вызывает дополнительные проблемы при использовании наследования. (В C ведь нет наследования.) Рассмотрим пример.
class B* . */> ; class D : public B* . */> ; void Foo(B[], int size); // обработка массива элементов типа B
Следующий код компилируется без ошибок и предупреждений.
D d[4]; Foo(d, _countof(d));
2.2. Копирование
Наряду со сведением (и тесно связанная с ним) есть еще одна особенность типа массива, которая делает его в некотором смысле «неполноценным». Массивы не поддерживают привычный синтаксис инициализации и присваивания, основанный на семантике копирования:
using I4 = int[4]; I4 a; I4 b = a; // ошибка I4 b; // ошибка I4 b(a); // ошибка I4 b2; b2 = a; // ошибка
Также функция не может возвращать массив.
I4 Foo(); // ошибка
Но если массив является членом класса/структуры/объединения, то копирующий конструктор и соответствующий оператор присваивания, генерируемые компилятором, выполняют поэлементное копирование такого массива.
struct X < int A[4]; >; X x = < >; X x2 = x; // копирование X x3; // копирование X x4; x4 = x; // присваивание X Foo() // возвращаемое значение функции < return < >; >
Еще одна ситуация, когда происходит копирование массива — это захват массива по значению в лямбда-выражении.
intptr_t x[4]; auto f = [x]() < return sizeof(x) / sizeof(x[0]); >; std::cout
Но если используется инициализирующий захват (C++14), то происходит сведение.
intptr_t u[4]; auto g = [x = u]() < return sizeof(x) / sizeof(x[0]); >; std::cout
3. Инициализация массивов
Для описания правил инициализации массивов необходимо кратко рассказать о тривиальных типах.
3.1. Тривиальные типы и неинициализированные переменные
Конструкторы и деструкторы можно назвать ключевыми элементами объектной модели С++. При создании объекта обязательно вызывается конструктор, а при удалении — деструктор. Но проблемы совместимости с С вынудили сделать некоторое исключение, и это исключение называется тривиальные типы. Они введены для моделирования сишных типов и сишного жизненного цикла переменных, без обязательного вызова конструктора и деструктора. Сишный код, если он компилируется и выполняется в С++, должен работать так же, как в С. К тривиальным типам относятся числовые типы, указатели, перечисления, а также классы, структуры, объединения и массивы, состоящие из тривиальных типов. Классы и структуры должны удовлетворять некоторым дополнительным условиям: отсутствие пользовательского конструктора, деструктора, копирования, присваивания, виртуальных функций.
Переменная тривиального типа будет неинициализированной, если не использовать какой-нибудь вариант явной инициализации. Для тривиального класса компилятор может сгенерировать конструктор по умолчанию и деструктор. Конструктор по умолчанию обнуляет объект, деструктор ничего не делает. Но этот конструктор будет сгенерирован и использован только, если использовать какой-нибудь вариант явной инициализации, иначе переменная останется неинициализированной.
Неинициализированная переменная устроена следующим образом: если она объявлена в области видимости пространства имен (глобально), будет иметь все биты нулевыми, если локально, или создана динамически, то получит случайный набор битов. Понятно, что использование такой переменной может привести к непредсказуемому поведению программы. Массивы достаточно часто имеют тривиальный тип и поэтому эта проблема для них весьма актуальна.
Неинициализированные константы тривиального типа выявляет компилятор, иногда он выявляет и другие неинициализированные переменные, но с этой задачей лучше справляются статические анализаторы кода.
В стандартной библиотеке С++11 есть шаблоны, называемые свойствами типов (заголовочный файл ). Один из них позволяет определить, является ли тип тривиальным. Выражение std::is_trivial::value имеет значение true , если T тривиальный тип и false в противном случае.
3.2. Синтаксис инициализации массивов
3.2.1. Общие положения
Если не использовать явную инициализацию, то для массивов нетривиального типа гарантируется вызов конструктора по умолчанию для каждого элемента. Естественно, что в этом случае такой конструктор должен быть, иначе возникает ошибка. Но для массивов тривиального типа или, если конструктор по умолчанию отсутствует или не устраивает, необходимо использовать явную инициализацию.
Со времен C массивы можно было инициализировать с помощью синтаксиса агрегатной инициализации:
int a[4] = < 1, 2, 3, 4 >;
В С++11 появилась универсальная инициализация (uniform initialization) и теперь можно инициализировать так:
int a[4]< 1, 2, 3, 4 >;
Для универсальной инициализации также можно использовать =, и различать эти два типа инициализации не всегда просто, а, скорее всего, не очень нужно.
Размер массива можно не указывать, тогда он определится по числу инициализаторов.
int a[] < 1, 2, 3, 4 >;
Если размер массива указан, то число инициализаторов не должно быть больше размера массива. Если размер массива больше числа инициализаторов, то для оставшихся элементов гарантируется вызов конструктора по умолчанию (который, естественно, должен быть), в том числе и для тривиальных типов. Таким образам, указав пустой список инициализации, мы гарантируем вызов конструктора по умолчанию для всех элементов массива тривиального типа.
int a[4]<>;
Массивы констант тривиального типа требуют обязательного списка инициализации.
const int a[4] = < 3, 2, 1 >;
Число инициализаторов может быть меньше размера массива, в этом случае оставшиеся элементы инициализируются конструктором по умолчанию.
Символьные массивы можно инициализировать строковым литералом.
const char str[] = "meow"; const wchar_t wstr[] = L"meow";
Размер такого массива будет на единицу больше числа символов строки, нужно хранить завершающий нулевой символ.
3.2.2. Инициализация членов класса
В С++11 появилась возможность инициализировать массивы, являющиеся нестатическими членами класса. Это можно сделать двумя способами: непосредственно при объявлении или в списке инициализации членов при определении конструктора.
class X < int a[4]< 1, 2, 3, 4 >; int b[2]; // . public: X(int u, int v) : b < u, v ><> // . >;
Правда в этом случае надо всегда явно задавать размер массива, неявное определение размера через список инициализации не разрешается.
Статические массивы, как и ранее, можно инициализировать только при определении, размер массива может быть определен через список инициализации.
class X < static int A[]; // . >; int X::A[] = < 1, 2, 3, 4 >;
В C++17 появилась возможность объявлять статические члены (включая массивы) как inline . Таки члены можно инициализировать при объявлении, определение не обязательно.
class X < inline static int A[]< 1, 2, 3, 4 >; // . >;
3.2.3. Требования к инициализаторам
Выражения, стоящие в списке инициализации, вычисляются непосредственно перед инициализацией, они не обязаны быть известными на стадии компиляции (конечно, за исключением массивов, объявленных как constexpr ). Требования к элементам списка инициализации такие же как и к аргументу функции, имеющей параметр того же типа, что и элемент массива — должно существовать неявное преобразование от типа элемента списка инициализации к типу элемента массива. Пусть у нас есть объявление массива:
Наличие нужного преобразования эквивалентно корректности инструкции
T t = x1;
Элемент списка инициализации может быть сам списком инициализации. В этом случае корректность этой инструкции также гарантирует корректную инициализацию элемента массива.
class Int < int m_Value; public: Int(int v) : m_Value(v) <>// . >; // . int x, y; // . Int rr[] = < x, y >;
Если мы объявим конструктор Int как explicit , то последнее объявление станет некорректным. В этом случае придется писать
Int rr[] = < Int(x), Int(y) >;
Этот пример также демонстрирует как с помощью списка инициализации мы можем создать массив для типа у которого нет конструктора по умолчанию. Но в этом случае число инициализаторов должно совпадать с размером массива.
4. Указатели и ссылки на массивы
4.1. Указатели на массивы
Пусть у нас объявлен массив
T a[N];
Указатель на этот массив объявляется и инициализируется следующим образом:
T(*pa)[N] = &a;
Для получения указателя используется традиционный оператор & . Тип указателя на массива обозначается как T(*)[N] .
Обратим внимание на использование скобок, без них мы получим объявление массива из N элементов типа указатель на T .
Указатель на массив — это не указатель на первый элемент (хотя побитово они, конечно, совпадают), здесь нет никакого сведения. Это полноценный тип, который «знает» размер массива. Поэтому при инициализации размеры должны совпадать.
int a[4]; int(*pa)[4] = &a; // OK int(*p2)[2] = &a; // ошибка, размеры не совпадают
При инкременте указатель на массив увеличивается на размер всего массива, а не на размер элемента.
Для доступа к элементу массива через указатель надо использовать оператор * и индексатор.
(*pa)[3] = 42;
При использовании псевдонимов можно получить более привычный синтаксис объявления указателя на массив.
using I4 = int[4]; I4 a< 1, 2, 3, 4 >; I4 *pa = &a;
Также можно использовать auto , компилятор правильно выводит тип переменной как указатель на массив исходя из типа инициализатора.
int a[4]; auto pa = &a; // тип pa выводится как int(*)[4]
Понимание указателей на массивы необходимо для правильной работы с многомерными массивами, которые подробно будут рассмотрены далее.
4.2. Ссылки на массивы
Пусть у нас объявлен массив
T a[N];
Ссылка на этот массив объявляется и инициализируется следующим образом:
Как и для любой ссылки, инициализация переменной типа ссылка на массив является обязательной. Тип ссылки на массива обозначается как T(&)[N] .
Также ссылку на массив можно инициализировать разыменованным указателем на массив.
T(*pa)[N] = &a; T(&ra)[N] = *pa;
Как и указатель, ссылка «знает» размер массива. Поэтому при инициализации размеры должны совпадать.
int a[4]; int(&ra)[4] = a; // OK int(&r2)[2] = a; // ошибка, размеры не совпадают
Доступ к элементу массива через ссылку осуществляется так же, как и через идентификатор массива.
ra[3] = 0;
Ссылки на массивы как раз и являются теми средствами, с помощью которых можно обойти сведение.
void Foo(T(&a)[N]);
ожидает аргументы типа T[N] , указатели для нее не подходят.
При использовании псевдонимов можно получить более привычный синтаксис объявления ссылки на массив.
using I4 = int[4]; I4 a< 1, 2, 3, 4 >; I4 &ra = a;
Также можно использовать auto , компилятор выводит тип переменной как ссылка на массив.
int a[4]; auto &ra = a; // тип ra выводится как int(&)[4]
Обратите внимание на наличие & после auto , без него произошло бы сведение, и тип ra вывелся бы как int* .
При конкретизации шаблона функции
template void Foo(T& t);
тип параметра шаблонной функции также будет выведен как ссылка на массив, если аргумент является массивом.
Особенно удобно использовать шаблоны с выводом типа и размера массива.
template void Foo(T(&a)[N]);
При конкретизации такого шаблона компилятор выводит тип элементов T и размер массива N (который гарантировано больше нуля). В качестве аргументов можно использовать только массивы, указатели будут отвергнуты. Именно этот прием используется при реализации макроса _countof() и шаблона функции std::size() , а так же шаблонов функций std::begin() и std::end() , которые обеспечивают для массивов реализацию диапазонного for и делают более комфортной работу с алгоритмами. В разделе 5 приведен пример реализации такого шаблона.
5. Многомерные массивы
C++ не поддерживает настоящие многомерные массивы, то есть выражение a[N, M] некорректно, но многомерность моделируется в виде «массива массивов», то есть можно использовать выражение a[N][M] .
Если T некоторый тип, N и M выражения, допустимые для определения размера массива, то инструкция
T a[N][M];
объявляет a как массив массивов, массив из N элементов, каждый из которых является массивом из M элементов типа T . Такой массив будем называть двумерным массивом. Выражение a[i][j] , где i от 0 до N-1 , j от 0 до M-1 , дает доступ к элементам этого массива. Первый индекс выбирает массив из массива массивов, второй выбирает элемент в этом массиве. Значение N можно назвать внешним размером двумерного массива, M внутренним. Тип многомерного массива обозначается как T[N][M] .
Выражение a[i] является массивом из M элементов типа T . Соответственно к нему может быть применено сведение, у него можно взять адрес или использовать для инициализации ссылки.
T *dai = a[i]; T(*pai)[M] = &a[i]; T(&rai)[M] = a[i];
Сведение преобразует массив к указателю на элемент. Для двумерного массива этот элемент сам является массивом, а значит двумерный массив сводится к указателю на массив.
T a[N][M]; T(*da)[M] = a;
Таким образом, при передаче двумерного массива в функцию следующие варианты объявления соответствующего параметра эквивалентны:
void Foo(T a[N][M]); void Foo(T a[][M]); void Foo(T(*a)[M]);
Это означает, что внешний размер двумерного массива теряется и его надо передавать отдельным параметром.
При использовании псевдонимов можно получить более лаконичный синтаксис объявления двумерных массивов.
using I4 = int[4]; I4 b[2];
Это то же самое, что
int b[2][4];
Двумерные массивы инициализируются следующим образом:
int b[2][4] = , >;
Если нужно гарантировать только инициализацию по умолчанию, то можно использовать пустой список инициализации <> . Определения размера по списку инициализации возможно только по внешнему размеру.
int b[][4] = , >; // ОК int b[][] = , >; // ошибка
Можно получить указатель на двумерный массив:
T a[N][M]; T(*pa)[N][M] = &a;
Также можно получить ссылку. Вот пример использования ссылки на двумерный массив.
template void Print2dArray(T(&a)[N][M]) < for (int i = 0; i < N; ++i) < for (int j = 0; j < M; ++j) < std::cout std::cout > // . int b[2][4] = , >; Print2dArray(b);
1 2 3 4 5 6 7 8Двумерный массив хорошо согласуется с математическими матрицами. В объявлении
T mtx[N][M];
N можно интерпретировать как число строк матрицы, M как число столбцов, тогда mtx[i][j] это элемент матрицы находящийся на пересечении i -й строки и j -го столбца, а mtx[i] это массив размера M , который представляет i -ю строку матрицы. Соответственно, такая матрица располагается в памяти по строкам. Правда в математике принято нумеровать строки и столбцы с единицы, а не с нуля.
6. Динамические массивы
В C++ отсутствует тип «динамический массив». Имеются только операторы для создания и удаления динамического массива, доступ к нему осуществляется через указатели на начало массива (своего рода полное сведение). Размер такого массива надо хранить отдельно. Динамические массивы желательно инкапсулировать в C++ классы.
6.1. Создание и удаление динамического массива
Если T некоторый тип, n переменная, значение которой может определяются в процессе выполнения программы, то инструкция
T *pa = new T[n];
создает массив в динамической памяти. Тип переменной n должен приводиться к std::size_t , значение может быть нулем. Размер памяти, необходимой для размещения массива, то есть n*sizeof(T) , ограничен сверху платформой и компилятором. Переменная pa указывает на первый элемент массива.
Если тип T тривиальный, то элементы будут иметь случайное значение, в противном случае для инициализации элементов будет использован конструктор по умолчанию.
В C++11 появилась возможность использовать список инициализации.
int *pa = new int[n];
Если число инициализаторов больше размера массива, то лишние не используются (компилятор может выдать ошибку, если значение n известно на стадии компиляции). Если размер массива больше числа инициализаторов, то для оставшихся элементов гарантируется вызов конструктора по умолчанию, в том числе и для тривиальных типов. Таким образам, указав пустой список инициализации, мы гарантируем вызов конструктора по умолчанию для всех элементов массива тривиального типа.
Оператор new[] сначала выделяет память для всего массива. Если выделение прошло успешно, то, если T нетривиальный тип или есть список инициализации, вызывается конструктор для каждого элемента массива начиная с нулевого. Если какой-нибудь конструктор выбрасывает исключение, то для всех созданных элементов массива вызывается деструктор в порядке, обратном вызову конструктора, затем выделенная память освобождается. Стандартные функции выделения памяти при невозможности удовлетворить запрос выбрасывают исключение типа std::bad_alloc .
Динамический массив удаляется оператором delete[] , который применяется к указателю, возвращаемому оператором new[] .
delete[] pa;
При этом, если при создании массива использовался конструктор, то для всех элементов массива вызывается деструктор в порядке, обратном вызову конструктора (деструктор не должен выбрасывать исключений), затем выделенная память освобождается.
В остальных отношениях указатель pa , возвращаемый оператором new[] , является просто указателем на начало массива, через него нельзя (во всяком случае «законно») получить размер массива, этот размер надо хранить отдельно. Соответственно с динамическим массивом нельзя использовать диапазонный for . Указатели в C/C++ поддерживают индексатор (встроенный оператор [] ), поэтому доступ к элементам динамического массива выглядит так же, как и к обычному массиву, контроля за корректностью индекса нет.
6.2. Динамические массивы и интеллектуальные указатели
Стандартный интеллектуальный указатель std::unique_ptr<> можно использовать для управления жизненным циклом динамического массива (см. [Josuttis]). Он имеет частичную специализацию для массивов (см. раздел 7), которая перегружает оператор [] вместо операторов -> и * , а также использует оператор delete[] в качестве удалителя по умолчанию. Вот пример:
int n = 100; std::unique_ptraptr(new int[n]); for (int i = 0; i Эта поддержка не является полноценной: не хранится информация о размере массива, поэтому нет возможности контролировать корректностью индекса, не поддерживается интерфейс стандартных контейнеров и диапазонный for .
В C++14 появилась возможность создать динамический массив и инициализировать им экземпляр std::unique_ptr<> с помощью std::make_unique<> :
auto aptr = std::make_unique
(n); При этом гарантируется инициализация элементов массива по умолчанию, в том числе и для тривиальных типов.
Интеллектуальный указатель std::shared_ptr<> стал поддерживать такую специализацию только в C++17, а использование std::make_shared<> для этой специализации появилось только в C++20.
В качестве альтернативы такому использованию интеллектуальных указателей можно рекомендовать std::vector<> .
6.3. Многомерные динамические массивы
Динамический массив не может быть динамическим по нескольким измерениям, то есть выражение new T[n][m] , где оба значения n и m определяются в процессе выполнения программы, не корректно. Но мы можем создать динамический массив, каждый элемент которого является встроенным массивом с размером, известным на стадии компиляции. Если M выражение, допустимое для определения размера массива, то следующая инструкция создает такой массив:
T(*pa)[M] = new T[n][M];
Оператор new[] возвращает указатель на массив. Доступ к элементам такого массива будет осуществляться через выражение pa[i][j] , в свою очередь pa[i] будет массив из M элементов типа T .
При использовании псевдонимов можно получить более лаконичный синтаксис.
using I4 = int[4]; I4 *pa = new I4[n];
Используя перегрузку оператора [] легко создать класс, который хранит данные в одномерном массиве, но при этом предоставляет интерфейс многомерного массива. Вот пример предельно упрощенного класса матрицы.
template class MatrixView // 2D interface to a buffer < T * const m_Buff; int const m_RowCount; int const m_ColCount; public: MatrixView(T* buff, int rowCount, int colCount) : m_Buff(buff) , m_RowCount(rowCount) , m_ColCount(colCount) <>T *operator[](int rowInd) const < return m_Buff + rowInd * m_ColCount; >>; template class DynBuffer // buffer owner < T* const m_Buff; protected: T* Buff() const < return m_Buff; >; DynBuffer(int length) : m_Buff(new T[length]<>) <> ~DynBuffer() < delete[] m_Buff; >DynBuffer(const DynBuffer&) = delete; DynBuffer& operator=(const DynBuffer&) = delete; >; template class MatrixSimple : DynBuffer, public MatrixView < using Buff = DynBuffer; using View = MatrixView; public: MatrixSimple(int rowCount, int colCount) : Buff(rowCount * colCount) , View(Buff::Buff(), rowCount, colCount) <> >;
Вот пример использования:
MatrixSimple mtx(3, 3); mtx[1][2] = 42; // первая строка, второй столбец
Более продвинутый класс матрицы может использовать специальный вложенный proxy-класс, представляющий строку, например RowProxy , и индексатор будет возвращать экземпляр этого класса. Такой класс может, например, контролировать значение индекса, предоставлять функции-члены begin() , end() , etc. Аналогичное решение может быть и для столбцов.
7. Использование массивов в шаблонах
Тип массива можно использовать в качестве шаблонных аргументов и для специализации шаблонов классов.
Можно определить частичную специализацию шаблона класса для массивов не задавая при этом размер массива, то есть для массивов «вообще». Для этого в качестве типа специализации надо использовать T[] . Конечно, можно определить частичную специализацию для массива с заданным размером. Вот пример.
// первичный шаблон template struct U < const char* Tag() const < return "primary"; >>; // частичная специализация для указателей template struct U < const char* Tag() const < return "pointer"; >>; // частичная специализация для массивов template struct U
< const char* Tag() const < return "array"; >>; // частичная специализация для массивов с заданным размером template structs U < const char* Tag() const < return "array[N]"; >>; U u1; U u2; U u3; U u4; std::cout primary pointer array array[N]В стандартной библиотеке частичная специализация интеллектуального указателя std::unique_ptr<> и std::shared_ptr<> для массивов используется для управления жизненным циклом динамического массива, подробнее см. раздел 6.2.
Для программирования шаблонов, использующих массивы в качестве шаблонных аргументов, в стандартной библиотеке (заголовочный файл ) имеется несколько свойст типов: std::is_array<> , std::extent<> , std::rank<> , std::remove_extent<> . Вот примеры их использования (в примерах используется появившаяся в C++17 возможность использовать суффикс _v вместо члена value ):
std::cout
1 1 1 0 0 4 8 0 1 1 2 0В качестве реального примера использования этих свойст типов приведем немного упрощенное определение перегруженного варианта шаблона функции std::make_unique<> для массивов (см. раздел 6.2):
template && extent_v == 0, int> s = 0> unique_ptr make_unique(size_t size) < using elem_t = remove_extent_t; // тип элемента массива return unique_ptr(new elem_t[size]<>); >
Шаблоны функций не поддерживают частичную специализацию, поэтому здесь используется техника, которая называется отключение шаблонов (template disabling). Этот шаблон будет отключен, то есть не будет конкретизироваться, для любых аргументов шаблона, тип которых отличается от T[] . Соответственно, перегруженный вариант std::make_unique<> для аргументов шаблона остальных типов аналогичным способом будет отключен для T[] .
8. Стандартные альтернативы массивам
Стандартная библиотека предоставляет несколько классов (точнее шаблонов классов), которые рекомендуется использовать вместо массивов.
Вместо встроенных массивов рекомендуется использовать шаблон std::array<> . (Появился в C++11, см. [Josuttis].) Этот шаблон является объектной оберткой встроенного массива, он имеет два шаблонных параметра: тип элементов и размер. Размер должен быть известен на стадии компиляции, но в отличии от встроенного массива может быть нулевым. Вот пример:
std::array a;
Этот шаблон поддерживает индексатор и традиционный интерфейс стандартного контейнера.
for (int i = 0; i < a.size(); ++i) < std::cout for (auto it = a.begin(); it != a.end(); ++it) < std::cout for (auto t : a)Вместо динамических массивов рекомендуется использовать std::vector<> . Этот шаблон хорошо известен программистам, подробно описан в литературе (стандартный контейнер №1), поэтому каких-то дополнительных подробностей можно не приводить.
Есть еще довольно специфический и не особо популярный шаблон std::valarray<> . Он позволяет эмулировать интерфейс многомерных массивов.
Список литературы
[Josuttis]
Джосаттис, Николаи М. Стандартная библиотека C++: справочное руководство, 2-е изд.: Пер. с англ. — М.: ООО «И.Д. Вильямс», 2014.