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

Прежде чем углубиться в объектно-ориентированную разработку, нам придется сделать небольшое отступление о работе с памятью в программе на С++. Мы не сможем написать сколько-нибудь сложную программу, не умея выделять память во время выполнения и обращаться к ней.

В С++ объекты могут быть размещены либо статически – во время компиляции, либо динамически – во время выполнения программы, путем вызова функций из стандартной библиотеки. Основная разница в использовании этих методов – в их эффективности и гибкости. Статическое размещение более эффективно, так как выделение памяти происходит до выполнения программы, однако оно гораздо менее гибко, потому что мы должны заранее знать тип и размер размещаемого объекта. К примеру, совсем не просто разместить содержимое некоторого текстового файла в статическом массиве строк: нам нужно заранее знать его размер. Задачи, в которых нужно хранить и обрабатывать заранее неизвестное число элементов, обычно требуют динамического выделения памяти.

До сих пор во всех наших примерах использовалось статическое выделение памяти. Скажем, определение переменной ival

заставляет компилятор выделить в памяти область, достаточную для хранения переменной типа int, связать с этой областью имя ival и поместить туда значение 1024. Все это делается на этапе компиляции, до выполнения программы.

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

то обращаемся к значению, содержащемуся в переменной ival: прибавляем к нему 1 и инициализируем переменную ival2 этим новым значением, 1025. Каким же образом обратиться к адресу, по которому размещена переменная?

С++ имеет встроенный тип “указатель”, который используется для хранения адресов объектов. Чтобы объявить указатель, содержащий адрес переменной ival, мы должны написать:

Существует также специальная операция взятия адреса, обозначаемая символом &. Ее результатом является адрес объекта. Следующий оператор присваивает указателю pint адрес переменной ival:

Мы можем обратиться к тому объекту, адрес которого содержит pint (ival в нашем случае), используя операцию разыменования, называемую также косвенной адресацией. Эта операция обозначается символом *. Вот как можно косвенно прибавить единицу к ival, используя ее адрес:

Это выражение производит в точности те же действия, что и

В этом примере нет никакого реального смысла: использование указателя для косвенной манипуляции переменной ival менее эффективно и менее наглядно. Мы привели этот пример только для того, чтобы дать самое начальное представление об указателях.

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

  • статические объекты обозначаются именованными переменными, и действия над этими объектами производятся напрямую, с использованием их имен. Динамические объекты не имеют собственных имен, и действия над ними производятся косвенно, с помощью указателей;
  • выделение и освобождение памяти под статические объекты производится компилятором автоматически. Программисту не нужно самому заботиться об этом. Выделение и освобождение памяти под динамические объекты целиком и полностью возлагается на программиста. Это достаточно сложная задача, при решении которой легко наделать ошибок. Для манипуляции динамически выделяемой памятью служат операторы new и delete.

Оператор new имеет две формы. Первая форма выделяет память под единичный объект определенного типа:

Здесь оператор new выделяет память под безымянный объект типа int, инициализирует его значением 1024 и возвращает адрес созданного объекта. Этот адрес используется для инициализации указателя pint. Все действия над таким безымянным объектом производятся путем разыменования данного указателя, т.к. явно манипулировать динамическим объектом невозможно.

Вторая форма оператора new выделяет память под массив заданного размера, состоящий из элементов определенного типа:

В этом примере память выделяется под массив из четырех элементов типа int. К сожалению, данная форма оператора new не позволяет инициализировать элементы массива. Некоторую путаницу вносит то, что обе формы оператора new возвращают одинаковый указатель, в нашем примере это указатель на целое.

И pint, и pia объявлены совершенно одинаково, однако pint указывает на единственный объект типа int, а pia – на первый элемент массива из четырех объектов типа int. Когда динамический объект больше не нужен, мы должны явным образом освободить отведенную под него память. Это делается с помощью оператора delete, имеющего, как и new, две формы – для единичного объекта и для массива:

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