Определимся для начала с тем, что такое динамический массив. Со времен Си существуют массивы, но их особенностью был фиксированный размер, который указывался при создании и больше не менялся. Благодаря этому они получили название статических массивов. Очевидно, динамический массив означает, что он может изменять свой размер в ходе работы программы. А также может создаваться тогда, когда количество предполагаемых элементов даже примерно неизвестно, то есть пустым.
Управление динамической памятью
Существует такое понятие, как динамическое управление памятью. Такой подход в программировании позволяет максимально эффективно использовать память компьютера. В C++ этот процесс управляется операциями new и delete. Операция new резервирует память в области динамической памяти или также называемой кучи (free store или heap на англ.). Соответственно, операция delete освобождает резервацию.
По стандартам программирования за динамической памятью нужно следить и своевременно очищать, поэтому операции new и delete часто используются парно. Данный принцип давно устарел. Его корни растут с тех времен, когда операционные системы плохо следили за памятью или просто не умели самостоятельно ее очищать. Теперь ОС всегда очищает память после работы программы. Однако явная очистка памяти является признаком хорошего тона в программировании.
Операция new резервирует память под объект определенного типа и возвращает адрес на эту память. Если выделение памяти по каким-либо причинам невозможно осуществить, то операция вернет нулевой указатель (указатель, который не ссылается ни на что) и выбросит исключение. Оператор new работает с объектами любых типов данных: double, char, int и т. д. Выделение памяти и ее удаление выглядят следующим образом.
int *p = new int; // * - означает, что переменная является указателем. Указатели хранят адреса.
*p = 9;
delete p;
Одномерный массив
Создание одномерного динамического массива происходит так же, как создание переменной в куче.
double *a = new double[10]; // a - указатель на память, выделенную под массив из 10 элементов типа double
a[5] = 2.5;
delete [] a; //внимательно отнеситесь к этой конструкции! Она хитрая!
После оператора delete необходимо указать квадратные скобки, чтобы обозначить для программы предстоящую операцию как освобождение не только указателя на массив, но и сам массив.
Двумерный массив
Создание одномерного динамического массива - тривиальная задача. А если нам нужен многомерный массив…
double **ma = new double *[5]; //шаг 1
for(int I = 0; I < 5; i++) //шаг 2
ma[i] = new double[10];
Таким образом, происходит создание динамического массива в C++ размерностью 5 на 10. Буквально это означает на первом шаге выделение в памяти массива из 5 элементов. А затем, на втором шаге, выделение памяти под массив из 10 элементов и запись адреса на него в предыдущий массив, и так для каждого столбца.
Почему используется указатель второго порядка? Это указатель на указатель. Понять это немного сложно. Хотя код буквально нам говорит, чтобы добраться до какого-либо значения, хранимого в массиве, нам нужно пройти по адресу, получить там еще один адрес и тогда мы выйдем на значение.
Помните о том, что конструкцию освобождения памяти для массива нужно было запомнить? Двумерный массив уничтожается следующим образом.
for(int I = 0; i < 5l i++)
delete [] ma[i];
//кажется, мы что-то забыли…
delete [] ma;
Теперь можете спокойно создать даже пятимерный массив.
Где же «динамика»?
В начале было сказано, что размер динамического массива изменяется в ходе работы программы, но в примерах выше нигде этого явно указано не было. Изменения размерности массива производятся по следующему алгоритму:
- В памяти создается новый массив требуемых размеров.
- Данные старого массива переписываются в новый массив.
- Старый массив уничтожается.
STL vector – новый динамический массив
Для использования векторов необходимо подключить <vector>.
Как известно, стандартная библиотека шаблонов (STL) снабжена набором контейнеров, которые управляют коллекциями элементов. Среди контейнеров есть последовательные контейнеры. Они отличаются главным образом упорядоченностью элементов по времени вставки. Иными словами, первый элемент всегда будет первым, второй всегда вторым и т. д. Есть и другие виды контейнеров – ассоциативные, упорядоченные по значению элементов и неупорядоченные совсем.
Одним из таких последовательных контейнеров является вектор. Он управляет элементами массива C++ в динамической памяти. Доступ к этим элементам осуществляется напрямую по индексу. Благодаря тому, что вектор – последовательный контейнер, добавление и удаление элементов происходит в конце массива, и эти операции производятся очень быстро. Однако вставка нового элемента в середину или начало вектора происходит значительно медленнее, так как для осуществления этой процедуры придется переместить все предыдущие элементы до текущего номера вставки. Рассмотрим пример.
#include <vector>
#include <iostream>
int main() {
std::vector<int> v; //создание пустого вектора для хранения элементов типа int
//определение вектора – шаблон пространства std, поэтому std::
for(int i = 0; i <= 5; ++i) {
v.push_back(i);
}
for(int i = 0; i < v.size(); ++i) {
std::cout << v[i] << ", ";
}
std::cout << std::endl;
system("pause");
}
Как видно из кода, работа с векторами осуществляется точно так же, как с массивами. При этом векторы снабжены полезными дополнительными свойствами, такими как функции С++ для динамических массивов .size() и .push_back(). Строго говоря, данные функции-члены принадлежат контейнеру.
Доступ к элементам вектора
Получение доступа к элементам вектора – это тема, о которой следует поговорить отдельно. Существуют следующие способы обратиться к элементам вектора.
V[index] | Стандартное обращение по индексу |
V.at(index) | Обращение к элементу по индексу, но при выходе за пределы диапазона генерируется исключение |
V.front() | Обращение к первому элементу вектора |
V.back() | Обращение к последнему элементу вектора |
Очевидно, что наиболее верным способом обращения к элементу вектора является вызов функции .at(), так как она генерирует исключение out_of_range, которое можно обработать в блоке try-catch. Операция [] и функции .front() и .back() в случае выхода за пределы допустимого диапазона работают непредсказуемым образом.
Двумерный вектор
Рассмотрим пример создания двумерного динамического массива С++ - простой матрицы 5 на 5.
vector< vector<int> > v(5, vector<int>(5));
v[2][3] = 10;
int a = v[2][3];
v[2].push_back(5); //создание нового элемента в конце вектора
v[2].pop_back(); //удаление последнего элемента
Двумерный вектор представляет собой вектор векторов. И с каждым отдельным вектором можно работать: добавлять и удалять элементы, использовать функции-члены, как показано в примере выше в последней строчке. Также можно использовать итераторы. На первый взгляд конструкция кажется пугающей. Но только на первый. Векторы обладают куда большими возможностями, чем описано в данной статье. С ними подробно можно ознакомиться в документации.
Заключение
В недалеком прошлом программисты вынуждены были создавать динамические массивы самостоятельно, включая все необходимые функции и алгоритмы для работы с ними. Но с появлением STL они могут использовать готовые, продуманные и эффективные классы. Поэтому следует не изобретать велосипед заново, а просто использовать векторы (или другой подходящий контейнер). И только при необходимости работать старыми методами.