Использование new delete для реализации массивов

Скачать на Телефон 20.05.2019
  • Tutorial

Привет! Ниже речь пойдет об известных всем операторах new и delete , точнее о том, о чем не пишут в книгах (по крайней мере в книгах для начинающих).
На написание данной статьи меня побудило часто встречаемое заблуждение по поводу new и delete , которое я постоянно вижу на форумах и даже(!!!) в некоторых книгах.
Все ли мы знаем, что такое на самом деле new и delete ? Или только думаем, что знаем?
Эта статья поможет вам разобраться с этим (ну, а те, кто знают, могут покритиковать:))

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

Итак, начнем с того, что обычно пишут в книгах для начинающих, когда описывают new (текст взят «с потолка», но вцелом соответствует правде):

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

И для примера показывают примитивную перегрузку (реализацию) оператора new, прототип которого выглядит так
void* operator new (std::size_t size) throw (std::bad_alloc);

На что хочется обратить внимание:
1. Нигде не разделяют new key-word языка С++ и оператор new , везде о них говорят как об одной сущности.
2. Везде пишут, что new вызывает конструктор(ы) для объекта(ов).
И первое и второе является распространенным заблуждением.

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

5.3.4
The new-expression attempts to create an object of the type-id (8.1) or new-type-id to which it is applied. /*дальше нам не интересно*/
18.6.1
void* operator new(std::size_t size) throw(std::bad_alloc);
Effects: The allocation function called by a new-expression (5.3.4) to allocate size bytes of
storage suitably aligned to represent any object of that size /*дальше нам не интересно*/

Тут мы уже видим, что в первом случае new именуется как expression , а во втором он объявлен как operator. И это действительно 2 разные сущности!
Попробуем разобраться почему так, для этого нам понадобятся ассемблерные листинги, полученные после компиляции кода, использующего new. Ну, а теперь обо все по порядку.

new-expression - это оператор языка, такой же как if , while и т.д. (хотя if, while и т.д. все же именуются как statement , но отбросим лирику) Т.е. встречая его в листинге компилятор генерирует определенный код, соответствующий этому оператору. Так же new - это одно из key-words языка С++, что еще раз подтверждает его общность с if "ами, for" ами и т.п. А operator new() в свою очередь - это просто одноименная функция языка С++, поведение которой можно переопределить. ВАЖНО - operator new() НЕ вызывает конструктор(ы) для объекта(ов), под который(ые) выделяется память. Он просто выделяет память нужного размера и все. Его отличие от сишных функций в том, что он может бросить исключение и его можно переопределить, а так же сделать оператором для отдельно взятого класса, тем самым переопределить его только для этого класса (остальное вспомните сами:)).
А вот new-expression как раз и вызывает конструктор(ы) объекта(ов). Хотя правильней сказать, что он тоже ничего не вызывает, просто, встречая его, компилятор генерирует код вызова конструктора(ов).

Для полноты картины рассмотрим следующий пример:

#include class Foo { public: Foo() { std::cout << "Foo()" << std::endl; } }; int main () { Foo *bar = new Foo; }

После исполнения данного кода, как и ожидалось, будет напечатано «Foo()». Разберемся почему, для этого понадобится заглянуть в ассемблер, который я немного прокомментировал для удобства.
(код получен компилятором cl, используемым в MSVS 2012, хотя в основном я использую gcc, но это к делу не относится)
/Foo *bar = new Foo; push 1 ; размер в байтах для объекта Foo call operator new (02013D4h) ; вызываем operator new pop ecx mov dword ptr ,eax ; записываем указатель, вернувшийся из new, в bar and dword ptr ,0 cmp dword ptr ,0 ; проверяем не 0 ли записался в bar je main+69h (0204990h) ; если 0, то уходим отсюда (возможно вообще из main или в какой-то обработчик, в данном случае неважно) mov ecx,dword ptr ; кладем указатель на выделенную память в ecx (MSVS всегда передает this в ecx(rcx)) call Foo::Foo (02011DBh) ; и вызываем конструктор; дальше не интересно
Для тех, кто ничего не понял, вот (почти) аналог того, что получилось на сиподобном псевдокоде (т.е. не надо пробовать это компилировать:))
Foo *bar = operator new (1); // где 1 - требуемый размер bar->Foo(); // вызываем конструктор

Приведенный код подтверждает все, написанное выше, а именно:
1. оператор (языка) new и operator new() - это НЕ одно и тоже.
2. operator new() НЕ вызывает конструктор(ы)
3. вызов конструктора(ов) генерирует компилятор, встречая в коде key-word «new»

Итог: надеюсь, эта статья помогла вам понять разницу между new-expression и operator new() или даже узнать, что она (эта разница) вообще существует, если кто-то не знал.

P.S. оператор delete и operator delete() имеют аналогичное различие, поэтому в начале статьи я сказал, что не буду его описывать. Думаю, теперь вы поняли, почему его описание не имеет смысла и сможете самостоятельно проверить справедливость написанного выше для delete .

Update:
Хабражитель с ником khim в личной переписке предложил следующий код, который хорошо демонстрирует суть написанного выше.
#include class Test { public: Test() { std::cout << "Test::Test()" << std::endl; } void* operator new (std::size_t size) throw (std::bad_alloc) { std::cout << "Test::operator new(" << size << ")" << std::endl; return::operator new(size); } }; int main() { Test *t = new Test(); void *p = Test::operator new(100); // 100 для различия в выводе }
Этот код выведет следующее
Test::operator new(1) Test::Test() Test::operator new(100)
что и следовало ожидать.

Оператор 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


Рекомендуем почитать

Наверх