Динамические массивы и переменные: легко и просто!
Всем привет! В этой статье мы создадим массив и переменные применяя указатели. Если вы еще не почитали прошлую (начальную) статью про указатели, то советуем сначала изучить ее. Ну а если вы это все знаете, то погнали!
Быстрый переход по статье.
- Как создать динамический массив
- Как создать двумерный динамический массив
Что такое динамические переменные
Динамические переменные — это переменные, которые созданы напрямую с помощью указателей. Для них существует функция удаление (это мы разберем ниже).
Чтобы мы могли полноценно создавать динамические переменные, нам понадобится изучить конструктор — new
, после его использования в оперативной памяти компьютера выделяются ячейки на тот тип данных, который мы указали.
На каждый тип данных выделяется разное количество ячеек.
Для создания динамических переменных нам понадобится применять конструкцию ниже:
<тип данных указателя> *<имя указателя> = new <тип данных>(<первоначальное значение>); |
Давайте подробно ее разберем:
<тип данных указателя>
— указанный тип данных почти ни на что не повлияет. Читайте ниже.new
— это конструктор, который и будет заключительным звеном для создания нашей переменной.<тип данных>
— здесь нам понадобится указать тип, какой будет храниться в переменной. Он необязательно должен совпадать с типом указателя.<первоначальное значение>
— с помощью круглых скобок можно указать значение переменной еще при ее инициализации. Использование круглых скобок в этой конструкции необязательно.
Вы должны знать! Если тип переменной отличается от типа указателя — то эта динамическая переменная будет весить больше в оперативной памяти, чем такая же переменная с одинаковыми типами!
Пример использования динамических переменных
Внизу мы решили использовать динамические переменные:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
#include <iostream> using namespace std; int main() { setlocale(0, “”); int *a = new int; int b = 10; *a = b; cout <<“Теперь переменная a равна “<< *a << endl; cout <<“Пришло время удалить эту переменную!”; system(“pause”); return 0; } |
- В строке 7: мы объявили переменную, оперируя конструктором
new
. - Дальше в строке 11: значение нашей переменной становится равно 10.
- И в самом конце, в строке 15: выводим значение нашей переменной на экран.
Важно помнить! Динамические переменные — это указатели, и поэтому перед ними обязательно должен стоять оператор *
.
Удаление динамических переменных
Как мы говорили выше, у нас есть возможность освобождать память переменной или, если понятным языком, удалять переменную из оперативной памяти ПК.
Конечно, эта переменная и так удалится из оперативной памяти компьютера при завершении программы. Но если нам захотелось удалить ее еще в середине программы, то это будет возможно благодаря оператору delete
.
Чтобы его использовать, нужно применить конструкцию ниже:
- В самом начале мы используем оператор
delete
. - Дальше идет имя переменной.
Вы должны обратить внимание на отсутствие оператора *
перед именем переменной. Многие начинающие прогеры забывают про это и в дальнейшем пытаются найти ошибку часами.
Статическое и динамическое объявление переменных
Статическое объявление переменных имеет такой вид:
int number;
Использование динамических переменных имеет маленький плюс. Он заключается в освобождении памяти переменной до завершения программы. Благодаря этому мы можем сначала удалить переменную, а потом ее снова создать в другом участке программы (когда это нам будет нужно).
Что такое динамические массивы
Мы уже знакомы с миром массивов в C++. Мы не раз создавали их на определенное количество ячеек и при этом использовали статическое создание массивов.
Но еще ни разу не затрагивали их использование с указателями!
Мы создавали массивы на сто тысяч элементов, а то и больше. И не один раз бывало, что большое количество ячеек оставались неиспользованными. Это является неправильным применением оперативной памяти в ПК.
Чтобы мы бесполезно не использовали оперативную память в компьютере, нам понадобится оперировать с указателями в свете массивов.
Нам нужно вспомнить, что для создания статического массива количество ячеек нужно задавать числовой константой (а не переменной). Это очень неприятно, потому что в программе мы не знаем, сколько нам может понадобится ячеек.
Например, пользователь захотел вписать 1000 чисел в массив, а мы из-за незнания этого факта сделали массив всего лишь на 500 ячеек.
Динамический массив — это массив, у которого количество ячеек можно задавать и переменной, и числовой константой. Это большой плюс перед использованием статического массива.
Как работают динамические массивы
Для работы динамических массивов нам понадобится при инициализации указатель (всего лишь при инициализации!) и уже знакомый конструктор new
.
Как создать динамический массив в C++
Чтобы создать динамический массив мы будем использовать конструкцию ниже:
<тип данных> *<имя массива> = new <тип переменных> [<количество ячеек>]; |
<тип данных>
— без разницы какой тип данных тут будет находиться, но лучше тот, который будет совпадать с типом переменных.<тип переменных>
— указанный сюда тип и будут иметь ячейки массива.<количество ячеек>
— здесь мы задаем размер массива (например[n]
или[25]
).
Динамический массив полностью идентичен обычному массиву, кроме:
- Своей инициализации
- Возможностью своевременно освободить память.
Давайте рассмотрим пример с использованием динамического массива:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
int main() { setlocale(0, “”); int n; cout << “Введите количество чисел, которое вы хотите ввести: “; cin >> n; cout << “Введите “ << n << ” чисел: “; int *dinamich_array = new int [n]; // создаем // динамический массив for (int i = 0; i < n; i++) { cin >> dinamich_array[i]; // считываем числа в ячейки массива } cout << “Теперь давайте выведем элементы массива в обратном порядке: “; for (int i = n – 1 ; i >= 0; i—) { cout << dinamich_array[i] << ” “; // выводим значение всех ячеек } cout << endl << “Удаляем массив!”; delete [] dinamich_array; // удаляем динамический массив return 0; } |
Вот что будет при выполнении программы:
Задайте количество чисел, которое вы хотите ввести: 5
Введите 5 чисел: 2 4 6 8 16
Теперь давайте выведем элементы массива в обратном порядке: 16 8 6 4 2
Удаляем массив!
Process returned 0 (0x0) execution time : 0.010 s
Press any key to continue.
Удаление динамического массива
Для удаления динамического массива нам понадобится уже знакомый оператор — delete
.
Важно запомнить, что квадратные скобки нужно ставить перед <именем массива>
.
Как создать двумерный динамический массив в C++
Для создания двумерного динамического массива мы будем использовать похожую конструкцию (как и в одномерном динамическом массиве):
<тип данных> **<имя массива> = new <тип данных массива>* [<количество ячеек>]; |
Вам нужно обратить внимание на:
- Дополнительный оператор
*
перед<имя массива>
и после<тип данных массива>
.
Дальше для каждой ячейки мы должны создать одномерный массив. Чтобы это сделать, нам понадобится цикл for и конструктор new
.
for (int i = 0; i < n; i++) { <имя массива>[i] = new <тип ячеек> [<количество ячеек>]; } |
В <количество ячеек>
можно задавать разные значения. Поэтому сначала для первого массива можно задать длину 1 (new int [1]
), потом для второго — длину 2 (new int [2]
), как в примере ниже.
Внизу находится пример двумерного динамического массива:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
#include <iostream> using namespace std; int main() { setlocale(0, “”); int **dinamic_array2 = new int* [5]; // создаем for (int i = 0; i < 5; i++) { // двумерный dinamic_array2[i] = new int [i + 1]; // массив } // ! for (int i = 0; i < 5; i++) { cout << “Введите числа” << “(“ << i + 1 << “)” << “:”; for (int j = 0; j < i + 1; j++) { cin >> dinamic_array2[i][j]; } } for (int i = 0; i < 5; i++) { int sum = 0; for (int j = 0; j < i + 1; j++) { sum += dinamic_array2[i][j]; } cout << “Сумма “ << i + 1 << ” массива равна “ << sum << endl; } for (int i = 0; i < 5; i++) { delete [] dinamic_array2[i]; // удаляем массив } system(“pause”); return 0; } |
- В строках 8 — 11: создали двумерный динамический массив.
- В строках 13 — 18: заполнили массив.
- В строках 20 — 26: подсчитали и вывели по отдельности на экран сумму всех массивов.
- В строках 28 — 30: происходит удаление массива (об этом ниже).
Удаление двумерного динамического массива
Для удаление двумерного динамического массива мы будем использовать уже похожую схему. Но в ней присутствует цикл for, и после <имя массива> находится индекс того массива который будет удален.
for (int i = 0; i < <количество элементов в массиве>; i++) { delete [] <имя массива>[i]; } |
Тест на тему «Динамические массивы и переменные». Проверь себя!
Пожалуйста, подождите пока страница загрузится полностью.
Если эта надпись не исчезает долгое время, попробуйте обновить страницу. Этот тест использует javascript. Пожалуйста, влкючите javascript в вашем браузере.
If loading fails, click here to try again
Если ты полностью разобрался в данном материале, то попробуй пройти несложный тест, который сможет выявить твои слабые стороны.
На этом у нас все! Если есть вопрос, пишите в комментариях. Удачи!
Динамическое выделение памяти необходимо для эффективного использования памяти компьютера. Например, мы написали какую-то программку, которая обрабатывает массив. При написании данной программы необходимо было объявить массив, то есть задать ему фиксированный размер (к примеру, от 0 до 100 элементов). Тогда данная программа будет не универсальной, ведь может обрабатывать массив размером не более 100 элементов. А если нам понадобятся всего 20 элементов, но в памяти выделится место под 100 элементов, ведь объявление массива было статическим, а такое использование памяти крайне не эффективно.
В С++ операции new
и delete
предназначены для динамического распределения памяти компьютера. Операция new
выделяет память из области свободной памяти, а операция delete
высвобождает выделенную память. Выделяемая память, после её использования должна высвобождаться, поэтому операции new
и delete
используются парами. Даже если не высвобождать память явно, то она освободится ресурсами ОС по завершению работы программы. Рекомендую все-таки не забывать про операцию delete
.
// пример использования операции new int *ptrvalue = new int; //где ptrvalue – указатель на выделенный участок памяти типа int //new – операция выделения свободной памяти под создаваемый объект.
Операция new
создает объект заданного типа, выделяет ему память и возвращает указатель правильного типа на данный участок памяти. Если память невозможно выделить, например, в случае отсутствия свободных участков, то возвращается нулевой указатель, то есть указатель вернет значение 0. Выделение памяти возможно под любой тип данных: int, float,double, char
и т. д.
// пример использования операции delete: delete ptrvalue; // где ptrvalue – указатель на выделенный участок памяти типа int // delete – операция высвобождения памяти
Разработаем программу, в которой будет создаваться динамическая переменная.
- MVS
- Code::Blocks
- Dev-C++
- QtCreator
// new_delete.cpp: определяет точку входа для консольного приложения. #include "stdafx.h" #include <iostream> using namespace std; int main(int argc, char* argv[]) { int *ptrvalue = new int; // динамическое выделение памяти под объект типа int *ptrvalue = 9; // инициализация объекта через указатель //int *ptrvalue = new int (9); инициализация может выполнятся сразу при объявлении динамического объекта cout << "ptrvalue = " << *ptrvalue << endl; delete ptrvalue; // высвобождение памяти system("pause"); return 0; }
// new_delete.cpp: определяет точку входа для консольного приложения. #include <iostream> using namespace std; int main(int argc, char* argv[]) { int *ptrvalue = new int; // динамическое выделение памяти под объект типа int *ptrvalue = 9; // инициализация объекта через указатель //int *ptrvalue = new int (9); инициализация может выполнятся сразу при объявлении динамического объекта cout << "ptrvalue = " << *ptrvalue << endl; delete ptrvalue; // высвобождение памяти return 0; }
В строке 10 показан способ объявления и инициализации девяткой динамического объекта, все, что нужно так это указать значение в круглых скобочках после типа данных. Результат работы программы показан на рисунке 1.
CppStudio.com
ptrvalue = 9 Для продолжения нажмите любую клавишу . . .
Рисунок 1 — Динамическая переменная
Создание динамических массивов
Как было сказано раньше, массивы также могут быть динамическими. Чаще всего операции new
и delete
применяются для создания динамических массивов, а не для создания динамических переменных. Рассмотрим фрагмент кода создания одномерного динамического массива.
// объявление одномерного динамического массива на 10 элементов: float *ptrarray = new float [10]; // где ptrarray – указатель на выделенный участок памяти под массив вещественных чисел типа float // в квадратных скобочках указываем размер массива
После того как динамический массив стал ненужным, нужно освободить участок памяти, который под него выделялся.
// высвобождение памяти отводимой под одномерный динамический массив: delete [] ptrarray;
После оператора delete ставятся квадратные скобочки, которые говорят о том, что высвобождается участок памяти, отводимый под одномерный массив. Разработаем программу, в которой создадим одномерный динамический массив, заполненный случайными числами.
- MVS
- Code::Blocks
- Dev-C++
- QtCreator
// new_delete_array.cpp: определяет точку входа для консольного приложения. #include "stdafx.h" #include <iostream> // в заголовочном файле <ctime> содержится прототип функции time() #include <ctime> // в заголовочном файле <iomanip> содержится прототип функции setprecision() #include <iomanip> using namespace std; int main(int argc, char* argv[]) { srand(time(0)); // генерация случайных чисел float *ptrarray = new float [10]; // создание динамического массива вещественных чисел на десять элементов for (int count = 0; count < 10; count++) ptrarray[count] = (rand() % 10 + 1) / float((rand() % 10 + 1)); //заполнение массива случайными числами с масштабированием от 1 до 10 cout << "array = "; for (int count = 0; count < 10; count++) cout << setprecision(2) << ptrarray[count] << " "; delete [] ptrarray; // высвобождение памяти cout << endl; system("pause"); return 0; }
// new_delete_array.cpp: определяет точку входа для консольного приложения. #include <iostream> // в заголовочном файле <ctime> содержится прототип функции time() #include <ctime> // в заголовочном файле <iomanip> содержится прототип функции setprecision() #include <iomanip> #include <cstdlib> using namespace std; int main(int argc, char* argv[]) { srand(time(0)); // генерация случайных чисел float *ptrarray = new float [10]; // создание динамического массива вещественных чисел на десять элементов for (int count = 0; count < 10; count++) ptrarray[count] = (rand() % 10 + 1) / float((rand() % 10 + 1)); //заполнение массива случайными числами с масштабированием от 1 до 10 cout << "array = "; for (int count = 0; count < 10; count++) cout << setprecision(2) << ptrarray[count] << " "; delete [] ptrarray; // высвобождение памяти cout << endl; system("pause"); return 0; }
Созданный одномерный динамический массив заполняется случайными вещественными числами, полученными c помощью функций генерации случайных чисел, причём числа генерируются в интервале от 1 до 10, интервал задается так — rand() % 10 + 1
. Чтобы получить случайные вещественные числа, выполняется операция деления, с использованием явного приведения к вещественному типу знаменателя — float((rand() % 10 + 1))
. Чтобы показать только два знака после запятой используем функцию setprecision(2)
, прототип данной функции находится в заголовочном файле <iomanip>
. Функция time(0)
засевает генератор случайных чисел временным значением, таким образом, получается, воспроизводить случайность возникновения чисел (см. Рисунок 2).
CppStudio.com
array = 0.8 0.25 0.86 0.5 2.2 10 1.2 0.33 0.89 3.5 Для продолжения нажмите любую клавишу . . .
Рисунок 2 — Динамический массив в С++
По завершению работы с массивом, он удаляется, таким образом, высвобождается память, отводимая под его хранение.
Как создавать и работать с одномерными динамическими массивами мы научились. Теперь рассмотрим фрагмент кода, в котором показано, как объявляется двумерный динамический массив.
// объявление двумерного динамического массива на 10 элементов: float **ptrarray = new float* [2]; // две строки в массиве for (int count = 0; count < 2; count++) ptrarray[count] = new float [5]; // и пять столбцов // где ptrarray – массив указателей на выделенный участок памяти под массив вещественных чисел типа float
Сначала объявляется указатель второго порядка float **ptrarray
, который ссылается на массив указателей float* [2]
, где размер массива равен двум. После чего в цикле for
каждой строке массива объявленного в строке 2 выделяется память под пять элементов. В результате получается двумерный динамический массив ptrarray[2][5]
. Рассмотрим пример высвобождения памяти отводимой под двумерный динамический массив.
// высвобождение памяти отводимой под двумерный динамический массив: for (int count = 0; count < 2; count++) delete [] ptrarray[count]; // где 2 – количество строк в массиве
Объявление и удаление двумерного динамического массива выполняется с помощью цикла, так как показано выше, необходимо понять и запомнить то, как это делается. Разработаем программу, в которой создадим двумерный динамический массив.
- MVS
- Code::Blocks
- Dev-C++
- QtCreator
// new_delete_array2.cpp: определяет точку входа для консольного приложения. #include "stdafx.h" #include <iostream> #include <ctime> #include <iomanip> using namespace std; int main(int argc, char* argv[]) { srand(time(0)); // генерация случайных чисел // динамическое создание двумерного массива вещественных чисел на десять элементов float **ptrarray = new float* [2]; // две строки в массиве for (int count = 0; count < 2; count++) ptrarray[count] = new float [5]; // и пять столбцов // заполнение массива for (int count_row = 0; count_row < 2; count_row++) for (int count_column = 0; count_column < 5; count_column++) ptrarray[count_row][count_column] = (rand() % 10 + 1) / float((rand() % 10 + 1)); //заполнение массива случайными числами с масштабированием от 1 до 10 // вывод массива for (int count_row = 0; count_row < 2; count_row++) { for (int count_column = 0; count_column < 5; count_column++) cout << setw(4) <<setprecision(2) << ptrarray[count_row][count_column] << " "; cout << endl; } // удаление двумерного динамического массива for (int count = 0; count < 2; count++) delete []ptrarray[count]; system("pause"); return 0; }
// new_delete_array2.cpp: определяет точку входа для консольного приложения. #include <iostream> #include <ctime> #include <iomanip> #include <cstdlib> using namespace std; int main(int argc, char* argv[]) { srand(time(0)); // генерация случайных чисел // динамическое создание двумерного массива вещественных чисел на десять элементов float **ptrarray = new float* [2]; // две строки в массиве for (int count = 0; count < 2; count++) ptrarray[count] = new float [5]; // и пять столбцов // заполнение массива for (int count_row = 0; count_row < 2; count_row++) for (int count_column = 0; count_column < 5; count_column++) ptrarray[count_row][count_column] = (rand() % 10 + 1) / float((rand() % 10 + 1)); //заполнение массива случайными числами с масштабированием от 1 до 10 // вывод массива for (int count_row = 0; count_row < 2; count_row++) { for (int count_column = 0; count_column < 5; count_column++) cout << setw(4) <<setprecision(2) << ptrarray[count_row][count_column] << " "; cout << endl; } // удаление двумерного динамического массива for (int count = 0; count < 2; count++) delete []ptrarray[count]; system("pause"); return 0; }
При выводе массива была использована функция setw()
, если вы не забыли, то она отводит место заданного размера под выводимые данные. В нашем случае, под каждый элемент массива по четыре позиции, это позволяет выровнять, по столбцам, числа разной длинны (см. Рисунок 3).
CppStudio.com
2.7 10 0.33 3 1.4 6 0.67 0.86 1.2 0.44 Для продолжения нажмите любую клавишу . . .
Рисунок 3 — Динамический массив в С++
Продолжаем серию «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 заголовочный файл <cstdlib>
) возвращает размер массива, то есть количество элементов. В С++17 появился стандартный шаблон функции std::size()
, которая делает то же самое (а еще имеет перегруженную версию, которая определяет размер стандартного контейнера).
int a[4];
std::cout << sizeof(a) << ' ' << std::size(a) << 'n';
Вывод:
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::cout << t << ' ';
}
А также в стандартных алгоритмах:
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<int> rr[2] = { u, v };
std::cout << rr[0] << ' ' << rr[1] << 'n'; // вывод: 42 5
++rr[0];
rr[1].get() = 125; // get() необходим
std::cout << u << ' ' << v << 'n'; // вывод: 43 125
Нельзя объявить массив функций.
int ff[2](double); // ошибка
Вместо этого можно использовать массив указателей на функцию.
int (*ff[2])(double);
Шаблон std::reference_wrapper<>
можно конкретизировать типом функции, но преимуществ перед указателем практически нет — функцию и так можно вызвать через указатель без разыменования, а инициализировать указатель именем функции без оператора &
. Есть еще вариант эмулирования массива функций — это использование шаблона std::function<>
, но этот шаблон является темой отдельного разговора.
Массив нельзя объявить с помощью ключевого слова auto
.
auto x[2] = {1, 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 < end; ++d)
{
*d = rand();
}
Но все же сведение можно отнести к сишным архаизмам и с ним надо быть внимательным и аккуратным, иначе можно столкнуться с не самыми приятными неожиданностями.
Вот как сведение влияет на объявления функций. Функции
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<typename T>
void Foo(T t);
тип параметра шаблонной функции также будет выведен как указатель, если аргумент является массивом.
Сведение вызывает дополнительные проблемы при использовании наследования. (В C ведь нет наследования.) Рассмотрим пример.
class B {/* ... */};
class D : public B {/* ... */};
void Foo(B[], int size); // обработка массива элементов типа B
Следующий код компилируется без ошибок и предупреждений.
D d[4];
Foo(d, _countof(d));
Но если sizeof(B) < sizeof(D)
, то в теле Foo()
смещение элементов массива d
(кроме нулевого, конечно) будет определятся неправильно и, соответственно, почти всегда Foo()
будет работать некорректно. Так что работать с массивами в полиморфном стиле, через указатель на базовый класс, нельзя.
2.2. Копирование
Наряду со сведением (и тесно связанная с ним) есть еще одна особенность типа массива, которая делает его в некотором смысле «неполноценным». Массивы не поддерживают привычный синтаксис инициализации и присваивания, основанный на семантике копирования:
using I4 = int[4];
I4 a;
I4 b = a; // ошибка
I4 b{a}; // ошибка
I4 b(a); // ошибка
I4 b2;
b2 = a; // ошибка
Также функция не может возвращать массив.
I4 Foo(); // ошибка
Но если массив является членом класса/структуры/объединения, то копирующий конструктор и соответствующий оператор присваивания, генерируемые компилятором, выполняют поэлементное копирование такого массива.
struct X
{
int A[4];
};
X x = { {1, 2, 3, 4} };
X x2 = x; // копирование
X x3{x}; // копирование
X x4;
x4 = x; // присваивание
X Foo() // возвращаемое значение функции
{
return { {4, 3, 2, 1} };
}
Еще одна ситуация, когда происходит копирование массива — это захват массива по значению в лямбда-выражении.
intptr_t x[4];
auto f = [x]() { return sizeof(x) / sizeof(x[0]); };
std::cout << f() << 'n';
Вывод:
4
Но если используется инициализирующий захват (C++14), то происходит сведение.
intptr_t u[4];
auto g = [x = u]() { return sizeof(x) / sizeof(x[0]); };
std::cout << g() << 'n';
Вывод:
1
3. Инициализация массивов
Для описания правил инициализации массивов необходимо кратко рассказать о тривиальных типах.
3.1. Тривиальные типы и неинициализированные переменные
Конструкторы и деструкторы можно назвать ключевыми элементами объектной модели С++. При создании объекта обязательно вызывается конструктор, а при удалении — деструктор. Но проблемы совместимости с С вынудили сделать некоторое исключение, и это исключение называется тривиальные типы. Они введены для моделирования сишных типов и сишного жизненного цикла переменных, без обязательного вызова конструктора и деструктора. Сишный код, если он компилируется и выполняется в С++, должен работать так же, как в С. К тривиальным типам относятся числовые типы, указатели, перечисления, а также классы, структуры, объединения и массивы, состоящие из тривиальных типов. Классы и структуры должны удовлетворять некоторым дополнительным условиям: отсутствие пользовательского конструктора, деструктора, копирования, присваивания, виртуальных функций.
Переменная тривиального типа будет неинициализированной, если не использовать какой-нибудь вариант явной инициализации. Для тривиального класса компилятор может сгенерировать конструктор по умолчанию и деструктор. Конструктор по умолчанию обнуляет объект, деструктор ничего не делает. Но этот конструктор будет сгенерирован и использован только, если использовать какой-нибудь вариант явной инициализации, иначе переменная останется неинициализированной.
Неинициализированная переменная устроена следующим образом: если она объявлена в области видимости пространства имен (глобально), будет иметь все биты нулевыми, если локально, или создана динамически, то получит случайный набор битов. Понятно, что использование такой переменной может привести к непредсказуемому поведению программы. Массивы достаточно часто имеют тривиальный тип и поэтому эта проблема для них весьма актуальна.
Неинициализированные константы тривиального типа выявляет компилятор, иногда он выявляет и другие неинициализированные переменные, но с этой задачей лучше справляются статические анализаторы кода.
В стандартной библиотеке С++11 есть шаблоны, называемые свойствами типов (заголовочный файл <type_traits>
). Один из них позволяет определить, является ли тип тривиальным. Выражение 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 a[] = {x1 /*, ... */};
или
T a[]{x1 /*, ... */};
Наличие нужного преобразования эквивалентно корректности инструкции
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(&ra)[N] = a;
Как и для любой ссылки, инициализация переменной типа ссылка на массив является обязательной. Тип ссылки на массива обозначается как 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<typename T>
void Foo(T& t);
тип параметра шаблонной функции также будет выведен как ссылка на массив, если аргумент является массивом.
Особенно удобно использовать шаблоны с выводом типа и размера массива.
template<typename T, std::size_t N>
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] = {{1, 2, 3, 4}, {5, 6, 7, 8}};
Если нужно гарантировать только инициализацию по умолчанию, то можно использовать пустой список инициализации {}
. Определения размера по списку инициализации возможно только по внешнему размеру.
int b[][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}}; // ОК
int b[][] = {{1, 2, 3, 4}, {5, 6, 7, 8}}; // ошибка
Можно получить указатель на двумерный массив:
T a[N][M];
T(*pa)[N][M] = &a;
Также можно получить ссылку. Вот пример использования ссылки на двумерный массив.
template<typename T, std::size_t N, std::size_t M>
void Print2dArray(T(&a)[N][M])
{
for (int i = 0; i < N; ++i)
{
for (int j = 0; j < M; ++j)
{
std::cout << a[i][j] << ' ';
}
std::cout << 'n';
}
}
// ...
int b[2][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}};
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]{1, 2, 3, 4};
Если число инициализаторов больше размера массива, то лишние не используются (компилятор может выдать ошибку, если значение 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_ptr<int[]> aptr(new int[n]);
for (int i = 0; i < n; ++i)
{
aptr[i] = i;
}
Эта поддержка не является полноценной: не хранится информация о размере массива, поэтому нет возможности контролировать корректностью индекса, не поддерживается интерфейс стандартных контейнеров и диапазонный for
.
В C++14 появилась возможность создать динамический массив и инициализировать им экземпляр std::unique_ptr<>
с помощью std::make_unique<>
:
auto aptr = std::make_unique<int[]>(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<typename T>
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<typename T>
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<typename T>
class MatrixSimple
: DynBuffer<T>, public MatrixView<T>
{
using Buff = DynBuffer<T>;
using View = MatrixView<T>;
public:
MatrixSimple(int rowCount, int colCount)
: Buff(rowCount * colCount)
, View(Buff::Buff(), rowCount, colCount)
{}
};
Вот пример использования:
MatrixSimple<int> mtx(3, 3);
mtx[1][2] = 42; // первая строка, второй столбец
Более продвинутый класс матрицы может использовать специальный вложенный proxy-класс, представляющий строку, например RowProxy
, и индексатор будет возвращать экземпляр этого класса. Такой класс может, например, контролировать значение индекса, предоставлять функции-члены begin()
, end()
, etc. Аналогичное решение может быть и для столбцов.
7. Использование массивов в шаблонах
Тип массива можно использовать в качестве шаблонных аргументов и для специализации шаблонов классов.
Можно определить частичную специализацию шаблона класса для массивов не задавая при этом размер массива, то есть для массивов «вообще». Для этого в качестве типа специализации надо использовать T[]
. Конечно, можно определить частичную специализацию для массива с заданным размером. Вот пример.
// первичный шаблон
template <typename T>
struct U
{
const char* Tag() const { return "primary"; }
};
// частичная специализация для указателей
template <typename T>
struct U<T*>
{
const char* Tag() const { return "pointer"; }
};
// частичная специализация для массивов
template <typename T>
struct U<T[]>
{
const char* Tag() const { return "array"; }
};
// частичная специализация для массивов с заданным размером
template <typename T, size_t N>
structs U<T[N]>
{
const char* Tag() const { return "array[N]"; }
};
U<int> u1;
U<int*> u2;
U<int[]> u3;
U<int[4]> u4;
std::cout << u1.Tag() << ' ' << u2.Tag() << ' ' << u3.Tag() ' ' << u4.Tag();
Вывод:
primary pointer array array[N]
В стандартной библиотеке частичная специализация интеллектуального указателя std::unique_ptr<>
и std::shared_ptr<>
для массивов используется для управления жизненным циклом динамического массива, подробнее см. раздел 6.2.
Для программирования шаблонов, использующих массивы в качестве шаблонных аргументов, в стандартной библиотеке (заголовочный файл <type_traits>
) имеется несколько свойст типов: std::is_array<>
, std::extent<>
, std::rank<>
, std::remove_extent<>
. Вот примеры их использования (в примерах используется появившаяся в C++17 возможность использовать суффикс _v
вместо члена value
):
std::cout <<
std::is_array_v<int[]> << ' ' <<
std::is_array_v<int[4]> << ' ' <<
std::is_array_v<int[4][8]> << ' ' <<
std::is_array_v<int> << 'n';
std::cout <<
std::extent_v<int[]> << ' ' <<
std::extent_v<int[4]> << ' ' <<
std::extent_v<int[4][8], 1> << ' ' <<
std::extent_v<int> << 'n';
std::cout <<
std::rank_v<int[]> << ' ' <<
std::rank_v<int[4]> << ' ' <<
std::rank_v<int[4][8]> << ' ' <<
std::rank_v<int> << 'n';
Вывод:
1 1 1 0 0 4 8 0 1 1 2 0
В качестве реального примера использования этих свойст типов приведем немного упрощенное определение перегруженного варианта шаблона функции std::make_unique<>
для массивов (см. раздел 6.2):
template <class T,
enable_if_t<is_array_v<T> && extent_v<T> == 0, int> s = 0>
unique_ptr<T> make_unique(size_t size) {
using elem_t = remove_extent_t<T>; // тип элемента массива
return unique_ptr<T>(new elem_t[size]{});
}
Шаблоны функций не поддерживают частичную специализацию, поэтому здесь используется техника, которая называется отключение шаблонов (template disabling). Этот шаблон будет отключен, то есть не будет конкретизироваться, для любых аргументов шаблона, тип которых отличается от T[]
. Соответственно, перегруженный вариант std::make_unique<>
для аргументов шаблона остальных типов аналогичным способом будет отключен для T[]
.
8. Стандартные альтернативы массивам
Стандартная библиотека предоставляет несколько классов (точнее шаблонов классов), которые рекомендуется использовать вместо массивов.
Вместо встроенных массивов рекомендуется использовать шаблон std::array<>
. (Появился в C++11, см. [Josuttis].) Этот шаблон является объектной оберткой встроенного массива, он имеет два шаблонных параметра: тип элементов и размер. Размер должен быть известен на стадии компиляции, но в отличии от встроенного массива может быть нулевым. Вот пример:
std::array<int, 4> a{1, 2, 3, 4};
Этот шаблон поддерживает индексатор и традиционный интерфейс стандартного контейнера.
for (int i = 0; i < a.size(); ++i)
{
std::cout << a[i] << ' ';
}
for (auto it = a.begin(); it != a.end(); ++it)
{
std::cout << *it << ' ';
}
for (auto t : a)
{
std::cout << t << ' ';
}
Вместо динамических массивов рекомендуется использовать std::vector<>
. Этот шаблон хорошо известен программистам, подробно описан в литературе (стандартный контейнер №1), поэтому каких-то дополнительных подробностей можно не приводить.
Есть еще довольно специфический и не особо популярный шаблон std::valarray<>
. Он позволяет эмулировать интерфейс многомерных массивов.
Список литературы
[Josuttis]
Джосаттис, Николаи М. Стандартная библиотека C++: справочное руководство, 2-е изд.: Пер. с англ. — М.: ООО «И.Д. Вильямс», 2014.
Динамические массивы
Последнее обновление: 24.03.2023
Кроме отдельных динамических объектов в языке C++ мы можем использовать динамические массивы. Для выделения памяти под динамический массив
также используется оператор new, после которого в квадратных скобках указывается, сколько массив будет содержать объектов:
int *numbers {new int[4]}; // динамический массив из 4 чисел // или так // int *numbers = new int[4];
Причем в этом случае оператор new также возвращает указатель на объект типа int – первый элемент в созданном массиве.
В данном случае определяется массив из четырех элементов типа int, но каждый из них имеет неопределенное значение. Однако мы также можем инициализировать массив значениями:
int *numbers1 {new int[4]{}}; // массив состоит из чисел 0, 0, 0, 0 int *numbers2 {new int[4]{ 1, 2, 3, 4 }}; // массив состоит из чисел 1, 2, 3, 4 int *numbers3 {new int[4]{ 1, 2 }}; // массив состоит из чисел 1, 2, 0, 0 // аналогичные определения массивов // int *numbers1 = new int[4]{}; // массив состоит из чисел 0, 0, 0, 0 // int *numbers1 = new int[4](); // массив состоит из чисел 0, 0, 0, 0 // int *numbers2 = new int[4]{ 1, 2, 3, 4 }; // массив состоит из чисел 1, 2, 3, 4 // int *numbers3 = new int[4]{ 1, 2 }; // массив состоит из чисел 1, 2, 0, 0
При инициализации массива конкретными значениями следует учитывать, что если значений в фигурных скобках
больше чем длина массива, то оператор new потерпит неудачу и не сможет создать массив. Если переданных значений, наоборот, меньше, то элементы,
для которых не предоставлены значения, инициализируются значением по умолчанию.
Стоит отметить, что в стандарт С++20 добавлена возможность выведения размера массива, поэтому, если применяется стандарт С++20, то можно не указывать длину массива:
int *numbers {new int[]{ 1, 2, 3, 4 }}; // массив состоит из чисел 1, 2, 3, 4
После создания динамического массива мы сможем с ним работать по полученному указателю, получать и изменять его элементы:
int *numbers {new int[4]{ 1, 2, 3, 4 }}; // получение элементов через синтаксис массивов std::cout << numbers[0] << std::endl; // 1 std::cout << numbers[1] << std::endl; // 2 // получение элементов через операцию разыменования std::cout << *numbers << std::endl; // 1 std::cout << *(numbers+1) << std::endl; // 2
Причем для доступа к элементам динамического массива можно использовать как синтаксис массивов (numbers[0]
), так и операцию разыменования (*numbers
)
Соответственно для перебора такого массива можно использовать различные способы:
unsigned n{ 5 }; // размер массива int* p{ new int[n] { 1, 2, 3, 4, 5 } }; // используем индексы for (unsigned i{}; i < n; i++) { std::cout << p[i] << "t"; } std::cout << std::endl; // добавляем к адресу в указателе смещение for (unsigned i{}; i < n; i++) { std::cout << *(p+i)<< "t"; } std::cout << std::endl; // проходим по массиву с помощью вспомогательного указателя for (int* q{ p }; q != p + n; q++) { std::cout << *q << "t"; } std::cout << std::endl;
Обратите внимание, что для задания размера динамического массива мы можем применять обычную переменную, а не константу, как в случае со стандартными массивами.
Для удаления динамического массива и освобождения его памяти применяется специальная форма оператора delete:
delete [] указатель_на_динамический_массив;
Например:
#include <iostream> int main() { unsigned n{ 5 }; // размер массива int* p{ new int[n] { 1, 2, 3, 4, 5 } }; // используем индексы for (unsigned i{}; i < n; i++) { std::cout << p[i] << "t"; } std::cout << std::endl; delete [] p; }
Чтобы после освобождения памяти указатель не хранил старый адрес, также рекомендуется обнулить его:
delete [] p; p = nullptr; // обнуляем указатель
Многомерные массивы
Также мы можем создавать многомерные динамические массивы. Рассмотрим на примере двухмерных массивов. Что такое по сути двухмерный массив? Это набор массив массивов.
Соответственно, чтобы создать динамический двухмерный массив, нам надо создать общий динамический массив указателей, а затем его элементы – вложенные динамические массивы. В общем случае
это выглядит так:
#include <iostream> int main() { unsigned rows = 3; // количество строк unsigned columns = 2; // количество столбцов int** numbers{new int*[rows]{}}; // выделяем память под двухмерный массив // выделяем память для вложенных массивов for (unsigned i{}; i < rows; i++) { numbers[i] = new int[columns]{}; } // удаление массивов for (unsigned i{}; i < rows; i++) { delete[] numbers[i]; } delete[] numbers; }
Вначале выделяем память для массива указателей (условно таблицы):
int** numbers{new int*[rows]{}};
Затем в цикле выделяем память для каждого отдельного массива (условно строки таблицы):
numbers[i] = new int[columns]{};
Освобождение памяти идет в обратном порядке – сначала освобождаем память для каждого отдельного вложенного массива, а затем для всего массива указателей.
Пример с вводом и выводом данных двухмерного динамического массива:
#include <iostream> int main() { unsigned rows = 3; // количество строк unsigned columns = 2; // количество столбцов int** numbers{new int*[rows]{}}; // выделяем память под двухмерный массив for (unsigned i{}; i < rows; i++) { numbers[i] = new int[columns]{}; } // вводим данные для таблицы rows x columns for (unsigned i{}; i < rows; i++) { std::cout << "Enter data for " << (i + 1) << " row" << std::endl; // вводим данные для столбцов i-й строки for (unsigned j{}; j < columns; j++) { std::cout << (j + 1) << " column: "; std::cin >> numbers[i][j]; } } // вывод данных for (unsigned i{}; i < rows; i++) { // выводим данные столбцов i-й строки for (unsigned j{}; j < columns; j++) { std::cout << numbers[i][j] << "t"; } std::cout << std::endl; } for (unsigned i{}; i < rows; i++) { delete[] numbers[i]; } delete[] numbers; }
Пример работы программы:
Enter data for 1 row 1 column: 2 2 column: 3 Enter data for 2 row 1 column: 4 2 column: 5 Enter data for 3 row 1 column: 6 2 column: 7 2 3 4 5 6 7
Указатель на массив
От типа int**
, который представляет указатель на указатель (pointer-to-pointer) следует отличать ситуацию “указатель на массив” (pointer to array). Например:
#include <iostream> int main() { unsigned n{3}; // количество строк int (*a)[2] = new int[n][2]; int k{}; // устанавливаем значения for (unsigned i{}; i < n; i++) { // устанавливаем данные для столбцов i-й строки for (unsigned j{}; j < 2; j++) { a[i][j] = ++k; } } // вывод данных for (unsigned i{}; i < n; i++) { // выводим данные столбцов i-й строки for (unsigned j{}; j < 2; j++) { std::cout << a[i][j] << "t"; } std::cout << std::endl; } // удаляем данные delete[] a; a = nullptr; }
Здесь запись int (*a)[2]
представляет указатель на массив из двух элементов типа int. Фактически мы можем работать с этим объектом как с двухмерным массивом (таблицей), только количество
столбцов в данном случае фиксировано – 2. И память для такого массива выделяется один раз:
int (*a)[2] = new int[n][2];
То есть в данном случае мы имеем дело с таблице из n строк и 2 столцов. Используя два индекса (для строки и столца), можно обращаться к определенному элементу, установить или получить его значение.
Консольный вывод данной программы:
1 2 3 4 5 6
Array in C is static in nature, so its size should be known at compile time and we can’t change the size of the array after its declaration. Due to this, we may encounter situations where our array doesn’t have enough space left for required elements or we allotted more than the required memory leading to memory wastage. To solve this problem, dynamic arrays come into the picture.
A Dynamic Array is allocated memory at runtime and its size can be changed later in the program.
We can create a dynamic array in C by using the following methods:
- Using malloc() Function
- Using calloc() Function
- Resizing Array Using realloc() Function
- Using Variable Length Arrays(VLAs)
- Using Flexible Array Members
1. Dynamic Array Using malloc() Function
The “malloc” or “memory allocation” method in C is used to dynamically allocate a single large block of memory with the specified size. It returns a pointer of type void which can be cast into a pointer of any form. It is defined inside <stdlib.h> header file.
Syntax:
ptr = (cast-type*) malloc(byte-size);
We can use this function to create a dynamic array of any type by simply allocating a memory block of some size and then typecasting the returned void pointer to the pointer of the required type.
Example:
ptr = (int*) malloc(100 * sizeof(int));
In the above example, we have created a dynamic array of type int and size 100 elements.
Note: It is to be noted that if malloc fails to allocate the required memory, it returns the NULL pointer. So, it is a good practice to check for NULL pointer to see if the memory is successfully allocated or not.
Example:
C
#include <stdio.h>
#include <stdlib.h>
int
main()
{
int
* ptr;
int
size;
printf
(
"Enter size of elements:"
);
scanf
(
"%d"
, &size);
ptr = (
int
*)
malloc
(size *
sizeof
(
int
));
if
(ptr == NULL) {
printf
(
"Memory not allocated.n"
);
}
else
{
printf
(
"Memory successfully allocated using "
"malloc.n"
);
for
(
int
j = 0; j < size; ++j) {
ptr[j] = j + 1;
}
printf
(
"The elements of the array are: "
);
for
(
int
k = 0; k < size; ++k) {
printf
(
"%d, "
, ptr[k]);
}
}
return
0;
}
Output:
Enter size of elements:5 Memory successfully allocated using malloc. The elements of the array are: 1, 2, 3, 4, 5,
2. Dynamic Array Using calloc() Function
The “calloc” or “contiguous allocation” method in C is used to dynamically allocate the specified number of blocks of memory of the specified type and initialized each block with a default value of 0.
The process of creating a dynamic array using calloc() is similar to the malloc() method. The difference is that calloc() takes arguments instead of one as compared to malloc(). Here, we provide the size of each element and the number of elements required in the dynamic array. Also, each element in the array is initialized to zero.
Syntax:
ptr = (cast-type*)calloc(n, element-size);
Example:
ptr = (int*) calloc(5, sizeof(float));
In the above example, we have created a dynamic array of type float having five elements.
Let’s take another example to create a dynamic array using the calloc() method.
Example:
C
#include <stdio.h>
#include <stdlib.h>
int
main()
{
int
* ptr;
int
size;
printf
(
"Enter size of elements:"
);
scanf
(
"%d"
, &size);
ptr = (
int
*)
calloc
(size,
sizeof
(
int
));
if
(ptr == NULL) {
printf
(
"Memory not allocated.n"
);
}
else
{
printf
(
"Memory successfully allocated using "
"malloc.n"
);
for
(
int
j = 0; j < size; ++j) {
ptr[j] = j + 1;
}
printf
(
"The elements of the array are: "
);
for
(
int
k = 0; k < size; ++k) {
printf
(
"%d, "
, ptr[k]);
}
}
return
0;
}
Output:
Enter size of elements:6 Memory successfully allocated using malloc. The elements of the array are: 1, 2, 3, 4, 5, 6,
3. Dynamically Resizing Array Using realloc() Function
The “realloc” or “re-allocation” method in C is used to dynamically change the memory allocation of a previously allocated memory.
Using this function we can create a new array or change the size of an already existing array.
Syntax:
ptr = realloc(ptr, newSize);
Let’s take an example to understand this properly.
Example:
C
#include <stdio.h>
#include <stdlib.h>
int
main()
{
int
* ptr;
int
size = 5;
ptr = (
int
*)
calloc
(size,
sizeof
(
int
));
if
(ptr == NULL) {
printf
(
"Memory not allocated.n"
);
exit
(0);
}
else
{
printf
(
"Memory successfully allocated using "
"calloc.n"
);
}
for
(
int
j = 0; j < size; ++j) {
ptr[j] = j + 1;
}
printf
(
"The elements of the array are: "
);
for
(
int
k = 0; k < size; ++k) {
printf
(
"%d, "
, ptr[k]);
}
printf
(
"n"
);
size = 10;
int
*temp = ptr;
ptr =
realloc
(ptr, size *
sizeof
(
int
));
if
(!ptr) {
printf
(
"Memory Re-allocation failed."
);
ptr = temp;
}
else
{
printf
(
"Memory successfully re-allocated using "
"realloc.n"
);
}
for
(
int
j = 5; j < size; ++j) {
ptr[j] = j + 10;
}
printf
(
"The new elements of the array are: "
);
for
(
int
k = 0; k < size; ++k) {
printf
(
"%d, "
, ptr[k]);
}
return
0;
}
Output
Memory successfully allocated using calloc. The elements of the array are: 1, 2, 3, 4, 5, Memory successfully re-allocated using realloc. The new elements of the array are: 1, 2, 3, 4, 5, 15, 16, 17, 18, 19,
To Know more about these above methods, please refer to the article – malloc, calloc,free in C
4. Variable Length Arrays(VLAs)
Variable length arrays or VLAs, are those arrays in which we can determine the size of the array at the run time. It allocates the memory in the stack and it’s based on the local scope level.
The size of a variable length array cannot be changed once it is defined and using variable length array as its found down sides as compared to above methods.
Example:
C
#include <stdio.h>
int
main()
{
int
n;
printf
(
"Enter the size of the array: "
);
scanf
(
"%d"
, &n);
int
arr[n];
printf
(
"Enter elements: "
);
for
(
int
i = 0; i < n; ++i) {
scanf
(
"%d"
, &arr[i]);
}
printf
(
"Elements of VLA of Given Size: "
);
for
(
int
i = 0; i < n; ++i) {
printf
(
"%d "
, arr[i]);
}
return
0;
}
Output:
Enter the size of the array: 5 Enter elements: 1 2 3 4 5 Elements of VLA of Given Size: 1 2 3 4 5
To know more about variable length arrays, please refer to the article –Variable Length Arrays in C/C++.
5. Flexible Array Members
The flexible array members are the array that is defined inside a structure without any dimension and their size is flexible. This feature was introduced in C99 standard.
We can control the size of a flexible member using malloc() function.
There are a few rules to follow in the case of flexible array members:
- The array inside the structure should preferably be declared as the last member of the structure and its size is variable(can be changed at runtime).
- The structure must contain at least one more named member in addition to the flexible array member.
Let’s take the following structure for example
struct student { int len; int };
Now to allocate memory, we will use malloc() function as shown below.
struct student *s = malloc(sizeof(*s) + 5 * sizeof(int));
Example:
C
#include <stdio.h>
#include <stdlib.h>
typedef
struct
{
int
len;
int
arr[];
} fam;
int
main()
{
fam* fam1
= (fam*)
malloc
(
sizeof
(fam*) + 5 *
sizeof
(
int
));
fam* fam2
= (fam*)
malloc
(
sizeof
(fam*) + 10 *
sizeof
(
int
));
for
(
int
i = 0; i < 5; i++) {
fam1->arr[i] = i + 1;
}
for
(
int
i = 0; i < 10; i++) {
fam2->arr[i] = i + 10;
}
printf
(
"Array of Size 5:n"
);
for
(
int
i = 0; i < 5; i++) {
printf
(
"%d, "
, fam1->arr[i]);
}
printf
(
"n"
);
printf
(
"Array of size 10:n"
);
for
(
int
i = 0; i < 10; i++) {
printf
(
"%d, "
, fam2->arr[i]);
}
return
0;
}
Output
Array of Size 5: 1, 2, 3, 4, 5, Array of size 10: 10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
To know more about flexible array members, please refer to the article – Flexible Array Members Structure in C.
Dynamic Allocation of Two-Dimensional Array
We can also create a two-dimensional dynamic array in c. These are the following different ways to create a 2D dynamic array.
- Using a single pointer and a 1D array with pointer arithmetic
- Using an array of pointers
- Using a pointer to a pointer
- Using a double-pointer and one malloc call
- Using a pointer to Variable Length Array
- Using a pointer to the first row of VLA
To know more about the Dynamic Allocation of Two-Dimensional Arrays, refer to the article – How to dynamically allocate a 2D array in C?