Динамическое выделение памяти c. Управляющие конструкции языка Си. Представление программ в виде функций. Работа с памятью. Структуры. Стандартные функции динамического выделения памяти

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

Int ival = 1024;

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

Int ival2 = ival + 1;

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

Int *pint; // указатель на объект типа int

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

Int *pint; pint = &ival; // pint получает значение адреса ival

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

*pint = *pint + 1; // неявно увеличивает ival

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

Ival = ival + 1; // явно увеличивает ival

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

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

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

Int *pint = new int(1024);

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

Int *pia = new int;

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

// освобождение единичного объекта delete pint; // освобождение массива delete pia;

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

Упражнение 2.3

Объясните разницу между четырьмя объектами:

(a) int ival = 1024; (b) int *pi = &ival; (c) int *pi2 = new int(1024); (d) int *pi3 = new int;

Упражнение 2.4

Что делает следующий фрагмент кода? В чем состоит логическая ошибка? (Отметим, что операция взятия индекса () правильно применена к указателю pia. Объяснение этому факту можно найти в разделе 3.9.2.)

Int *pi = new int(10); int *pia = new int;
while (*pi < 10) {
pia[*pi] = *pi; *pi = *pi + 1;
} delete pi; delete pia;

Итак. третий тип, самый интересный в этой теме для нас – динамический тип памяти.

Как мы работали с массивами раньше? int a Как мы работаем сейчас? Выделяем столько, сколько нужно:

#include < stdio.h> #include < stdlib.h> int main () { size_t size; // Создаём указатель на int // – по сути, пустой массив. int *list; scanf (" %lu " , &size); // Выделяем память для size элементов размером int // и наш "пустой массив" теперь ссылается на эту память. list = (int *)malloc (size * sizeof (int )); for (int i = 0 ; i < size; ++i) { scanf (" %d " < size; ++i) { printf (" %d " , *(list + i)); } // Не забываем за собой прибраться! free (list); } // *

Void * malloc(size_t size);

Но в общем и целом это функция, выделяет size байт неинициализированной памяти (не нули, а мусор).

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

Если неуспешно – NULL. Также errno будет равен ENOMEM (эту замечательную переменную мы рассмотрим позднее). То есть правильнее было написать:

#include < stdio.h> #include < stdlib.h> int main () { size_t size; int *list; scanf (" %lu " , &size); list = (int *)malloc (size * sizeof (int )); if (list == NULL ) { goto error; } for (int i = 0 ; i < size; ++i) { scanf (" %d " , list + i); } for (int i = 0 ; i < size; ++i) { printf (" %d " , *(list + i)); } free (list); return 0 ; error: return 1 ; } // *

Очищать NULL указатель не нужно

#include < stdlib.h> int main () { free (NULL ); }

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

Рядом с malloc и free в мане можно увидеть ещё:

    void * calloc (size_t count, size_t size);

    Равно как и malloc выделит память под count объектов размером по size байт. Выделяемая память инициализируется нулями.

    void * realloc (void *ptr, size_t size);

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

    Если ptr равен NULL , realloc идентичен вызову malloc .

    Если size равен нулю, а ptr не NULL , выделяется кусок памяти минимального размера, а исходная освобождается.

    void * reallocf (void *ptr, size_t size);

    Придумка из FreeBSD API. Как и realloc , но если не сможет перевыделить, очищает принятый указатель.

    void * valloc (size_t size);

    Как и malloc , но выделенная память выравнивается по границе страницы.

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

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

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

· Для локальных переменных, объявленных внутри какого-либо блока и не имеющих спецификатора static, память выделяется другим способом. До начала выполнения программы (при её загрузке) выделяется довольно объёмная область памяти, называемая стеком (иногда используют термины стек программы или стек вызовов , чтобы сделать различие между стеком как абстрактным типом данных). Размер стека зависит от среды разработки, например, в MS Visual C++ по умолчанию под стек выделяется 1 мегабайт (это значение поддаётся настройке). В процессе выполнения программы при входе в определённый блок выделяется память в стеке для локализованных в блоке переменных (в соответствии с описанием их типа), при выходе из блока эта память освобождается. Данные процессы выполняются автоматически, поэтому локальные переменные в С++ часто называют автоматическими .

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

Использование термина "стек" объяснить легко – при принятом подходе к выделению и освобождению памяти переменные, которые помещаются в стек последними (это переменные, локализованные в самом глубоко вложенном блоке), удаляются из него первыми. То есть, выделение и освобождение памяти происходит по принципу LIFO (LAST IN – FIRST OUT, последним пришёл – первым вышел). Это и есть принцип работы стека. Стек как динамическую структуру данных и его возможную реализацию мы рассмотрим в следующем разделе.



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

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

Рисунок 2.1 – схема распределения памяти

В заключение этого раздела коснёмся одной болезненной проблемы в процессе работы со стеком – возможности его переполнения (эта аварийная ситуация обычно называется Stack Overflow ). Причина, породившая проблему, понятна – ограниченный объём памяти, которая выделяется под стек при загрузке программы. Наиболее вероятные ситуации для переполнения стека – локальные массивы больших размеров и глубокая вложенность рекурсивных вызовов функций (обычно возникает при неаккуратном программировании рекурсивных функций, допустим, забыта какая-либо терминальная ветвь).



Для того, чтобы лучше понять проблему переполнения стека, советуем провести такой нехитрый эксперимент. В функции main объявите массив целых чисел размером, допустим, на миллион элементов. Программа скомпилируется, но при её запуске возникнет ошибка переполнения стека. Теперь добавьте в начало описания массива спецификатор static (или вынесите описание массива из функции main ) – программа заработает!

Ничего чудесного в этом нет – просто теперь массив помещается не в стек, а в область глобальных и статических переменных. Размер памяти для этой области определяет компилятор – если программа скомпилировалась, значит, она будет работать.

Тем не менее, объявлять в программе статически формируемые массивы огромных размеров, как правило, нет необходимости. В большинстве случаев более эффективным и гибким способом будет динамическое выделение памяти для таких данных.

Динамическое и статическое выделение памяти. Преимущества и недостатки. Выделение памяти для одиночных переменных операторами new и delete . Возможные критические ситуации при выделении памяти. Инициализация при выделении памяти

1. Динамическое и статическое (фиксированное) выделение памяти. Главные различия

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

1. Статическое (фиксированное ) выделение памяти. В этом случае память выделяется только один раз во время компиляции. Размер выделенной памяти есть фиксированным и неизменным до конца выполнения программы. Примером такого выделения может служить объявление массива из 10 целых чисел:

int M; // память для массива выделяется один раз, размер памяти фиксированный

2. Динамическое выделение памяти. В этом случае используется комбинация операторов new и delete . Оператор new выделяет память для переменной (массива) в специальной области памяти, которая называется «куча» (heap). Оператор delete освобождает выделенную память. Каждому оператору new должен соответствовать свой оператор delete .

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

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

  • память выделяется по мере необходимости программным путем;
  • нет лишних затрат неиспользованной памяти. Выделяется столько памяти сколько нужно и если нужно;
  • можно выделять память для массивов информации, размер которых заведомо неизвестен. Определение размера массива формируется в процессе выполнения программы;
  • удобно осуществлять перераспределение памяти. Или другими словами, удобно выделять новый фрагмент для одного и того же массива, если нужно выделить дополнительную память или освободить ненужную;
  • при статическом способе выделения памяти трудно перераспределять память для переменной-массива, поскольку она уже выделена фиксировано. В случае динамического способа выделения, это делается просто и удобно.

Преимущества статического способа выделения памяти:

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

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

3. Как выделить память оператором new для одиночной переменной? Общая форма.

Общая форма выделения памяти для одиночной переменной оператором new имеет следующий вид:

ptrName = new type;
  • ptrName – имя переменной (указателя), которая будет указывать на выделенную память;
  • type – тип переменной. Размер памяти выделяется достаточный для помещения в нее значения переменной данного типа type .
4. Как освободить память, выделенную под одиночную переменную оператором delete ? Общая форма

Если память для переменной выделена оператором new, то после завершения использования переменной, эту память нужно освободить оператором delete . В языке C++ это есть обязательным условием. Если не освободить память, то память останется выделенной (занятой), но использовать ее не сможет ни одна программа. В данном случае произойдет «утечка памяти» (memory leak).

В языках программирования Java, C# освобождать память после выделения не нужно. Этим занимается «сборщик мусора» (garbage collector ).

Общая форма оператора delete для одиночной переменной:

delete ptrName;

где ptrName – имя указателя, для которого была раньше выделена память оператором new . После выполнения оператора delete указатель ptrName указывает на произвольный участок памяти, который не является зарезервированным (выделенным).

5. Примеры выделения (new ) и освобождения (delete ) памяти для указателей базовых типов

В примерах демонстрируется использование операторов new и delete . Примеры имеют упрощенный вид.

Пример 1. Указатель на тип int . Простейший пример

// выделение памяти оператором new int * p; // указатель на int p = new int ; // выделить память для указателя *p = 25; // записать значения в память // использование памяти, выделенной для указателя int d; d = *p; // d = 25 // освободить память, выделенную для указателя - обязательно delete p;

Пример 2. Указатель на тип double

// выделение памяти для указателя на double double * pd = NULL ; pd = new double ; // выделить память if (pd!=NULL ) { *pd = 10.89; // записать значения double d = *pd; // d = 10.89 - использование в программе // освободить память delete pd; }
6. Что такое «утечка памяти» (memory leak )?

«Утечка памяти » – это когда память для переменной выделяется оператором new , а по окончании работы программы она не освобождается оператором delete . В этом случае память в системе остается занятой, хотя потребности в ее использовании уже нет, поскольку программа, которая ее использовала, уже давно завершила свою работу.

«Утечка памяти» есть типичной ошибкой программиста. Если «утечка памяти» повторяется многократно, то возможная ситуация, когда будет «занята» вся доступная память в компьютере. Это приведет к непредсказуемым последствиям работы операционной системы.

7. Каким образом выделить память оператором new с перехватом критической ситуации, при которой память может не выделиться? Исключительная ситуация bad_alloc . Пример

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

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

В этом случае генерируется исключительная ситуация bad_alloc . Программа может перехватить эту ситуацию и соответствующим образом обработать ее.

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

int main() { // объявить массив указателей на float float * ptrArray; try { // попробовать выделить память для 10 элементов типа float ptrArray = new float ; } catch (bad_alloc ba) { cout << << endl; cout << ba.what() << endl; return -1; // выход из функции } // если все в порядке, то использовать массив for (int i = 0; i < 10; i++) ptrArray[i] = i * i + 3; int d = ptrArray; cout << d << endl; delete ptrArray; // освободить память, выделенную под массив return 0; }
8. Выделение памяти для переменной с одновременной инициализацией. Общая форма. Пример

Оператор выделения памяти new для одиночной переменной допускает одновременную инициализацию значением этой переменной.

В общем, выделение памяти для переменной с одновременной инициализацией имеет вид

ptrName = new type(value )
  • ptrName – имя переменной-указателя, для которой выделяется память;
  • type – тип на который указывает указатель ptrName ;
  • value – значение, которое устанавливается для выделенного участка памяти (значение по указателю).

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

#include "stdafx.h" #include using namespace std; int main() { // выделение памяти с одновременной инициализацией float * pF; int * pI; char * pC; try { // попробовать выделить память для переменных с одновременной инициализацией pF = new float (3.88); // *pF = 3.88 pI = new int (250); // *pI = 250 pC = new char ("M" ); // *pC = "M" } catch (bad_alloc ba) { cout << "Исключительная ситуация. Память не выделена" << endl; cout << ba.what() << endl; return -1; // выход из функции } // если память выделена, то использование указателей pF, pI, pC float f = *pF; // f = 3.88 int i = *pI; // i = 250; char c; c = *pC; // c = "M" // вывести инициализированные значения cout << "*pF = " << f<< endl; cout << "*pI = " << i << endl; cout << "*pC = " << c << endl; // освободить память, выделенную ранее для указателей delete pF; delete pI; delete pC; return 0; }

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

Основные проблемы применения

Нулевой указатель

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

·В языках C# и Java: null

·В языках Си и C++: 0 или макрос NULL. Кроме того, в стандарте C++11 для обозначения нулевого указателя предложено новое ключевое слово nullptr

·В языках Паскаль и Ruby: nil

·В языке Компонентный Паскаль:NIL

·В языке Python: None

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

Для решения части проблем есть методы предохранения и страховки:

Изучив указатели в языке Си, мы открыли для себя возможности динамического выделения памяти. Что это значит? Это значит то, что при динамическом выделении памяти, память резервируется не на этапе компиляции а на этапе выполнения программы. И это дает нам возможность выделять память более эффективно, в основном это касается массивов. С динамическим выделением память, нам нет необходимости заранее задавать размер массива, тем более, что не всегда известно, какой размер должен быть у массива. Далее рассмотрим каким же образом можно выделять память.

Функция malloc() определена в заголовочном файле stdlib.h, она используется для инициализации указателей необходимым объемом памяти. Память выделяется из сектора оперативной памяти доступного для любых программ, выполняемых на данной машине. Аргументом функции malloc() является количество байт памяти, которую необходимо выделить, возвращает функция - указатель на выделенный блок в памяти. Функция malloc() работает также как и любая другая функция, ничего нового.

Так как различные типы данных имеют разные требования к памяти, мы как-то должны научиться получить размер в байтах для данных разного типа. Например, нам нужен участок памяти под массив значений типа int - это один размер памяти, а если нам нужно выделить память под массив того же размера, но уже типа char - это другой размер. Поэтому нужно как-то вычислять размер памяти. Это может быть сделано с помощью операции sizeof(), которая принимает выражение и возвращает его размер. Например, sizeof(int) вернет количество байтов, необходимых для хранения значения типа int. Рассмотрим пример:


Яндекс.Директ


#include int *ptrVar = malloc(sizeof(int));

В этом примере, встроке 3 указателю ptrVar присваивается адрес на участок памяти, размер которого соответствует типу данных int. Автоматически, этот участок памяти становится недоступным для других программ. А это значит, что после того, как выделенная память станет ненужной, её нужно явно высвободить. Если же память не будет явно высвобождена, то по завершению работы программы, память так и не освободится для операционной системы, это называется утечкой памяти. Также можно определять размер выделяемой памяти, которую нужно выделить передавая пустой указатель, вот пример:



Как видите, в такой записи есть одна очень сильная сторона, мы не должны вызывать функцию malloc() с использованиемsizeof(float). Вместо этого мы передали в malloc() указатель на тип float, в таком случае, размер выделяемой памяти автоматически определится сам!

Особенно это пригодится, если выделять память потребуется далеко от определения указателя:


float *ptrVar; /* . . . сто строк кода */ . . . ptrVar = malloc(sizeof(*ptrVar));

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