§11 Массивы и указатели. Операции new и delete

Массивы и указатели

Массивы и указатели на самом деле тесно связаны. Имя массива является указателем-константой, значением которой служит адрес первого элемента массива (&arr[0]). Следовательно, имя массива может являться инициализатором указателя к которому будут применимы все правила адресной арифметики, связанной с указателями. Пример программы:
Программа 11.1

#include <iostream>
using namespace std;

int main() {
	const int k = 10;
	int arr[k];
	int *p = arr; // указатель указывает на первый элемент массива
	for (int i = 0; i < 10; i++){
		*p = i;
		p++; // указатель указывает на следующий элемент
	}
	p = arr; // возвращаем указатель на первый элемент
	for (int i = 0; i < 10; i++){
		cout << *p++ << " ";
	}
	cout << endl;
	// аналогично:
	for (int i = 0; i < 10; i++){
		cout << *(arr + i) << " ";
	}
	cout << endl;
	p = arr;
	// выводим адреса элементов:
	for (int i = 0; i < 10; i++){
		cout << "arr[" << i << "] => " << p++ << endl;
	}
	return 0;
}

Вывод программы:

0 1 2 3 4 5 6 7 8 9 
0 1 2 3 4 5 6 7 8 9 
arr[0] => 0xbffc8f00
arr[1] => 0xbffc8f04
arr[2] => 0xbffc8f08
arr[3] => 0xbffc8f0c
arr[4] => 0xbffc8f10
arr[5] => 0xbffc8f14
arr[6] => 0xbffc8f18
arr[7] => 0xbffc8f1c
arr[8] => 0xbffc8f20
arr[9] => 0xbffc8f24

Выражение arr[i] — обращение к элементу по индексу соответствует выражению *(arr + i), которое называется указателем-смещением (строка 22). Это выражение более наглядно иллюстрирует, как C++ на самом деле работает с элементами массива. Переменная-счетчик i указывает на сколько элементов необходимо сместиться от первого элемента. В строке 17 значение элемента массива выводится после разыменования указателя.
pointer
Что означает выражение *p++? Оператор * имеет более низкий приоритет, в тоже время постфиксный инкремент ассоциативен слева-направо. Следовательно, в этом сложном выражении сначала будет выполняться косвенная адресация (получение доступа к значению элемента массива), а затем инкрементация указателя. Иначе это выражение можно было бы представить так: cout << *p << " ", p++;.
Примечание. Оператор sizeof(), применяемый к имени массива, вернет размер всего массива (а не первого элемента).
Примечание. Оператор взятия адреса (&) для элементов массива используется также, как и для обычных переменных (элементы массива иногда называют индексированными переменными). Например, &arr[10]. Поэтому можно всегда получить указатель на любой элемент массива. Однако, операция &arr (где arr - имя массива) вернет адрес всего массива и такая, например, операция (&arr + 1) будет означать шаг размером с массив, т. е. получение указателя на элемент, следующий за последним.

Преимущества использования указателей при работе с элементами массива

Рассмотрим два примера программ приводящих к одинаковому результату: элементам массива присваиваются новые значения от 0 до 1999999 и осуществляется их вывод.
Программа 11.2

#include <iostream>
using namespace std;

int main() {
	const int n = 2000000;
	int mass[n] {};

	for (int i = 0; i < n; i++) {
		mass[i] = i;
		cout << mass[i];
	}
	return 0;
}

Программа 11.3

#include <iostream>
using namespace std;

int main() {
	const int n = 2000000;
	int mass[n] {};

	int *p = mass;
	for (int i = 0; i < n; i++) {
		*p = i;
		cout << *p++;
	}
	return 0;
}

Программа 11.3 будет выполняться быстрее, чем программа 11.2 (с ростом количества элементов эффективность программы 11.3 будет возрастать)! Причина заключается в том, что в программе 11.2 каждый раз пересчитывается местоположение (адрес) текущего элемента массива относительно первого (11.2, строки 12 и 13). В программе 11.3 обращение к адресу первого элемента происходит один раз в момент инициализации указателя (11.3, строка 11).

Выход за границы массива

Отметим еще одну важный аспект работы с С-массивами в С++. В языке С++ отсутствует контроль соблюдения выхода за границы С-массива. Т. о. ответственность за соблюдение режима обработки элементов в пределах границ массива лежит целиком на разработчике алгоритма. Рассмотрим пример.
Программа 11.4

#include <iostream>
#include <ctime>
#include <random>
using namespace std;

int main() {
	int mas[5];
	default_random_engine rnd(time(0));
	uniform_int_distribution<unsigned> d(10, 99);
	for (int i = 0; i < 10; i++)
		mas[i] = d(rnd);
	cout << "Элементы массива:" << endl;
	for (int i = 0; i < 10; i++)
		cout << mas[i] << endl;
	return 0;
}

Программа выведет приблизительно следующее:

Элементы массива:
21
58
38
91
23
5
38
-1219324996
-1074960992
0

В программе 11.4 умышленно допущена ошибка. Но компилятор не сообщит об ошибке: в массиве объявлено пять элементов, а в циклах подразумевается, что элементов 10! В итоге, правильно проинициализированы будут только пять элементов (далее возможно повреждение данных), они же и будут выведены вместе с "мусором". С++ предоставляет возможность контроля границ с помощью библиотечных функций begin() и end() (необходимо подключить заголовочный файл iterator). Модифицируем программу 11.4
Программа 11.5

#include <iostream>
#include <ctime>
#include <random>
#include <iterator>
using namespace std;

int main() {
	int mas[5];
	int *first = begin(mas);
	int *last = end(mas);
	default_random_engine rnd(time(0));
	uniform_int_distribution<unsigned> d(10, 99);
	while(first != last) {
        	*first = d(rnd);
        	first++;
	}
	first = begin(mas);
	cout << "Элементы массива:" << endl;
	while(first != last) {
		cout << *first++ << " ";
	}
	return 0;
}

Функции begin() и end() возвращают итераторы. Понятие итераторов мы раскроем позже, а пока скажем, что они ведут себя как указатели, указывающие на первый элемент (first) и элемент, следующий за последним (last). В программе 11.5 мы, для компактности и удобства, заменили цикл for на while (поскольку счетчик нам уже здесь не нужен - мы используем арифметику указателей). Имея два указателя мы легко можем сформулировать условие выхода из цикла, так как на каждом шаге цикла указатель first инкрементируется.
Еще одним способом сделать обход элементов массива более безопасным основан на применении цикла range-based for, упомянутого нами в теме Инструкция цикла с параметром for (см. программу 9.3)

Операции new и delete

До момента знакомства с указателями вам был известен единственный способ записи изменяемых данных в память посредством переменных. Переменная - это поименованная область памяти. Блоки памяти для соответствующих переменных выделяются в момент запуска программы и используются до прекращения ее работы. С помощью указателей можно создавать неименованные блоки памяти определенного типа и размера (а также освобождать их) в процессе работы самой программы. В этом проявляется замечательная особенность указателей, наиболее полно раскрывающаяся в объектно-ориентированном программировании при создании классов.
Динамическое выделение памяти осуществляется с помощью операции new. Синтаксис:

тип_данных *имя_указателя = new тип_данных;

Например:

int *a = new int;    // Объявление указателя типа int
int *b = new int(5); // Инициализация указателя

Правая часть выражения говорит о том, что new запрашивает блок памяти для хранения данных типа int. Если память будет найдена, то возвращается адрес, который присваивается переменной-указателем, имеющей тип int. Теперь получить доступ к динамически созданной памяти можно только с помощью указателей! Пример работы с динамической памятью показан в программе 3.
Программа 11.6

#include <iostream>
using namespace std;

int main() {
	int *a = new int(5);
	int *b = new int(4);
	int *c = new int;
	*c = *a + *b;
	cout << *c << endl;
	delete a;
	delete b;
	delete c;
	return 0;
}

После выполнения работы с выделенной памятью ее необходимо освободить (вернуть, сделать доступной для других данных) с помощью операции delete. Контроль над расходованием памяти - важная сторона разработки приложений. Ошибки, при которых память не освобождается, приводят к "утечкам памяти", что, в свою очередь, может привести к аварийному завершению программы. Операция delete может применяться к нулевому указателю (nullptr) или созданному с помощью new (т. о. new и delete используются в паре).

Динамические массивы

Динамический массив - это массив, размер которого определяется в процессе работы программы. Строго говоря C-массив не является динамическим в C++. То есть, можно определять только размер массива, а изменение размера массива, в процессе работы программы, по-прежнему невозможно. Для получения массива нужного размера необходимо выделять память под новый массив и копировать в него данные из исходного, а затем освобождать память выделенную ранее под исходный массив. Подлинно динамическим массивом в C++ является тип vector, который мы рассмотрим позднее. Для выделения памяти под массив используется операция new. Синтаксис выделения памяти для массива имеет вид:
указатель = new тип[размер]. Например:

int n = 10;
int *arr = new int[n];

Освобождение памяти производится с помощью оператора delete:

delete [] arr;

При этом размер массива не указывается.
Пример программы. Заполнить динамический целочисленный массив arr1 случайными числами. Показать исходный массив. Переписать в новый динамический целочисленный массив arr2 все элементы с нечетными порядковыми номерами (1, 3, ...). Вывести содержимое массива arr2.
Программа 11.7

#include <iostream>
#include <ctime>
#include <random>
using namespace std;

int main() {
	int n;
	cout << "n = "; cin >> n;
	int *arr1 = new int[n];
	default_random_engine rnd(time(0));
	uniform_int_distribution<unsigned> d(10, 99);
	for (int i = 0; i < n; i++) {
        	arr1[i] = d(rnd);
        	cout << arr1[i] << " ";
	}
	cout << endl;
	int *arr2 = new int[n / 2];
	for (int i = 0; i < n / 2; i++) {
		arr2[i] = arr1[2 * i + 1];
		cout << arr2[i] << " ";
	}
	delete [] arr1;
	delete [] arr2;
	return 0;
}
n = 10
73 94 17 52 11 76 22 70 57 68 
94 52 76 70 68 

Мы знаем, что в C++ двумерный массив представляет собой массив массивов. Следовательно, для создания двумерного динамического массива необходимо выделять память в цикле для каждого входящего массива, предварительно определив количество создаваемых массивов. Для этого используется указатель на указатель, иными словами описание массива указателей:

int **arr = new int *[m];

где m - количество таких массивов (строк двумерного массива).
Пример задачи. Заполнить случайными числами и вывести элементы двумерного динамического массива.
Программа 11.8

#include <iostream>
#include <iomanip>
#include <ctime>
#include <random>
using namespace std;

int main() {
	int n, m;
	default_random_engine rnd(time(0));
	uniform_int_distribution<unsigned> d(10, 99);
	cout << "Введите количество строк:" << endl;
	cout << "m = "; cin >> m;
	cout << "введите количество столбцов:" << endl;
	cout << "n = "; cin >> n;
	int **arr = new int *[m];
	// заполнение массива:
	for (int i = 0; i < m; i++) {
		arr[i] = new int[n];
		for (int j = 0; j < n; j++) {
			arr[i][j] = d(rnd);
		}
	}
	// вывод массива:
	for (int i = 0; i < m; i++) {
		for (int j = 0; j < n; j++) {
			cout << arr[i][j] << setw(3);
		}
		cout << '\n';
	}
	// освобождение памяти выделенной для каждой
	// строки:
	for (int i = 0; i < m; i++)
		delete [] arr[i];
	// освобождение памяти выделенной под массив:
	delete [] arr;
	return 0;
}
Введите количество строк:
m = 5
введите количество столбцов:
n = 10
66 99 17 47 90 70 74 37 97 39  
28 67 60 15 76 64 42 65 87 75  
17 38 40 81 66 36 15 67 82 48  
73 10 47 42 47 90 64 22 79 61  
13 98 28 25 13 94 41 98 21 28
Вопросы
  1. В чем заключается связь указателей и массивов?
  2. Почему использование указателей при переборе элементов массива более эффективно, нежели использование операции обращения по индексу []?
  3. В чем суть понятия "утечка памяти"?
  4. Перечислите способы предупреждения выхода за границы массива?
  5. Что такое динамический массив? Почему в С++ С-массив не является динамическим по существу?
  6. Опишите процесс создания динамического двумерного массива
Презентация к уроку

lesson-11

Домашнее задание

Используя динамические массивы решить следующую задачу: Дан целочисленный массив A размера N. Переписать в новый целочисленный массив B все четные числа из исходного массива (в том же порядке) и вывести размер полученного массива B и его содержимое.

Учебник

§62 (10) §40 (11)

Литература
  1. Лафоре Р. Объектно-ориентированное программирование в C++ (4-е изд.). Питер: 2004
  2. Прата, Стивен. Язык программирования C++. Лекции и упражнения, 6-е изд.: Пер. с англ. — М.: ООО «И.Д. Вильяме», 2012
  3. Липпман Б. Стенли, Жози Лажойе, Барбара Э. Му. Язык программирования С++. Базовый курс. Изд. 5-е. М: ООО "И. Д. Вильямс", 2014
  4. Эллайн А. C++. От ламера до программера. СПб.: Питер, 2015
  5. Шилдт Г. С++: Базовый курс, 3-изд. М.: Вильямс, 2010
  6. C++. Динамические матрицы
  7. Указатели, ссылки и массивы в C и C++: точки над i


Добавить комментарий