Выделение памяти в Си (функция malloc). Динамическое выделение памяти в си Выделение памяти для программы в с

Последнее обновление: 28.05.2017

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

Double numbers = {1.0, 2.0, 3.0, 4.0, 5.0};

Для такого массива выделяется память 5 * 8 (размер типа double) = 40 байт. Таким образом, мы точно знаем, сколько в массиве элементов и сколько он занимает памяти. Однако это не всегда удобно. Иногда бывает необходимо, чтобы количество элементов и соответственно размер выделяемой памяти для массива определялись динамически в зависимости от некоторых условий. Например, пользователь сам может вводить размер массива. И в этом случае для создания массива мы можем использовать динамическое выделение памяти.

Для управления динамическим выделением памяти используется ряд функций, которые определены в заголовочном файле stdlib.h :

    malloc() . Имеет прототип

    Void *malloc(unsigned s);

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

    calloc() . Имеет прототип

    Void *calloc(unsigned n, unsigned m);

    Выделяет память для n элементов по m байт каждый и возвращает указатель на начало выделенной памяти. В случае неудачного выполнения возвращает NULL

    realloc() . Имеет прототип

    Void *realloc(void *bl, unsigned ns);

    Изменяет размер ранее выделенного блока памяти, на начало которого указывает указатель bl, до размера в ns байт. Если указатель bl имеет значение NULL , то есть память не выделялась, то действие функции аналогично действию malloc

    free() . Имеет прототип

    Void *free(void *bl);

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

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

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

#include #include int main(void) { int *block; // указатель для блока памяти int n; // число элементов массива // ввод числа элементов printf("Size of array="); scanf("%d", &n); // выделяем память для массива // функция malloc возвращает указатель типа void* // который автоматически преобразуется в тип int* block = malloc(n * sizeof(int)); // вводим числа в массив for(int i=0;i

Консольный вывод программы:

Size of array=5 block=23 block=-4 block=0 block=17 block=81 23 -4 0 17 81

Здесь для управления памятью для массива определен указатель block типа int . Количество элементов массива заранее неизвестно, оно представлено переменной n.

Вначале пользователь вводит количество элементов, которое попадает в переменную n. После этого необходимо выделить память для данного количества элементов. Для выделения памяти здесь мы могли бы воспользоваться любой из трех вышеописанных функций: malloc, calloc, realloc. Но конкретно в данной ситуации воспользуемся функцией malloc :

Block = malloc(n * sizeof(int));

Прежде всего надо отметить, что все три выше упомянутые функции для универсальности возвращаемого значения в качестве результата возвращают указатель типа void * . Но в нашем случае создается массив типа int, для управления которым используется указатель типа int * , поэтому выполняется неявное приведение результата функции malloc к типу int * .

В саму функцию malloc передается количество байтов для выделяемого блока. Это количество подсчитать довольно просто: достаточно умножить количество элементов на размер одного элемента n * sizeof(int) .

После выполнения всех действий память освобождается с помощью функции free() :

Free(block);

Важно, что после выполнения этой функции мы уже не сможем использовать массив, например, вывести его значения на консоль:

Free(block); for(int i=0;i

И если мы попытаемся это сделать, то получим неопределенные значения.

Вместо функции malloc аналогичным образом мы могли бы использовать функцию calloc() , которая принимает количество элементов и размер одного элемента:

Block = calloc(n, sizeof(int));

Либо также можно было бы использовать функцию realloc() :

Int *block = NULL; block = realloc (block, n * sizeof(int));

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

Но в целом все три вызова в данном случае имели бы аналогичное действие:

Block = malloc(n * sizeof(int)); block = calloc(n, sizeof(int)); block = realloc (block, n * sizeof(int));

Теперь рассмотрим более сложную задачу - динамическое выделение памяти для двухмерного массива:

#include #include int main(void) { int **table; // указатель для блока памяти для массива указателей int *rows; // указатель для блока памяти для хранения информации по строкам int rowscount; // количество строк int d; // вводимое число // ввод количества строк printf("Rows count="); scanf("%d", &rowscount); // выделяем память для двухмерного массива table = calloc(rowscount, sizeof(int*)); rows = malloc(sizeof(int)*rowscount); // цикл по строкам for (int i = 0; i

Переменная table представляет указатель на массив указателей типа int* . Каждый указатель table[i] в этом массиве представляет указатель на подмассив элементов типа int , то есть отдельные строки таблицы. А переменная table фактически представляет указатель на массив указателей на строки таблицы.

Для хранения количества элементов в каждом подмассиве определяется указатель rows типа int . Фактически он хранит количество столбцов для каждой строки таблицы.

Сначала вводится количество строк в переменную rowscount . Количество строк - это количество указателей в массиве, на который указывает указатель table . И кроме того, количество строк - это количество элементов в динамическом массиве, на который указывает указатель rows . Поэтому вначале необходимо для всех этих массивов выделить память:

Table = calloc(rowscount, sizeof(int*)); rows = malloc(sizeof(int)*rowscount);

Далее в цикле осуществляется ввод количества столбцов для каждый строки. Введенное значение попадает в массив rows. И в соответствии с введенным значением для каждой строки выделяется необходимый размер памяти:

Scanf("%d", &rows[i]); table[i] = calloc(rows[i], sizeof(int));

Затем производится ввод элементов для каждой строки.

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

Free(table[i]);

И кроме того, освобождается память, выделенная для указателей table и rows:

Free(table); free(rows);

Консольный вывод программы:

Rows count=2 Columns count for 1=3 table=1 table=2 table=3 Columns count for 2=2 table=4 table=5 1 2 3 4 5

Динамическое и статическое выделение памяти. Преимущества и недостатки. Выделение памяти для одиночных переменных операторами 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; } время выполнения программы. Под локальные переменные программа отводит память из стекового пространства. Однако локальные переменные требуют предварительного определения объема памяти, выделяемой для каждой ситуации. Хотя С++ эффективно реализует такие переменные, они требуют от программиста заранее знать, какое количество памяти необходимо для каждой ситуации.

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


Рис. 24.1.

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

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

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

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

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

Все переменные, объявленные в программе размещаются в одной непрерывной области памяти, которую называют сегментом данных . Такие переменные не меняют своего размера в ходе выполнения программы и называются статическими . Размера сегмента данных может быть недостаточно для размещения больших объемов информации. Выходом из этой ситуации является использование динамической памяти. Динамическая память – это память , выделяемая программе для ее работы за вычетом сегмента данных, стека, в котором размещаются локальные переменные подпрограмм и собственно тела программы.

Для работы с динамической памятью используют указатели. С их помощью осуществляется доступ к участкам динамической памяти, которые называются динамическими переменными . Для хранения динамических переменных выделяется специальная область памяти, называемая " кучей ".

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

В С++ используется два способа работы с динамической памятью:

  1. использование операций new и delete ;
  2. использование семейства функций mallос (calloc ) (унаследовано из С).

Работа с динамической памятью с помощью операций new и delete

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

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

Синтаксис :

new ИмяТипа;

new ИмяТипа [Инициализатор];

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

Синтаксис применения операции :

Указатель = new ИмяТипа [Инициализатор];

Операция new float выделяет участок памяти размером 4 байта. Операция new int(15) выделяет участок памяти 4 байта и инициализирует этот участок целым значением 15. Синтаксис использования операций new и delete предполагает применение указателей. Предварительно каждый указатель должен быть объявлен:

тип *ИмяУказателя;

Например:

float *pi; //Объявление переменной pi pi=new float; //Выделение памяти для переменной pi * pi = 2.25; //Присваивание значения

В качестве типа можно использовать, например, стандартные типы int, long, float, double, char .

Оператор new чаще всего используется для размещения в памяти данных определенных пользователем типов, например, структур:

struct Node { char *Name; int Value; Node *Next }; Node *PNode; //объявляется указатель PNode = new Node; //выделяется память PNode->Name = "Ata"; //присваиваются значения PNode->Value = 1; PNode->Next = NULL;

В качестве имени типа в операции new может быть использован массив :

new ТипМассива

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

ptr = new int ;//10 элементов типа int или 40 байт ptr = new int ;//неверно, т.к. не определен размер

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

int *n = new int;

Операция new выполняет выделение достаточного для размещения величины типа int участка динамической памяти и записывает адрес начала этого участка в переменную n . Память под саму переменную n (размера, достаточного для размещения указателя) выделяется на этапе компиляции.

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

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

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

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

Язык С++ определяет два оператора динамического выделения памя­ти - new и delete.

Стандарт ANSI С определяет только четыре функции динамического выделения памяти: calloc(), malloc(), free() и realloc(). Однако Borland С++ содержит несколько других функций динамичес­кого выделения памяти. При компиляции кода для современной 32-разрядной модели памяти, память являет­ся плоской и обычно используются только четыре стандартные функции выделения памяти.

Стандарт ANSI С определяет, что заголовочная информация, необходимая для динамического выделения памяти, содержится в файле stdlib.h. Однако Borland С++ позволяет использовать заго­ловочные файлы stdlib.h или alloc.h. Здесь мы используем заголовочный файл stdlib.h, поскольку это обеспечивает переносимость. Некоторые другие функции динамического выделения памяти требуют заголовочных файлов alloc.h, malloc.h или dos.h. Необходимо обращать особое внимание на то, какой заголовочный файл необходим для использования каждой функции.

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