Работа с памятью с помощью new и delete. Использование new delete для реализации массивов

Оператор new позволяет выделять память под массивы. Он возвращает

указатель на первый элемент массива в квадратных скобках. При выделении памяти под многомерные массивы все размерности кроме крайней левой должны быть константами. Первая размерность может быть задана переменной, значение которой к моменту использования new известно пользователю, например:

int *p=new int[k]; // ошибка cannot convert from "int (*)" to "int *"

int (*p)=new int[k]; // верно

При выделении памяти под объект его значение будет неопределенным. Однако объекту можно присвоить начальное значение.

int *a = new int (10234);

Этот параметр нельзя использовать для инициализации массивов. Однако

на место инициализирующего значения можно поместить через запятую список

значений, передаваемых конструктору при выделении памяти под массив (мас-

сив новых объектов, заданных пользователем). Память под массив объектов

может быть выделена только в том случае, если у соответствующего класса

имеется конструктор, заданный по умолчанию.

matr(){}; // конструктор по умолчанию

matr(int i,float j): a(i),b(j) {}

{ matr mt(3,.5);

matr *p1=new matr; // верно р1 − указатель на 2 объекта

matr *p2=new matr (2,3.4); // неверно, невозможна инициализация

matr *p3=new matr (2,3.4); // верно р3 – инициализированный объект

{ int i; // компонента-данное класса А

A(){} // конструктор класса А

~A(){} // деструктор класса А

{ A *a,*b; // описание указателей на объект класса А

float *c,*d; // описание указателей на элементы типа float

a=new A; // выделение памяти для одного объекта класса А

b=new A; // выделение памяти для массива объектов класса А

c=new float; // выделение памяти для одного элемента типа float

d=new float; // выделение памяти для массива элементов типа float

delete a; // освобождение памяти, занимаемой одним объектом

delete b; // освобождение памяти, занимаемой массивом объектов

delete c; // освобождение памяти одного элемента типа float

delete d; } // освобождение памяти массива элементов типа float

Организация внешнего доступа к локальным компонентам класса(friend)

Мы уже познакомились с основным правилом ООП – данные (внутренние

переменные) объекта защищены от воздействий извне и доступ к ним можно

получить только с помощью функций (методов) объекта. Но бывают такие слу-

чаи, когда нам необходимо организовать доступ к данным объекта, не исполь-

зуя его интерфейс (функции). Конечно, можно добавить новую public-функцию

к классу для получения прямого доступа к внутренним переменным. Однако в

большинстве случаев интерфейс объекта реализует определенные операции, и

новая функция может оказаться излишней. В то же время иногда возникает не-

обходимость организации прямого доступа к внутренним (локальным) данным

двух разных объектов из одной функции. При этом в С++ одна функция не мо-

жет быть компонентой двух различных классов.

Для реализации этого в С++ введен спецификатор friend. Если некоторая

функция определена как friend-функция для некоторого класса, то она:

Не является компонентой-функцией этого класса;

Имеет доступ ко всем компонентам этого класса (private, public и protected).

Ниже рассматривается пример, когда внешняя функция получает доступ к

внутренним данным класса.

#include

using namespace std;

kls(int i,int J) : i(I),j(J) {} // конструктор

int max() {return i>j? i: j;} // функция-компонента класса kls

friend double fun(int, kls&); // friend-объявление внешней функции fun

double fun(int i, kls &x) // внешняя функция

{ return (double)i/x.i;

cout << obj.max() << endl;

В С(С++) известны три способа передачи данных в функцию: по значе-

можно на некоторый существующий объект. Можно выделить следующие раз-

личия ссылок и указателей. Во-первых, невозможность существования нулевых

ссылок подразумевает, что корректность их не требуется проверять. А при использовании указателя требуется проверять его на ненулевое значение. Во-вторых, указатели могут указывать на различные объекты, а ссылка всегда на один объект, заданный при ее инициализации. Если требуется предоставить возможность функции изменять значения

передаваемых в нее параметров, то в языке С они должны быть объявлены либо

глобально, либо работа с ними в функции осуществляется через передаваемые в

нее указатели на эти переменные. В С++ аргументы в функцию можно переда-

ром ставится знак &.

void fun1(int,int);

void fun2(int &,int &);

{ int i=1,j=2; // i и j – локальные параметры

cout << "\n адрес переменных в main() i = "<<&i<<" j = "<<&j;

cout << "\n i = "<

#include cstddef

// статические члены инициализируются

// в исходных файлах программы, а не в заголовочных файлах

Screen *Screen::freeStore = 0;

const int Screen::screenChunk = 24;

void *Screen::operator new(size_t size)

if (!freeStore) {

// связанный список пуст: получить новый блок

// вызывается глобальный оператор new

size_t chunk = screenChunk * size;

reinterpret_cast Screen* (new char[ chunk ]);

// включить полученный блок в список

p != &freeStore[ screenChunk - 1 ];

freeStore = freeStore-next;

А вот реализация оператора delete():

void Screen::operator delete(void *p, size_t)

// вставить "удаленный" объект назад,

// в список свободных

(static_cast Screen* (p))-next = freeStore;

freeStore = static_cast Screen* (p);

Оператор new() можно объявить в классе и без соответствующего delete(). В таком случае объекты освобождаются с помощью одноименного глобального оператора. Разрешается также объявить и оператор delete() без new(): объекты будут создаваться с помощью одноименного глобального оператора. Однако обычно эти операторы реализуются одновременно, как в примере выше, поскольку разработчику класса, как правило, нужны оба.

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

Выделение памяти с помощью оператора new(), например:

Screen *ptr = new Screen(10, 20);

// Псевдокод на C++

ptr = Screen::operator new(sizeof(Screen));

Screen::Screen(ptr, 10, 20);

Иными словами, сначала вызывается определенный в классе оператор new(), чтобы выделить память для объекта, а затем этот объект инициализируется конструктором. Если new() неудачно завершает работу, то возбуждается исключение типа bad_alloc и конструктор не вызывается.

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

эквивалентно последовательному выполнению таких инструкций:

// Псевдокод на C++

Screen::~Screen(ptr);

Screen::operator delete(ptr, sizeof(*ptr));

Таким образом, при уничтожении объекта сначала вызывается деструктор класса, а затем определенный в классе оператор delete() для освобождения памяти. Если значение ptr равно 0, то ни деструктор, ни delete() не вызываются.

15.8.1. Операторы new и delete

Оператор new(), определенный в предыдущем подразделе, вызывается только при выделении памяти для единичного объекта. Так, в данной инструкции вызывается new() класса Screen:

Screen *ps = new Screen(24, 80);

тогда как ниже вызывается глобальный оператор new() для выделения из хипа памяти под массив объектов типа Screen:

// вызывается Screen::operator new()

Screen *psa = new Screen;

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

Оператор-член new() должен возвращать значение типа void* и принимать в качестве первого параметра значение типа size_t. Вот его объявление для Screen:

void *operator new(size_t);

Когда с помощью new создается массив объектов типа класса, компилятор проверяет, определен ли в классе оператор new(). Если да, то для выделения памяти под массив вызывается именно он, в противном случае – глобальный new(). В следующей инструкции в хипе создается массив из десяти объектов Screen:

Screen *ps = new Screen;

В этом классе есть оператор new(), поэтому он и вызывается для выделения памяти. Его параметр size_t автоматически инициализируется значением, равным объему памяти в байтах, необходимому для размещения десяти объектов Screen.

Даже если в классе имеется оператор-член new(), программист может вызвать для создания массива глобальный new(), воспользовавшись оператором разрешения глобальной области видимости:

Screen *ps = ::new Screen;

Оператор delete(), являющийся членом класса, должен иметь тип void, а в качестве первого параметра принимать void*. Вот как выглядит его объявление для Screen:

void operator delete(void *);

Чтобы удалить массив объектов класса, delete должен вызываться следующим образом:

Когда операндом delete является указатель на объект типа класса, компилятор проверяет, определен ли в этом классе оператор delete(). Если да, то для освобождения памяти вызывается именно он, в противном случае – его глобальная версия. Параметр типа void* автоматически инициализируется значением адреса начала области памяти, в которой размещен массив.

Даже если в классе имеется оператор-член delete(), программист может вызвать глобальный delete(), воспользовавшись оператором разрешения глобальной области видимости:

Добавление операторов new() или delete() в класс или удаление их оттуда не отражаются на пользовательском коде: вызовы как глобальных операторов, так и операторов-членов выглядят одинаково.

При создании массива сначала вызывается new() для выделения необходимой памяти, а затем каждый элемент инициализируется с помощью конструктора по умолчанию. Если у класса есть хотя бы один конструктор, но нет конструктора по умолчанию, то вызов оператора new() считается ошибкой. Не существует синтаксической конструкции для задания инициализаторов элементов массива или аргументов конструктора класса при создании массива подобным образом.

При уничтожении массива сначала вызывается деструктор класса для уничтожения элементов, а затем оператор delete() – для освобождения всей памяти. При этом важно использовать правильный синтаксис. Если в инструкции

ps указывает на массив объектов класса, то отсутствие квадратных скобок приведет к вызову деструктора лишь для первого элемента, хотя память будет освобождена полностью.

У оператора-члена delete() может быть не один, а два параметра, при этом второй должен иметь тип size_t:

// заменяет

// void operator delete(void*);

void operator delete(void*, size_t);

Если второй параметр присутствует, то компилятор автоматически инициализирует его значением, равным объему отведенной под массив памяти в байтах.

Из книги Справочное руководство по C++ автора Страустрап Бьярн

R.5.3.4 Операция delete Операция delete уничтожает объект, созданный с помощью new.выражение-освобождения: ::opt delete выражение-приведения::opt delete выражение-приведенияРезультат имеет тип void. Операндом delete должен быть указатель, который возвращает new. Эффект применения операции delete

Из книги Microsoft Visual C++ и MFC. Программирование для Windows 95 и Windows NT автора Фролов Александр Вячеславович

Операторы new и delete Оператор new создает объект заданного типа. При этом он выделяет память, необходимую для хранения объекта и возвращает указатель, указывающий на него. Если по каким-либо причинам получить память не удается, оператор возвращает нулевое значение. Оператор

Из книги Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ автора Мейерс Скотт

Правило 16: Используйте одинаковые формы new и delete Что неправильно в следующем фрагменте?std::string *stringArray = new std::string;...delete stringArray;На первый взгляд, все в полном порядке – использованию new соответствует применение delete, но кое-что здесь совершенно неверно. Поведение программы

Из книги Windows Script Host для Windows 2000/XP автора Попов Андрей Владимирович

Глава 8 Настройка new и delete В наши дни, когда вычислительные среды снабжены встроенной поддержкой «сборки мусора» (как, например, Java и. NET), ручной подход C++ к управлению памятью может показаться несколько устаревшим. Однако многие разработчики, создающие требовательные к

Из книги Стандарты программирования на С++. 101 правило и рекомендация автора Александреску Андрей

Метод Delete Если параметр force равен false или не указан, то с помощью метода Delete будет нельзя удалить каталог с атрибутом "только для чтения" (read-only). Установка для force значения true позволит сразу удалять такие каталоги.При использовании метода Delete неважно, является ли заданный

Из книги Справочник по Flash автора Коллектив авторов

Метод Delete Если параметр force равен false или не указан, то с помощью метода Delete будет нельзя удалить файл с атрибутом "только для чтения" (read-only). Установка для force значения true позволит сразу удалять такие файлы. Замечание Вместо метода Delete можно использовать метод DeleteFile

Из книги Firebird РУКОВОДСТВО РАЗРАБОТЧИКА БАЗ ДАННЫХ автора Борри Хелен

Операторы отношения и логические операторы Операторы отношения используются для сравнения значений двух переменных. Эти операторы, описанные в табл. П2.11, могут возвращать только логические значения true или false.Таблица П2.11. Операторы отношения Оператор Условие, при

Из книги Linux и UNIX: программирование в shell. Руководство разработчика. автора Тейнсли Дэвид

45. new и delete всегда должны разрабатываться вместе РезюмеКаждая перегрузка void* operator new(parms) в классе должна сопровождаться соответствующей перегрузкой оператора void operator delete(void* , parms), где parms - список типов дополнительных параметров (первый из которых всегда std::size_t). То же

Из книги Справка по SQL автора

delete - Удаление объекта, элемента массива или переменной delete(Оператор)Этот оператор используется для удаления из сценария объекта, свойства объекта, элемента массива или переменных.Синтаксис:delete identifier;Аргументы:Описание:Оператор delete уничтожает объект или переменную, имя

Из книги Понимание SQL автора Грубер Мартин

Оператор DELETE Запрос DELETE используется для удаления целых строк таблицы. SQL не дает возможности одному оператору DELETE удалять строки более чем из одной таблицы. Запрос DELETE, который изменяет только одну текущую строку курсора, называется позиционированным удалением.

Из книги автора

15.8. Операторы new и delete По умолчанию выделение объекта класса из хипа и освобождение занятой им памяти выполняются с помощью глобальных операторов new() и delete(), определенных в стандартной библиотеке C++. (Мы рассматривали эти операторы в разделе 8.4.) Но класс может реализовать

Из книги автора

15.8.1. Операторы new и delete Оператор new(), определенный в предыдущем подразделе, вызывается только при выделении памяти для единичного объекта. Так, в данной инструкции вызывается new() класса Screen:// вызывается Screen::operator new()Screen *ps = new Screen(24, 80);тогда как ниже вызывается

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

#include 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 => 0xbffc8f00 arr => 0xbffc8f04 arr => 0xbffc8f08 arr => 0xbffc8f0c arr => 0xbffc8f10 arr => 0xbffc8f14 arr => 0xbffc8f18 arr => 0xbffc8f1c arr => 0xbffc8f20 arr => 0xbffc8f24

Выражение arr[i] – обращение к элементу по индексу соответствует выражению *(arr + i) , которое называется указателем-смещением (строка 22). Это выражение более наглядно иллюстрирует, как C++ на самом деле работает с элементами массива. Переменная-счетчик i указывает на сколько элементов необходимо сместиться от первого элемента . В строке 17 значение элемента массива выводится после разыменования указателя.

Что означает выражение *p++ ? Оператор * имеет более низкий приоритет, в тоже время постфиксный инкремент ассоциативен слева-направо. Следовательно, в этом сложном выражении сначала будет выполняться косвенная адресация (получение доступа к значению элемента массива), а затем инкрементация указателя. Иначе это выражение можно было бы представить так: cout Примечание . Оператор sizeof() , применяемый к имени массива, вернет размер всего массива (а не первого элемента).
Примечание . Оператор взятия адреса (&) для элементов массива используется также, как и для обычных переменных (элементы массива иногда называют индексированными переменными). Например, &arr . Поэтому можно всегда получить указатель на любой элемент массива. Однако, операция &arr (где arr - имя массива) вернет адрес всего массива и такая, например, операция (&arr + 1) будет означать шаг размером с массив, т. е. получение указателя на элемент, следующий за последним.

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

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

#include 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 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 #include #include using namespace std; int main() { int mas; default_random_engine rnd(time(0)); uniform_int_distribution < 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 #include #include #include using namespace std; int main() { int mas; int *first = begin(mas); int *last = end(mas); default_random_engine rnd(time(0)); uniform_int_distribution 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 , упомянутого нами в теме ()

Операции new и delete

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

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

Например:

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

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

#include 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++ является тип , который мы рассмотрим позднее. Для выделения памяти под массив используется операция new . Синтаксис выделения памяти для массива имеет вид:
указатель = new тип[размер] . Например:

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

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

Delete arr;

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

#include #include #include 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 d(10, 99); for (int i = 0; i < n; i++) { arr1[i] = d(rnd); cout << arr1[i] << " "; } cout << endl; int *arr2 = new int; for (int i = 0; i < n / 2; i++) { arr2[i] = arr1; 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 #include #include #include using namespace std; int main() { int n, m; default_random_engine rnd(time(0)); uniform_int_distribution 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. Опишите процесс создания динамического двумерного массива
Презентация к уроку
Домашнее задание

Используя динамические массивы решить следующую задачу: Дан целочисленный массив 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

Как известно, в языке С для динамического выделения и освобождения памяти используются фун­кции malloc() и free(). Вместе с тем С++ содержит два оператора, выполняющих выделение и освобождение памяти более эффективно и более просто. Этими операторами являются new и delete. Их общая форма имеет вид:

переменная_указатель = new тип_переменной;

delete переменная_указатель;

Здесь переменная_указaтель является указателем типа тип_переменной. Оператор new выделяет память для хранения значения типа тип_переменной и возвращает ее адрес. С помощью new могут быть размещены любые типы данных. Оператор delete освобождает память, на которую указывает указатель переменная_указатель.

Если операция выделения памяти не может быть выполнена, то оператор new генерирует ис­ключение типа xalloc. Если программа не перехватит это исключение, тогда она будет снята с выполнения. Хотя для коротких программ такое поведение по умолчанию является удовлетвори­тельным, для реальных прикладных программ обычно требуется перехватить исключение и обра­ботать его соответствующим образом. Для того чтобы отследить это исключение, необходимо вклю­чить заголовочный файл except.h.

Оператор delete следует использовать только для указателей на память, выделенную с исполь­зованием оператора new. Использование оператора delete с другими типами адресов может по­родить серьезные проблемы.

Есть ряд преимуществ использования new перед использованием malloc(). Во-первых, оператор new автоматически вычисляет размер необходимой памяти. Нет необходимости в использовании оператора sizeof(). Более важно то, что он предотвращает случайное выделение неправильного количества памяти. Во-вторых, оператор new автоматически возвращает указатель требуемого типа, так что нет необходимости в использовании оператора преобразования типа. В-третьих, как ско­ро будет описано, имеется возможность инициализации объекта при использовании оператора new. И наконец, имеется возможность перегрузить оператор new и оператор delete глобально или по отношению к тому классу, который создается.

Ниже приведен простой пример использования операторов new и delete. Следует обратить вни­мание на использование блока try/catch для отслеживания ошибок выделения памяти.

#include
#include
int main()
{
int *p;
try {
p = new int; // выделение памяти для int
} catch (xalloc xa) {
cout << "Allocation failure.\n";
return 1;
}
*p = 20; // присвоение данному участку памяти значения 20
cout << *р; // демонстрация работы путем вывода значения
delete р; // освобождение памяти
return 0;
}

Эта программа присваивает переменной р адрес блока памяти, имеющего достаточный размер для того, чтобы содержать число целого типа. Далее этой памяти присваивается значение и содер­жимое памяти выводится на экран. Наконец, динамически выделенная память освобождается.

Как отмечалось, можно инициализировать память с использованием оператора new. Для этого надо указать инициализирующее значение в скобках после имени типа. Например, в следующем примере память, на которую указывает указатель р, инициализируется значением 99:

#include
#include
int main()
{
int *p;
try {
p = new int (99); // инициализация 99-ю
} catch (xalloc xa) {
cout << "Allocation failure.\n";
return 1;
}
cout << *p;
delete p;
return 0;
}

С помощью new можно размещать массивы. Общая форма для одномерного массива имеет вид:

переменная_указатель = new тип_переменной [размер];

Здесь размер определяет число элементов в массиве. Необходимо запомнить важное ограничение при размещении массива: его нельзя инициализировать.

Для освобождения динамически размещенного массива необходимо использовать следующую форму оператора delete:

delete переменная_указатель;

Здесь скобки информируют оператор delete, что необходимо освободить память, выделенную для массива.

В следующей программе выделяется память для массива из 10 элементов типа float. Элементам массива присваиваются значения от 100 до 109, а затем содержимое массива выводится на экран:

#include
#include
int main()
{
float *p;
int i;
try {
p = new float ; // получение десятого элемента массива
} catch(xalloc xa) {
cout << "Allocation failure.\n";
return 1;
}
// присвоение значений от 100 до 109
for (i=0; i<10; i + +) p[i] = 100.00 + i;
// вывод содержимого массива
for (i=0; i<10; i++) cout << p[i] << " ";
delete p; // удаление всего массива
return 0;
}

С++ поддерживает три основных типа выделения (распределения ) памяти , с двумя из которых мы уже знакомы:

Статическое выделение памяти выполняется для и переменных. Память выделяется один раз, при запуске программы, и сохраняется на протяжении работы всей программы.

Автоматическое выделение памяти выполняется для и . Память выделяется при входе в блок, в котором находятся эти переменные, и освобождается при выходе из него.

является темой этой статьи.

Как статическое, так и автоматическое распределение памяти имеют две общие черты:

Размер переменной/массива должен быть известен во время компиляции.

Выделение и освобождение памяти происходит автоматически (когда переменная создается или уничтожается).

В большинстве случаев с этим всё ОК. Однако когда дело доходит до работы с внешним вводом, то эти ограничения могут привести к проблемам.

Например, при использовании для хранения имени мы не знаем наперед насколько длинным оно будет, пока пользователь его не введет. Или когда нам нужно записать количество записей с диска в переменную, но мы не знаем заранее, сколько этих записей есть. Или мы можем создать игру с непостоянным количеством монстров (во время игры одни монстры умирают, другие рождаются), пытаясь, таким образом, убить игрока.

Если нам нужно объявить размер всех переменных во время компиляции, то самое лучшее, что мы можем сделать – это попытаться угадать их максимальный размер, надеясь, что этого будет достаточно:

char name; // будем надеяться, что пользователь введет имя менее 30 символов! Record record; // будем надеяться, что количество записей будет не больше 400! Monster monster; // 30 монстров максимум Polygon rendering; // этому 3d rendering лучше состоять из менее чем 40,000 полигонов!

Это плохое решение, по крайней мере, по трем причинам:

Во-первых, теряется память, если переменные фактически не используются или используются, но не на полную. Например, если мы выделим 30 символов для каждого имени, но имена в среднем будут занимать по 15 символов, то потребление памяти получится в два раза больше, чем нужно на самом деле. Или рассмотрим массив rendering: если он использует только 20 000 полигонов, то память с 20 000 полигонами фактически тратится впустую (т.е. не используется)!

Во-вторых, память для большинства обычных переменных (включая фиксированные массивы) выделяется из специального резервуара памяти — стека . Объем памяти стека в программе, как правило, невелик – в Visual Studio он по умолчанию равен 1МБ. Если вы превысите это число, то произойдет переполнение стека , и операционная система автоматически завершит выполнение вашей программы.

В Visual Studio это можно проверить, запустив следующую программу:

int main() { int array; // выделяем 1 миллион целочисленных значений }

Лимит в 1МБ памяти может быть проблематичным для многих программ, особенно где используется графика.

В-третьих, и самое главное, это может привести к искусственным ограничениям и/или переполнению массива. Что произойдет, если пользователь попытается прочесть 500 записей с диска, но мы выделили память максимум для 400? Либо мы выведем пользователю ошибку, что максимальное количество записей — 400, либо (в худшем случае) выполнится переполнение массива и затем что-то очень нехорошее.

К счастью, эти проблемы легко устраняются с помощью динамического выделения памяти. Динамическое выделение памяти — это способ запроса памяти из операционной системы запущенными программами при необходимости. Эта память не выделяется из ограниченной памяти стека программы, а из гораздо большего хранилища, управляемого операционной системой — heap (кучи ) . На современных компьютерах размер кучи может составлять гигабайты памяти.

Динамическое выделение переменных

Для динамического выделения памяти для одной переменной используется оператор new :

new int; // динамически выделяем целочисленную переменную и сразу же отбрасываем результат (так как нигде его не сохраняем)

В примере выше мы запрашиваем выделение памяти для целочисленной переменной из операционной системы. Оператор new возвращает , содержащий адрес выделенной памяти.

Для доступа к выделенной памяти создается указатель:

int *ptr = new int; // динамически выделяем целочисленную переменную и присваиваем её адрес ptr, чтобы потом иметь возможность доступа к ней

Затем мы можем разыменовать указатель для получения значения:

*ptr = 8; // присваиваем значение 8 только что выделенной памяти

Вот один из случаев, когда указатели полезны. Без указателя с адресом на только что выделенную память, у нас бы не было способа получить доступ к ней.

Как работает динамическое выделение памяти?

На вашем компьютере имеется память (возможно, большая ее часть), которая доступна для использования приложениями. При запуске приложения ваша операционная система загружает это приложение в некоторую часть этой памяти. И эта память, используемая вашим приложением, разделена на несколько частей, каждая из которых выполняет определенную задачу. Одна часть содержит ваш код, другая используется для выполнения обычных операций (отслеживание вызываемых функций, создание и уничтожение глобальных и локальных переменных и т. д.). Мы поговорим об этом позже. Тем не менее, большая часть доступной памяти просто находится там, ожидая запросов на выделение от программ.

Когда вы динамически выделяете память, вы просите операционную систему зарезервировать часть этой памяти для использования вашей программой. Если ОС может выполнить этот запрос, то возвращается адрес этой памяти обратно, в ваше приложение. С этого момента и в дальнейшем ваше приложение может использовать эту память, как только пожелает. Когда вы уже выполнили всё, что было необходимо, с этой памятью, то её нужно вернуть обратно в операционную систему, для распределения между другими запросами.

В отличие от статического или автоматического выделения памяти, сама программа отвечает за запрос и обратный возврат динамически выделенной памяти.

Инициализация динамически выделенных переменных

Когда вы динамически выделяете переменную, то вы также можете её инициализировать посредством или uniform инициализации (в С++11):

int *ptr1 = new int (7); // используем прямую инициализацию int *ptr2 = new int { 8 }; // используем uniform инициализацию

Удаление переменных

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

// предположим, что ptr ранее уже был выделен с помощью оператора new delete ptr; // возвращаем память, на которую указывал ptr, обратно в операционную систему ptr = 0; // делаем ptr нулевым указателем (используйте nullptr вместо 0 в C++11)

Что означает «удаление памяти»?

Оператор delete на самом деле ничего не удаляет. Он просто возвращает память, которая была выделена ранее, обратно в операционную систему. Затем операционная система может переназначить эту память другому приложению (или этому же снова).

Хотя может показаться, что мы удаляем переменную , но это не так! Переменная-указатель по-прежнему имеет ту же область видимости, что и раньше, и ей можно присвоить новое значение, как и любой другой переменной.

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

Висячие указатели

C++ не дает никаких гарантий относительно того, что произойдет с содержимым освобожденной памяти, или со значением удаляемого указателя. В большинстве случаев память, возвращаемая операционной системе, будет содержать те же значения, которые были у нее до освобождения , а указатель так и останется указывать на память, только уже освобожденную (удаленную).

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

#include int main() { int *ptr = new int; *ptr = 8; // помещаем значение в выделенную ячейку памяти delete ptr; // возвращаем память обратно в операционную систему. ptr теперь уже висячий указатель std::cout << *ptr; // разыменование висячого указателя приведет к неожиданным результатам delete ptr; // попытка освободить память снова приведет к неожиданным результатам также return 0; }

#include

int main ()

int * ptr = new int ; // динамически выделяем целочисленную переменную

* ptr = 8 ; // помещаем значение в выделенную ячейку памяти

delete ptr ; // возвращаем память обратно в операционную систему. ptr теперь уже висячий указатель

std :: cout << * ptr ; // разыменование висячого указателя приведет к неожиданным результатам

delete ptr ; // попытка освободить память снова приведет к неожиданным результатам также

return 0 ;

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

Процесс освобождения памяти может также привести к созданию нескольких висячих указателей. Рассмотрим следующий пример:

#include int main() { int *ptr = new int; // динамически выделяем целочисленную переменную int *otherPtr = ptr; // otherPtr теперь указывает на ту же самую выделенную память, что и ptr delete ptr; // возвращаем память обратно в операционную систему. ptr и otherPtr теперь висячие указатели ptr = 0; // ptr теперь уже nullptr // однако otherPtr по-прежнему является висячим указателем! return 0; }

#include

int main ()

int * ptr = new int ; // динамически выделяем целочисленную переменную

int * otherPtr = ptr ; // otherPtr теперь указывает на ту же самую выделенную память, что и ptr

delete ptr ; // возвращаем память обратно в операционную систему. ptr и otherPtr теперь висячие указатели

ptr = 0 ; // ptr теперь уже nullptr

// однако otherPtr по-прежнему является висячим указателем!

return 0 ;

Во-первых, старайтесь избегать ситуаций, когда несколько указателей указывают на одну и ту же часть выделенной памяти. Если это невозможно, то проясните, какой указатель из всех «владеет» памятью (и отвечает за ее удаление), а какие указатели просто получают доступ к ней.

Во-вторых, когда вы удаляете указатель, и если он не выходит из сразу же после удаления, то его нужно сделать нулевым, т.е. задать значение 0 (или в С++11). Под «выходом из области видимости сразу же после удаления» имеется в виду, что вы удаляете указатель в самом конце блока, в котором он объявлен.

Правило: Присваивайте удаленным указателям значение 0 (или nullptr в C++11), если они не выходят из области видимости сразу же после удаления.

Работа оператора new

При запросе памяти из операционной системы в редких случаях она может быть не доступной (т.е. её может и не быть в наличии).

По умолчанию, если new не сработал, память не выделилась, то генерируется исключение bad_alloc . Если это исключение будет неправильно обрабатываться (а именно так и будет, поскольку мы еще не рассмотрели исключения и их обработку), то программа просто прекратит своё выполнение (произойдет сбой) с необработанной ошибкой исключения.

Во многих случаях процесс генерации исключения оператором new (как и сбой программы) нежелателен, поэтому есть альтернативная форма new, которая возвращает нулевой указатель, если память не может быть выделена. Нужно просто добавить константу std::nothrow между ключевым словом new и типом выделения данных:

int *value = new (std::nothrow) int; // указатель value станет нулевым, если динамическое выделение целочисленной переменной не выполнится

В примере выше, если new не возвратит указатель с динамически выделенной памятью, то возвратится нулевой указатель.

Разыменовывать его также не рекомендуется, так как это приведет к неожиданным результатам (скорее всего, к сбою в программе). Поэтому, наилучшей практикой является проверка всех запросов на выделение памяти, для обеспечения того, что эти запросы выполняться успешно и память будет выделена.

int *value = new (std::nothrow) int; // запрос на выделение динамической памяти для целочисленного значения if (!value) // обрабатываем случай, когда new возвращает null (т.е. память не выделяется) { // обработка этого случая std::cout << "Could not allocate memory"; }

Поскольку не выделение памяти оператором new происходит крайне редко, то обычно программисты забывают выполнять эту проверку!

Нулевые указатели и динамическое выделение памяти

Нулевые указатели (указатели со значением 0 или nullptr) особенно полезны в процессе выделения динамической памяти. Их наличие как бы говорит: «этому указателю не выделено никакой памяти». А это в свою очередь можно использовать для выполнения условного выделения памяти:

// если ptr-у до сих пор не выделено памяти, выделяем её if (!ptr) ptr = new int;

Удаление нулевого указателя ни на что не влияет. Таким образом, в следующем нет необходимости:

if (ptr) delete ptr;

if (ptr )

delete ptr ;

Вместо этого вы можете просто написать:

delete ptr ;

Если ptr не является нулевым, то динамически выделенная переменная будет удалена. Если значением указателя является нуль, то ничего не произойдет.

Утечка памяти

Динамически выделенная память не имеет области видимости. То есть она остается выделенной до тех пор, пока не будет явно освобождена или пока ваша программа не завершится (и операционная система очистит все буфера памяти самостоятельно). Однако указатели, используемые для хранения динамически выделенных адресов памяти, следуют правилам области видимости нормальных переменных. Это несоответствие может вызвать интересное поведение.

Рассмотрим следующую функцию:

void doSomething() { int *ptr = new int; }