2.2. Динамическое выделение памяти и указатели
2.2. Динамическое выделение памяти и указатели
Прежде чем углубиться в объектно-ориентированную разработку, нам придется сделать небольшое отступление о работе с памятью в программе на С++. Мы не сможем написать сколько-нибудь сложную программу, не умея выделять память во время выполнения и обращаться к ней.
В С++ объекты могут быть размещены либо статически – во время компиляции, либо динамически – во время выполнения программы, путем вызова функций из стандартной библиотеки. Основная разница в использовании этих методов – в их эффективности и гибкости. Статическое размещение более эффективно, так как выделение памяти происходит до выполнения программы, однако оно гораздо менее гибко, потому что мы должны заранее знать тип и размер размещаемого объекта. К примеру, совсем не просто разместить содержимое некоторого текстового файла в статическом массиве строк: нам нужно заранее знать его размер. Задачи, в которых нужно хранить и обрабатывать заранее неизвестное число элементов, обычно требуют динамического выделения памяти.
До сих пор во всех наших примерах использовалось статическое выделение памяти. Скажем, определение переменной 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[4];
В этом примере память выделяется под массив из четырех элементов типа 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[1024];
Упражнение 2.4
Что делает следующий фрагмент кода? В чем состоит логическая ошибка? (Отметим, что операция взятия индекса ([]) правильно применена к указателю pia. Объяснение этому факту можно найти в разделе 3.9.2.)
int *pi = new int(10);
int *pia = new int[10];
while ( *pi 10 ) {
pia[*pi] = *pi;
*pi = *pi + 1;
}
delete pi;
delete[] pia;
Более 800 000 книг и аудиокниг! 📚
Получи 2 месяца Литрес Подписки в подарок и наслаждайся неограниченным чтением
ПОЛУЧИТЬ ПОДАРОКЧитайте также
3.2. Выделение памяти
3.2. Выделение памяти Четыре библиотечные функции образуют основу управления динамической памятью С Мы опишем сначала их, затем последуют описания двух системных вызовов, поверх которых построены эти библиотечные функции. Библиотечные функции С, в свою очередь, обычно
14.1. Выделение выровненной памяти: posix_memalign() и memalign()
14.1. Выделение выровненной памяти: posix_memalign() и memalign() Для большинства задач отлично подходят стандартные процедуры выделения памяти — malloc(), realloc() и т.д. Но иногда может понадобиться память, которая выровнена тем или иным способом. Другими словами, адрес первого выделенного
Статическое выделение памяти в стеке
Статическое выделение памяти в стеке В пространстве пользователя многие операции выделения памяти, в частности некоторые рассмотренные ранее примеры, могут быть выполнены с использованием стека, потому что априори известен размер выделяемой области памяти. В
Выделение памяти, связанной с определенным процессором
Выделение памяти, связанной с определенным процессором В современных операционных системах широко используются данные, связанные с определенными процессорами (per-CPU data). Это данные, которые являются уникальными для каждого процессора. Данные, связанные с процессорами,
Выделение дескриптора памяти
Выделение дескриптора памяти Указатель на дескриптор памяти, выделенный для какой-либо задачи, хранится в поле mm дескриптора процесса этой задачи. Следовательно, выражение current->mm позволяет получить дескриптор памяти текущего процесса. Функция copy_mm() используется для
Выделение памяти
Выделение памяти При обсуждении формата исполняемых файлов и образа программы в памяти мы отметили, что сегменты данных и стека могут изменять свои размеры. Если для стека операцию выделения памяти операционная система производит автоматически, то приложение имеет
18.6.5 Выделение оперативной памяти для VMware
18.6.5 Выделение оперативной памяти для VMware Система VMware позволяет пользователям задавать как объем оперативной памяти, выделяемой каждому виртуальному компьютеру, так и общее количество ОП, зарезервированное для использования виртуальными машинами. Правильная настройка
6.1.6. Динамическое выделение адреса
6.1.6. Динамическое выделение адреса Как я уже сказал, IP-адрес любого устройства, подключенного к Интернету, должен быть уникальным. Это означает, что статически назначенный вам адрес не сможет использовать никто другой, даже тогда, когда вы отключитесь от сети. Избавиться
Динамическое связывание
Динамическое связывание Упрощенно говоря, динамическое связывание, или динамическая привязка, - это подход, с помощью которого можно создавать экземпляры заданного типа и вызывать их члены в среде выполнения и условиях, когда во время компиляции о типе еще ничего не
Выделение памяти
Выделение памяти Сначала следует определить место для размещения строки при вводе. Как было отмечено раньше, это значит, выделить память, достаточную для размещения любых строк, которые мы предполагаем читать. Не следует надеяться, что компьютер подсчитает длину
5.1.3. Выделение сегментов памяти
5.1.3. Выделение сегментов памяти Процесс выделяет сегмент памяти с помощью функции shmget(). Первым аргументом функции является целочисленный ключ, идентифицирующий создаваемый сегмент. Если несвязанные процессы хотят получить доступ к одному и тому же сегменту, они должны
Динамическое распределение памяти
Динамическое распределение памяти Библиотека языка Си предоставляет механизм распределения динамической памяти (heap). Этот механизм позволяет динамически (по мере возникновения необходимости) запрашивать из программы дополнительные области оперативной памяти.Работа
Динамическое связывание
Динамическое связывание Сочетание последних двух механизмов, переопределения и полиморфизма, непосредственно предполагает следующий механизм. Допустим, есть вызов, целью которого является полиморфная сущность, например сущность типа BOAT вызывает компонент turn.
Динамическое связывание
Динамическое связывание Динамическое связывание дополнит переопределение, полиморфизм и статическую типизацию, создавая базисную тетралогию
Ошибка 0x000000C2: неправильное выделение памяти
Ошибка 0x000000C2: неправильное выделение памяти Некорректное выделение памяти. Причина – некорректно работающий