Динамические массивы. Операторы new и delete

Указатель в С++ — переменная, которая в себе хранит адрес данных (значения) в памяти, а не сами данные.

Рассмотрев следующие примеры, вы поймете главное — зачем нам нужны в программировании указатели, как их объявлять и применять.

Допустим, в программе нам необходимо создать целочисленный массив, точный размер которого нам не известен до начала работы программы. То есть мы не знаем какое количество чисел понадобится пользователю внести в этот массив. Конечно, мы можем подстраховаться и объявить массив на несколько тысяч элементов (к примеру на 5 000). Этого (по нашему субъективному мнению) должно хватить пользователю для работы. Да — действительно — этого может быть достаточно. Но не будем забывать, что этот массив займет в оперативной памяти много места (5 000 * 4 (тип int) = 20 000 байт). Мы то подстраховались, а пользователь будет заполнять только 10 элементов нашего массива. Получается, что реально 40 байт в работе, а 19 960 байт напрасно занимают память.

#include <iostream>

using namespace std;

 

int main()

{

setlocale(LC_ALL, "rus");

 

const int SizeOfArray = 5000;

int arrWithDigits[SizeOfArray] = {};

cout << "Массив занял в памяти " << sizeof(arrWithDigits) << " байт" << endl;

 

int amount = 0;

cout << "Сколько чисел вы введёте в массив? ";

cin >> amount;

cout << "Реально необходимо " << amount * sizeof(int) << " байт" << endl;

 

for (int i = 0; i < amount; i++)

{

cout << i + 1 << "-е число: ";

cin >> arrWithDigits[i];

}

cout << endl;

 

for (int i = 0; i < amount; i++)

{

cout << arrWithDigits[i] << "  ";

}

cout << endl;

return 0;

}

В стандартную библиотечную функцию sizeof() передаем объявленный массив arrWithDigits строка 10. Она вернёт на место вызова размер в байтах, который занимает этот массив в памяти. На вопрос «Сколько чисел вы введете в массив?» ответим — 10. В строке 15, выражение amount * sizeof(int) станет равнозначным 10 * 4, так как функция sizeof(int) вернет 4 (размер в байтах типа int). Далее введем числа с клавиатуры и программа покажет их на экран. Получается, что остальные 4990 элементов будут хранить нули. Так что нет смысла их показывать.

Главная информация на экране: массив занял 20 000 байт, а реально для него необходимо 40 байт. Как выйти из этой ситуации? Возможно, кому-то захочется переписать программу так, чтобы пользователь с клавиатуры вводил размер массива и уже после ввода значения объявить массив с необходимым количеством элементов. Но это невозможно реализовать без указателей. Как вы помните — размер массива должен быть константой. То есть целочисленная константа должна быть инициализирована до объявления массива и мы не можем запросить её ввод с клавиатуры. Поэкспериментируйте — проверьте.

Тут нам подсвечивает красным оператор >> так как изменять константное значение нельзя.

Тут нас предупреждают о том, что размером массива НЕ может быть значение обычной переменной. Необходимо константное значение!

В следующем коде мы будем использовать указатель и новые для вас операторы new (выделяет память) и delete (освобождает память).

#include <iostream>

#include <clocale>

using namespace std;

 

int main()

{

setlocale(LC_ALL, "rus");

 

int sizeOfArray = 0; // размер массива (введет пользователь)

 

cout << "Чтобы создать массив чисел, введите его размер: ";

cin >> sizeOfArray;

 

// ВНИМАНИЕ! int* arrWithDigits - объявление указателя

// на участок памяти, которую выделит new

int* arrWithDigits = new int [sizeOfArray];

 

for (int i = 0; i < sizeOfArray; i++)

{

arrWithDigits[i] = i + 1;

cout << arrWithDigits[i] << " ";

}

cout << endl;

 

delete [] arrWithDigits; // освобождение памяти

return 0;

}

Пользователь вводит значение с клавиатуры — строка 12. Ниже определён указатель: int* arrWithDigits Эта запись означает, что arrWithDigits — это указатель. Он создан для хранения адреса ячейки, в которой будет находиться целое число. В нашем случае arrWithDigits будет указывать на ячейку массива с индексом 0. Знак * — тот же что применяется при умножении. По контексту компилятор «поймет», что это объявление указателя, а не умножение. Далее следует знак = и оператор new, который выделяет участок памяти. Мы помним, что у нас память должна быть выделена под массив, а не под одно число. Запись new int[sizeOfArray] можно расшифровать так: new (выдели память) int(для хранения целых чисел) [sizeOfArray] (в количестве sizeOfArray).

Таким образом в строке 16 был определён динамический массив. Это значит, что память под него выделится (или не выделится) во время работы программы, а не во время компиляции, как это происходит с обычными массивами. То есть выделение памяти зависит от развития программы и решений, которые принимаются непосредственно в её работе. В нашем случае — зависит от того, что введёт пользователь в переменнуюsizeOfArray

В строке 25 применяется оператор delete. Он освобождает выделенную оператором new память. Так как new выделил память под размещение массива, то и при её освобождении надо дать понять компилятору, что необходимо освободить память массива, а не только его нулевой ячейки, на которую указывает arrWithDigits. Поэтому между delete и именем указателя ставятся квадратные скобки [] — delete [] arrWithDigits; Следует запомнить, что каждый раз, когда выделяется память с помощью new, необходимо эту память освободить используя delete. Конечно, по завершении программы память, занимаемая ей, будет автоматически освобождена. Но пусть для вас станет хорошей привычкой использование операторов new и delete в паре. Ведь в программе могут располагаться 5-6 массивов например. И если вы будете освобождать память, каждый раз, когда она уже не потребуется в дальнейшем в запущенной программе — память будет расходоваться более разумно.

Допустим в нашей программе мы заполнили массив десятью значениями. Далее посчитали их сумму и записали в какую-то переменную. И всё — больше мы с этим массивом работать уже не будем. Программа же продолжает работу и в ней создаются новые динамические массивы для каких-то целей. В этом случае целесообразно освободить память, которую занимает первый массив. Тогда при выделении памяти под остальные массивы эта память может быть использована в программе повторно.

Рассмотрим использование указателей, как параметров функций. Для начала, наберите и откомпилируйте следующий код. В нем функция получает две переменные и предлагает внести изменения в их значения.

#include <iostream>

#include <clocale>

using namespace std;

 

void changeData(int varForCh1, int varForCh2);

 

int main()

{

setlocale(LC_ALL, "rus");

 

int variableForChange_1 = 0;

int variableForChange_2 = 0;

 

cout << "variableForChange_1 = " << variableForChange_1 << endl;

cout << "variableForChange_2 = " << variableForChange_2 << endl;

cout << endl;

 

changeData(variableForChange_1, variableForChange_2);

 

cout << endl;

cout << "variableForChange_1 = " << variableForChange_1 << endl;

cout << "variableForChange_2 = " << variableForChange_2 << endl;

 

return 0;

}

 

void changeData(int varForCh1, int varForCh2)

{

cout << "Введите новое значение первой переменной: ";

cin >> varForCh1;

cout << "Введите новое значение второй переменной: ";

cin >> varForCh2;

}

Запустите программу и введите новые значения переменных. Вы увидите в итоге, что по завершении работы функции, переменные не изменились и равны 0.

Как вы помните, функция работает не на прямую с переменными, а создает их точные копии. Эти копии уничтожаются после выхода из функции. То есть функция получила в виде параметра какую-то переменную, создала её копию, поработала с ней и уничтожила. Сама переменная останется при этом неизменной.

Используя указатели, мы можем передавать в функцию адреса переменных. Тогда функция получит возможность работать непосредственно с данными переменных по адресу. Внесём изменения в предыдущую программу.

#include <iostream>

#include <clocale>

using namespace std;

 

void changeData(int* varForCh1, int* varForCh2);

 

int main()

{

setlocale(LC_ALL, "rus");

 

int variableForChange_1 = 0;

int variableForChange_2 = 0;

 

cout << "variableForChange_1 = " << variableForChange_1 << endl;

cout << "variableForChange_2 = " << variableForChange_2 << endl;

cout << endl;

 

changeData(&variableForChange_1, &variableForChange_2);

 

cout << endl;

cout << "variableForChange_1 = " << variableForChange_1 << endl;

cout << "variableForChange_2 = " << variableForChange_2 << endl;

 

return 0;

}

 

void changeData(int* varForCh1, int* varForCh2)

{

cout << "Введите новое значение первой переменной: ";

cin >> *varForCh1;

cout << "Введите новое значение второй переменной: ";

cin >> *varForCh2;

}

В заголовке (строка 27) и прототипе функции (строка 5), добавляем операцию * перед именами параметров. Это говорит о том, что функция получит адреса, а не значения переменных. При вызове функции из main() добавляем перед именами передаваемых переменных операцию & (амперсанд — Shift + 7). &означает взятие адреса. Вы помните, что указатель хранит адрес. Поэтому мы не можем передать обычное значение, если в заголовке указано, что функция примет указатель. Используя &перед именами переменных, функция получит их адреса.

В теле функции, при вводе значений в переменные, необходимо использовать разыменование указателей. Делается это с помощью всё той же операции * : cin >> *varForCh1; Так мы внесем изменения в значения переменных, а не в адреса. Проверим работу программы:

Всё получилось — значения переменных были изменены в функции.

Такой способ (передача параметров в функцию через указатель) широко использовался в программировании на C. В C++ всё чаще используют передачу параметров в функцию по ссылке. Там отпадает необходимость использовать разыменование * и взятие адреса & переменных. Поэтому использовать передачу параметров по ссылке удобней.