Всё ли мы знаем об операторах new и delete? Использование new delete для реализации массивов

  • 20.04.2019

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

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

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

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

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

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

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

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

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

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

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; }

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

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

Определим операторы new() и delete() в нашем классе Screen.

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

void *operator new(size_t);

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

Screen *ps = new Screen;

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

Добавление оператора new() в класс или его удаление оттуда не отражаются на пользовательском коде. Вызов new выглядит одинаково как для глобального оператора, так и для оператора-члена. Если бы в классе Screen не было собственного new(), то обращение осталось бы правильным, только вместо оператора-члена вызывался бы глобальный оператор.

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

Screen *ps = ::new Screen;

void operator delete(void *);

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

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

С помощью оператора разрешения глобальной области видимости можно вызвать глобальный delete(), даже если в Screen определена собственная версия:

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

Оператор delete(), определенный для типа класса, может содержать два параметра вместо одного. Первый параметр по-прежнему должен иметь тип void*, а второй – предопределенный тип size_t (не забудьте включить заголовочный файл):

// заменяет

// void operator delete(void *);

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

Рассмотрим реализацию операторов new() и delete() в классе Screen более детально. В основе нашей стратегии распределения памяти будет лежать связанный список объектов Screen, на начало которого указывает член freeStore. При каждом обращении к оператору-члену new() возвращается следующий объект из списка. При вызове delete() объект возвращается в список. Если при создании нового объекта список, адресованный freeStore, пуст, то вызывается глобальный оператор new(), чтобы получить блок памяти, достаточный для хранения screenChunk объектов класса Screen.

Как screenChunk, так и freeStore представляют интерес только для Screen, поэтому мы сделаем их закрытыми членами. Кроме того, для всех создаваемых объектов нашего класса значения этих членов должны быть одинаковыми, а следовательно, нужно объявить их статическими. Чтобы поддержать структуру связанного списка объектов Screen, нам понадобится третий член next:

void *operator new(size_t);

void operator delete(void *, size_t);

static Screen *freeStore;

static const int screenChunk;

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

#include "Screen.h"

#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);тогда как ниже вызывается

Автоматические объекты удаляются неявно в соответствии с чёткими правилами, которые реализованы в компиляторе. Локальные переменные функции удаляются, когда поток управления покидает область видимости, в которой они объявлены. Члены класса удаляются после выполнения деструктора этого класса.

А вот для динамических объектов таких правил нет. Их нужно всегда удалять явно (явное удаление может быть скрыто в недрах утилитарных классов и функций). Вот небольшая иллюстрация для лучшего понимания:
struct A { std::string str; // Автоматический объект, неявно удаляется в деструкторе A (который сгенерирован // автоматически). Сам строковый буфер - динамический объект (*), будет явно // удалён в деструкторе std::string, который будет неявно вызван в деструкторе A. // (*) Если только строка не слишком короткая, тогда сработает Small String Optimization и динамический // буфер вообще не будет выделен. }; void foo() { std::vector v; // Автоматический объект, неявно удаляется при выходе из функции. v.push_back(10); // Содержимое вектора - динамический объект (массив), будет явно удалён в деструкторе // вектора, который будет неявно вызван при выходе из функции. A a; // Автоматический объект класса А, неявно удаляется при выходе из функции. A* pa = new A; // Указатель pa - автоматический объект, неявно удаляется при выходе из функции, // но он указывает на динамический объект класса А, который нужно удалить в явном виде. delete pa; // Явное удаление динамического объекта. auto upa = // Умный указатель upa - автоматический объект, неявно удаляется при выходе из функции, std::make_unique(); // но он указывает на динамический объект класса А, который будет явно удалён // в деструкторе умного указателя. }
Обычно динамические объекты находятся в куче, хотя в общем случае это не так. Автоматические объекты могут находиться как на стеке, так и в куче. В примере выше автоматический объект upa->str находится в куче, т.к. он - часть динамического объекта *upa . Т.е. свойства динамический/автоматический определяют время жизни, но не место жизни объекта.

Свойство динамический/автоматический принадлежит именно объекту, а не типу, т.к. объекты одного и того же типа могут быть как динамическими, так и автоматическими ). В примере выше объекты a и *pa оба имеют тип А, но первый является автоматическим, а второй - динамическим.

Динамические объекты в С++ создаются с помощью new , а удаляются с помощью delete . Вот отсюда и все проблемы: никто не говорил, что эти конструкции следует использовать напрямую! Это низкоуровневые вызовы, они как бы под капотом. И не нужно лезть под капот без необходимости.

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

* Существуют техники, чтобы ограничить свойство динамический/автоматический на уровне типа. Например, закрытые конструкторы.

В чём проблема с new и delete ?

С самого момента своего изобретения операторы new и delete используются неоправданно часто. Самые большие проблемы относятся к оператору delete:
  • Можно вообще забыть вызвать delete (утечка памяти, memory leak).
  • Можно забыть вызвать delete в случае исключения или досрочного возврата из функции (тоже утечка памяти).
  • Можно вызвать delete дважды (двойное удаление, double delete).
  • Можно вызвать не ту форму оператора: delete вместо delete или наоборот (неопределённое поведение, undefined behavior).
  • Можно использовать объект после вызова delete (dangling pointer).
Все эти ситуации приводят в лучшем случае к падениям программы, а в худшем к утечкам памяти и назальным демонам .

Поэтому люди давно сообразили прятать оператор delete в недрах контейнеров и умных указателей, убрав тем самым его из клиентского кода. Однако с оператором new тоже связаны проблемы, но для них решения появились не сразу, и, по факту, многие разработчики до сих пор стесняются этими решениями пользоваться. Об этом мы подробнее поговорим, когда дойдём до make -функций.

Теперь перейдём к сценариям использования new и delete . Напомню, что мы рассмотрим несколько сценариев и планомерно покажем, что в большинстве из них код станет лучше, если отказаться от использования new и delete .

Начнём с простого - с динамических массивов.

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

Динамический массив - это массив с элементами, выделенными в динамической памяти. Он необходим в случае, если размер неизвестен на этапе компиляции, или если размер достаточно большой, и мы не хотим выделять массив на стеке, размер которого обычно сильно ограничен.

Для выделения динамических массивов С++ на низком уровне предоставляет векторную форму операторов new и delete: new и delete . В качестве примера рассмотрим некоторую функцию, которая работает с внешним буфером:
void DoWork(int* buffer, size_t bufSize);
Подобные функции часто встречаются в библиотеках с API на чистом С . Ниже приведён пример, как может выглядеть использующий её код. Это плохой код, т.к. он в явном виде использует delete , а связанные с ним проблемы мы уже описали выше.
void Call(size_t n) { int* p = new int[n]; DoWork(p, n); delete p; // Плохо! }
Тут всё просто и большинству известно, что для подобных целей в С++ следует использовать стандартный контейнер std::vector . Он сам выделит память в конструкторе и освободит её в деструкторе. К тому же, он ещё может менять свой размер во время жизни, но для нас это сейчас значения не имеет. С использованием вектора код будет выглядеть так:
void Call(size_t n) { std::vector v(n); // Лучше. DoWork(v.data(), v.size()); }
Тем самым мы решаем все проблемы, связанные с вызовом delete , и к тому же вместо безликой пары указатель+число, имеем явный контейнер с удобным интерфейсом.

При этом никаких new и delete . Не буду более подробно останавливаться на этом сценарии. По моему опыту большинство разработчиков и так знает, что следует делать в данном случае и почему.

* На С++ подобный интерфейс следовало бы реализовать с использованием типа span . Он предоставляет унифицированный STL-совместимый интерфейс для доступа к непрерывным последовательностям элементов, при этом никак не влияя на их время жизни (невладеющая семантика).

** Поскольку эту статью читают программисты на С++, я почти уверен, что кто-то подумает: «Ха! std::vector хранит в себе целых три (!) указателя, когда старый добрый int* - это по определению всего один указатель. Налицо перерасход памяти и нескольких машинных инструкций на их инициализацию! Это неприемлемо!». Майерс отлично прокомментировал это свойство программистов на С++ в своём докладе Why C++ Sails When the Vasa Sank . Если для вас это действительно проблема, то могу порекомендовать std::unique_ptr , а в будущем стандарт может подарить нам dynarray .

Динамические объекты

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

Когда динамический объект создан, кто-то должен его удалить, и условно типы объектов можно разделить на две группы: те, которые никак не осведомлены о процессе своего удаления, и те, которые что-то подозревают. Будем говорить, что первые имеют стандартную модель управления памятью, а вторые - нестандартную.

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

К типам с нестандартной моделью управления памятью можно отнести, например, объекты Qt. Здесь у каждого объекта есть родитель, который ответственен за его удаление. И объект об этом знает, т.к. он наследуется от класса QObject . Сюда же относятся типы со счётчиком ссылок, например, рассчитанные на работу с boost::intrusive_ptr .

Иными словами, тип со стандартной моделью управления памятью не предоставляет никаких дополнительных механизмов для управления своим временем жизни. Этим целиком и полностью должна заниматься пользовательская сторона. А вот тип с нестандартной моделью такие механизмы предоставляет. Например, QObject имеет методы setParent() и children() и содержит в себе список детей, а тип boost::intrusive_ptr опирается на функции intrusive_ptr_add_ref и intrusive_ptr_release и содержит в себе счётчик ссылок.

Если тип объекта имеет стандартную модель управления памятью, то будем для краткости говорить, что это объект со стандартным управлением памятью. Аналогично, если тип объекта имеет нестандартную модель управления памятью, то будем говорить, что это объект с нестандартным управлением памятью.

Далее рассмотрим объекты обеих моделей. Забегая вперёд, стоит сказать, что для объектов со стандартным управлением памятью однозначно не стоит использовать new и delete в клиентском коде, а для объектов с нестандартным - зависит от конкретной модели.

* Некоторые исключения: идиома pimpl; очень большой объект (например, буфер памяти).

** Исключение составляет std::locale::facet (см. дальше).

Динамические объекты со стандартным управлением памятью

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

Собственно, умные указатели, да, это ответ. Именно им следует отдать управление временем жизни динамических объектов. Их в С++ целых два: std::shared_ptr и std::unique_ptr . Не будем здесь выделять std::weak_ptr , т.к. это просто помощник для std::shared_ptr в определённых сценариях использования.

Что касается std::auto_ptr , он был официально исключён из С++ начиная с С++17. Покойся с миром!

Не буду здесь останавливаться на устройстве и использовании умных указателей, т.к. это выходит за рамки статьи. Сразу напомню, что они идут в комплекте с замечательными функциями std::make_shared и std::make_unique , и именно их следует использовать для создания умных указателей.

Т.е. вместо вот такого:
std::unique_ptr cookie(new Cookie(dough, sugar, cinnamon));
следует писать вот так:
auto cookie = std::make_unique(dough, sugar, cinnamon);
Преимущества make -функций над явным созданием умных указателей прекрасно описаны Гербом Саттером в его GotW #89 и Скоттом Майерсом в его Effective Modern C++ , Item 21. Не буду повторяться, лишь приведу здесь краткий список тезисов:

  • Для обеих make -функций:
    • Безопасность с точки зрения исключений.
    • Нет дублирования имени типа.
  • Для std::make_shared:
    • Выигрыш в производительности, т.к. контрольный блок выделяется рядом с самим объектом, что уменьшает количество обращений к менеджеру памяти и увеличивает локальность данных. Оптимизация .
У make-функций имеется и ряд ограничений, подробно описанных в тех же источниках:
  • Для обеих make -функций:
    • Нельзя передать свой deleter . Это вполне логично, т.к. внутри себя make -функции по определению используют стандартный new .
    • Нельзя использовать braced initializer , а также все прочие тонкости, связанные с perfect forwarding (см. Effective Modern C++, Item 30).
  • Для std::make_shared:
    • Потенциальный перерасход памяти для больших объектов при долгоживущих слабых ссылках (std::weak_pointer).
    • Проблемы с операторами new и delete переопределёнными на уровне класса.
    • Потенциальное ложное разделение (false sharing) между объектом и контрольным блоком (см. вопрос на StackOverflow).
На практике указанные ограничения встречаются редко и не умаляют преимуществ. Получается, что умные указатели скрыли от нас вызов delete , а make -функции скрыли от нас вызов new . В итоге мы получили более надёжный код, в котором нет ни new , ни delete .

Кстати, устройство make -функций серьёзно раскрывает в своих докладах Стефан Лававей (a.k.a. STL). Приведу здесь красноречивый слайд из его доклада Don’t Help the Compiler:

Динамические объекты с нестандартным управлением памятью

Помимо стандартного подхода управления памятью через умные указатели встречаются и другие модели. Например, подсчёт количества ссылок (reference counting) и отношения родитель-ребёнок (parent to child relationship).

Динамические объекты с подсчётом ссылок


Очень часто встречающийся приём, используемый во многих библиотеках. Рассмотрим в качестве примера библиотеку OpenSceneGraph. Это открытый кроссплатформенный 3D-движок, написанный на С++ и OpenGL.

Большая часть классов в нём наследуется от класса osg::Referenced , который осуществляет внутри себя подсчёт ссылок. Метод ref() увеличивает счётчик, метод unref() уменьшает счётчик и удаляет объект, когда счётчик опускается до нуля.

В комплекте также идёт умный указатель osg::ref_ptr , который вызывает метод T::ref() для хранимого объекта в своём конструкторе и метод T::unref() в деструкторе. Такой же подход используется в boost::intrusive_ptr , только там вместо методов ref() и unref() выступают внешние функции.

Рассмотрим фрагмент кода, который приведён в официальном руководстве OpenSceneGraph 3.0: Beginner"s guide :
osg::ref_ptr vertices = new osg::Vec3Array; // ... osg::ref_ptr normals = new osg::Vec3Array; // ... osg::ref_ptr geom = new osg::Geometry; geom->setVertexArray(vertices.get()); geom->
Очень знакомые конструкции вида osg::ref_ptr p = new T . Абсолютно аналогично тому, как функции std::make_unique и std::make_shared служат для создания классов std::unique_ptr и std::shared_ptr , мы можем написать функцию osg::make_ref для создания класса osg::ref_ptr . Делается это очень просто, по аналогии с функцией std::make_unique:
namespace osg { template osg::ref_ptr make_ref(Args&&... args) { return new T(std::forward(args)...); } }
Перепишем этот фрагмент кода вооружившись нашей новой функцией:
auto vertices = osg::make_ref(); // ... auto normals = osg::make_ref(); // ... auto geom = osg::make_ref(); geom->setVertexArray(vertices.get()); geom->setNormalArray(normals.get()); // ...
Изменения тривиальны и легко могут быть выполнены автоматически. Таким нехитрым способом мы получаем безопасность с точки зрения исключений , отсутствие дублирования имени типа и прекрасное соответствие стандартному стилю.

Вызов delete уже был спрятан в методе osg::Referenced::unref() , а теперь мы спрятали и вызов new в функции osg::make_ref . Так что никаких new и delete .

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

Динамические объекты для немодальных диалогов в MFC


Рассмотрим пример, специфичный для библиотеки MFC. Это обёртка из классов С++ над Windows API. Она используется для упрощения разработки GUI под Windows.

Интересен приём, которым Microsoft официально рекомендует пользоваться для создания немодальных диалогов. Т.к. диалог немодальный, не совсем ясно, кто ответственен за его удаление. Предлагается ему удалять себя самому в переопределённом методе CDialog::PostNcDestroy() . Этот метод вызывается после обработки сообщения WM_NCDESTROY - последнего сообщения, получаемого окном в его жизненном цикле.

В примере ниже диалог создаётся по нажатию на кнопку в методе CMainFrame::OnBnClickedCreate() и удаляется в переопределённом методе CMyDialog::PostNcDestroy() .
void CMainFrame::OnBnClickedCreate() { auto* pDialog = new CMyDialog(this); pDialog->ShowWindow(SW_SHOW); } class CMyDialog: public CDialog { public: CMyDialog(CWnd* pParent) { Create(IDD_MY_DIALOG, pParent); } protected: void PostNcDestroy() override { CDialog::PostNcDestroy(); delete this; } };
Здесь у нас не спрятан ни вызов new , ни вызов delete . Способов выстрелить себе в ногу - масса. Помимо обычных проблем с указателями, можно забыть переопределить в своём диалоге метод PostNcDestroy() , получим утечку памяти. При виде вызова new , может возникнуть желание самостоятельно вызвать в определённый момент delete , получим двойное удаление. Можно случайно создать объект диалога в автоматической памяти, снова получим двойное удаление.

Попробуем спрятать вызовы к new и delete внутри промежуточного класса CModelessDialog и фабрики CreateModelessDialog , которые будут отвечать в нашем приложении за немодальные диалоги:
class CModelessDialog: public CDialog { public: CModelessDialog(UINT nIDTemplate, CWnd* pParent) { Create(nIDTemplate, pParent); } protected: void PostNcDestroy() override { CDialog::PostNcDestroy(); delete this; } }; // Фабрика для создания модальных диалогов template Derived* CreateModelessDialog(Args&&... args) { // Вместо static_assert в теле функции, можно использовать std::enable_if в её заголовке, что позволит нам использовать SFINAE. // Но т.к. вряд ли ожидаются другие перегрузки этой функции, разумным выглядит использовать более простое и наглядное решение. static_assert(std::is_base_of::value, "CreateModelessDialog should be called for descendants of CModelessDialog"); auto* pDialog = new Derived(std::forward(args)...); pDialog->ShowWindow(SW_SHOW); return pDialog; }
Класс сам переопределяет метод PostNcDestroy() , в котором мы спрятали delete , а для создания классов наследников используется фабрика, в которой мы спрятали new . Создание и определение класса наследника теперь выглядит так:
void CMainFrame::OnBnClickedCreate() { CreateModelessDialog(this); } class CMyDialog: public CModelessDialog { public: CMyDialog(CWnd* pParent) : CModelessDialog(IDD_MY_DIALOG, pParent) {} };
Конечно, подобным образом мы не решили всех проблем. Например, объект всё равно можно выделить на стеке и получить двойное удаление. Запретить выделение объекта на стеке можно только путём модификации самого класса объекта, например добавлением закрытого конструктора. Но мы никак не можем этого сделать из базового класса CModelessDialog . Можно, конечно, вообще сокрыть класс CMyDialog и сделать фабрику не шаблонной, а более классической, принимающей некоторый идентификатор класса. Но это всё уже выходит за рамки статьи.

Так или иначе, мы упростили создание диалога из клиентского кода и написание нового класса диалога. И при этом мы убрали из клиентского кода вызовы new и delete .

Динамические объекты с отношением родитель-ребёнок



Встречаются достаточно часто, особенно в библиотеках для разработки GUI. В качестве примера рассмотрим Qt - хорошо известную библиотеку для разработки приложений и UI.

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

Отличный пример ситуации, когда избавиться от new и delete так просто не получится. Библиотека проектировалась таким образом, что эти операторы можно и нужно применять во многих случаях. Я предлагал обёртку для создания объектов с ненулевым родителем, но идея не пошла (см. обсуждение в Qt mailing list).

Таким образом, мне неизвестен хороший способ избавиться от new и delete в Qt.

Динамические объекты std::locale::facet


Для управления выводом данных в потоки в С++ используются объекты std::locale . Локаль является набором фасетов (facet), которые определяют способ вывода тех или иных данных. Фасеты имеют свой счётчик ссылок и при копировании локалей не происходит копирования фасетов, копируется лишь указатель и увеличивается счётчик ссылок.

Локаль сама ответственна за удаление фасетов, когда счётчик ссылок падает до нуля, но вот создавать фасеты должен пользователь, используя оператор new (см. секцию Notes в описании конструктора std::locale) :
std::locale default; std::locale myLocale(default, new std::codecvt_utf8);
Этот механизм был реализован ещё до внедрения стандартных умных указателей и выбивается из общих правил применения классов стандартной библиотеки.

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

Заключение

Итак, сначала мы рассмотрели такие сценарии, как создание динамических массивов и динамических объектов со стандартным управлением памятью. Вместо new и delete мы использовали стандартные контейнеры и make -функции и получили более простой и надёжный код.

Затем мы рассмотрели ряд примеров нестандартного управления памятью и увидели, как можно сделать код лучше, убрав new и delete в подходящие обёртки. Мы также обнаружили пример, когда подобный подход не работает.

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

  • Избегайте использования new и delete в коде. Воспринимайте их как низкоуровневые операции ручного управления динамической памятью.
  • Используйте стандартные контейнеры для динамических структур данных.
  • Используйте make -функции для создания динамических объектов, когда это возможно.
  • Создавайте обёртки для объектов с нестандартной моделью памяти.

От автора

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

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

Чуть больше года назад я выступал с докладом на эту тему на конференции C++ Russia. После моего выступления аудитория разделилась на две группы: те, для кого всё было очевидным, и те, кто сделал для себя замечательное открытие. Полагаю, что на конференции чаще ходят уже достаточно опытные разработчики, так что, если даже среди них было множество людей, для кого эта информация была в новинку, я надеюсь, что эта статья будет полезна для сообщества.

PS В процессе обсуждения статьи, у нас с коллегами разгорелся целый спор, как правильно: «Майерс» или «Мейерс». С одной стороны, для русского слуха более привычно звучит «Мейерс», и мы сами вроде бы всегда говорили именно так. С другой стороны, на вики используется именно «Майерс». Если посмотреть локализованные книги , то там вообще кто во что горазд: к этим двум вариантам прибавляется ещё и «Мэйерс». На конференциях разные люди представляют его по-разному. В конечном итоге нам удалось выяснить , что сам себя он называет именно «Майерс», на чём и порешили.

Ссылки

  1. Herb Sutter, GotW #89 Solution: Smart Pointers .
  2. Scott Meyers, Effective Modern C++ , Item 21, p. 139.
  3. Stephan T. Lavavej, Don’t Help the Compiler .
  4. Bjarne Stroustrup, The C++ Programming Language , 11.2.1, p. 281.
  5. Five Popular Myths about C++ . , Part 2
  6. Mikhail Matrosov, C++ without new and delete .

Теги:

Добавить метки

Комментарии 134

Массивы и указатели на самом деле тесно связаны. Имя массива является указателем-константой , значением которой служит адрес первого элемента массива (&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