12.1.2. Непосредственное управление памятью
Язык определяет два оператора, позволяющие резервировать и освобождать области в динамической памяти. Оператор new резервирует память, а оператор delete освобождает память, зарезервированную оператором new.
По причинам, которые станут ясны позже, использование этих операторов для управления памятью существенно более подвержено ошибкам, чем использование интеллектуальных указателей. Кроме того, классы, самостоятельно управляющие памятью (в отличие от таковых, использующих интеллектуальные указатели), не могут полагаться на стандартные определения тех их членов, которые копируют, присваивают и удаляют объекты класса (см. раздел 7.1.4). В результате программы, использующие интеллектуальные указатели, вероятно, будет проще написать и отлаживать.
Использование оператора new для динамического резервирования и инициализации объектов
Созданные в динамической памяти объекты не имеют имен, поэтому оператор new не предполагает никаких способов именования резервируемых объектов. Вместо этого оператор new возвращает указатель на зарезервированный объект:
int *pi = new int; // pi указывает на динамически созданный,
// безымянный,
// неинициализированный объект типа int
Это выражение new создает в динамической памяти объект типа int и возвращает указатель на него.
По умолчанию создаваемые в динамической памяти объекты инициализируются значением по умолчанию (см. раздел 2.2.1). Это значит, что у объектов встроенного или составного типа будет неопределенное значение, а объекты типа класса инициализируются их стандартным конструктором:
string *ps = new string; // инициализируется пустой строкой
int *pi = new int; // pi указывает на неинициализированный int
int *pi = new int(1024); // pi указывает на объект со значением 1024
string *ps = new string(10, '9'); // *ps = "9999999999"
// вектор на десять элементов со значениями от 0 до 9
vector<int> *pv = new vector<int>{0,1,2,3,4,5,6,7,8,9};
Динамически созданный объект можно также инициализировать значением по умолчанию (см. раздел 3.3.1), сопроводив имя типа парой пустых круглых скобок:
string *ps1 = new string; // инициализация по умолчанию пустой строкой
string *ps = new string(); // инициализация значением по умолчанию
// (пустой строкой)
int *pi1 = new int; // инициализация по умолчанию;
// значение *pi1 не определено
int *pi2 = new int(); // инициализация значением по умолчанию 0;
// *pi2 = 0
Для типов классов (таких как string), определяющих собственные конструкторы (см. раздел 7.1.4), запрос инициализации значением по умолчанию не имеет последствий; независимо от формы, объект инициализируется стандартным конструктором. Различие существенно в случае встроенных типов: инициализация объекта встроенного типа значением по умолчанию присваивает ему вполне конкретное значение, а инициализация по умолчанию — нет. Точно так же полагающиеся на синтезируемый стандартный конструктор члены класса встроенного типа также не будут не инициализированы, если эти члены не будут инициализированы в теле класса (см. раздел 7.1.4).
auto p1 = new auto(obj); // p указывает на объект типа obj
// этот объект инициализируется значением obj
auto p2 = new auto{a,b,c}; // ошибка: для инициализатора нужно
// использовать круглые скобки
Тип p1 — это указатель на автоматически выведенный тип obj. Если obj имеет тип int, то тип p1 — int*; если obj имеет тип string, то тип p1 — string* и т.д. Вновь созданный объект инициализируется значением объекта obj.
Динамически созданные константные объекты
Для резервирования константных объектов вполне допустимо использовать оператор new:
// зарезервировать и инициализировать
const int const int *pci = new const int(1024);
// зарезервировать и инициализировать значением по умолчанию
const string const string *pcs = new const string;
Подобно любым другим константным объектам, динамически созданный константный объект следует инициализировать. Динамический константный объект типа класса, определяющего стандартный конструктор (см. раздел 7.1.4), можно инициализировать неявно. Объекты других типов следует инициализировать явно. Поскольку динамически зарезервированный объект является константой, возвращенный оператором new указатель является указателем на константу (см. раздел 2.4.2).
Исчерпание памяти
Хотя современные машины имеют огромный объем памяти, всегда существует вероятность исчерпания динамической памяти. Как только программа использует всю доступную ей память, выражения с оператором new будут терпеть неудачу. По умолчанию, если оператор new неспособен зарезервировать требуемый объем памяти, он передает исключение типа bad_alloc (см. раздел 5.6). Используя иную форму оператора new, можно воспрепятствовать передаче исключения:
// при неудаче оператор new возвращает нулевой указатель
int *p1 = new int; // при неудаче оператор new передает
// исключение std::bad_alloc
int *p2 = new (nothrow) int; // при неудаче оператор new возвращает
// нулевой указатель
По причинам, рассматриваемым в разделе 19.1.2, эта форма оператора new упоминается как размещающий оператор new (placement new). Выражение размещающего оператора new позволяет передать дополнительные аргументы. В данном случае передается определенный библиотекой объект nothrow. Передача объекта nothrow оператору new указывает, что он не должен передавать исключения. Если эта форма оператора new окажется неспособна зарезервировать требуемый объем памяти, она возвратит нулевой указатель. Типы bad_alloc и nothrow определены в заголовке new.
Освобождение динамической памяти
Чтобы предотвратить исчерпание памяти, по завершении использования ее следует возвратить операционной системе. Для этого используется оператор delete, получающий указатель на освобождаемый объект:
delete p; // p должен быть указателем на динамически созданный объект
// или нулевым указателем
Подобно оператору new, оператор delete выполняет два действия: удаляет объект, на который указывает переданный ему указатель, и освобождает соответствующую область памяти.
Значения указателя и оператор delete
Передаваемый оператору delete указатель должен либо указывать на динамически созданный объект, либо быть нулевым указателем (см. раздел 2.3.2). Результат удаления указателя на область памяти, зарезервированную не оператором new, или повторного удаления значения того же указателя непредсказуем:
int i, *pi1 = &i, *pi2 = nullptr;
double *pd = new double(33), *pd2 = pd;
delete i; // ошибка: i - не указатель
delete pi1; // непредсказуемо: pi1 - локальный
delete pd; // ok
delete pd2; // непредсказуемо: память, на которую указывает pd2,
// уже освобождена
delete pi2; // ok: освобождение нулевого указателя всегда допустимо
Компилятор сообщает об ошибке оператора delete i, поскольку знает, что i — не указатель. Ошибки, связанные с выполнением оператора delete для указателей pi1 и pd2, коварней: обычно компиляторы неспособны выяснить, указывает ли указатель на объект, созданный статически или динамически. Точно так же компилятор не может установить, была ли уже освобождена память, на которую указывает указатель. Большинство компиляторов примет такие выражения delete, несмотря на их ошибочность.
Хотя значение константного объекта не может быть изменено, сам объект вполне может быть удален. Подобно любым динамическим объектам, константный динамический объект освобождается выполнением оператора delete для указателя, указывающего на этот объект:
const int *pci = new const int(1024);
delete pci; // ok: удаляет константный объект
Динамически созданные объекты существуют до тех пор, пока не будут освобождены
Как упоминалось в разделе 12.1.1, управляемая указателем shared_ptr память автоматически освобождается при удалении последнего указателя shared_ptr. Динамический объект, управляемый указателем встроенного типа, существует до тех пор, пока к областям памяти, управляемой при помощи указателей встроенных типов, не будет удален явно.
Функции, возвращающие обычные (а не интеллектуальные) указатели на области динамической памяти, возлагают ответственность за их удаление на вызывающую сторону:
// возвращает указатель на динамически созданный объект
Foo* factory(Т arg) {
// обработать аргумент соответственно
return new Foo(arg); // за освобождение этой памяти отвечает
// вызывающая сторона
}
Подобно прежней версии функции factory() (см. раздел 12.1.1), эта версия резервирует объект, но не удаляет его. Ответственность за освобождение памяти динамического объекта, когда он станет больше не нужен, несет вызывающая сторона функции factory(). К сожалению, вызывающая сторона слишком часто забывает сделать это:
void use_factory(Т arg) {
Foo *p = factory(arg);
// использовать p, но не удалить его
} // p выходит из области видимости, но память,
// на которую он указывает, не освобождается!
Здесь функция use_factory() вызывает функцию factory() резервирующую новый объект типа Foo. Когда функция use_factory() завершает работу, локальная переменная p удаляется. Эта переменная — встроенный указатель, а не интеллектуальный.
В отличие от классов, при удалении объектов встроенного типа не происходит ничего. В частности, когда указатель выходит из области видимости, с объектом, на который он указывает, ничего не происходит. Если этот указатель указывает на динамическую память, она не освобождается автоматически.
В этом примере указатель p был единственным указателем на область памяти, зарезервированную функцией factory(). По завершении функции use_factory() у программы больше нет никакого способа освободить эту память. Согласно общей логике программирования, следует исправить эту ошибку и напомнить о необходимости освобождения памяти в функции use_factory():
void use_factory(Т arg) {
Foo *p = factory(arg);
// использование p
delete p; // не забыть освободить память сейчас, когда
// она больше не нужна
}
Если созданный функцией use_factory() объект должен использовать другой код, то эту функцию следует изменить так, чтобы она возвращала указатель на зарезервированную ею память:
Foo* use_factory(Т arg) {
Foo *p = factory(arg);
// использование p
return p; // освободить память должна вызывающая сторона
}
Внимание! Управление динамической памятью подвержено ошибкам
Есть три общеизвестных проблемы, связанных с использованием операторов new и delete при управлении динамической памятью:
1. Память забыли освободить. Когда динамическая память не освобождается, это называется "утечка памяти", поскольку она уже не возвращается в пул динамической памяти. Проверка утечек памяти очень трудна, поскольку она обычно не проявляется, пока приложение, проработав достаточно долго, фактически не исчерпает память.
2. Объект использован после удаления. Иногда эта ошибка обнаруживается при создании нулевого указателя после удаления.
3. Повторное освобождение той же памяти. Эта ошибка может произойти в случае, когда два указателя указывают на тот же динамически созданный объект. Если оператор delete применен к одному из указателей, то память объекта возвращается в пул динамической памяти. Если впоследствии применить оператор delete ко второму указателю, то динамическая память может быть нарушена.
Допустить эти ошибки значительно проще, чем потом найти и исправить.
Переустановка значения указателя после удаления…
Когда указатель удаляется, он становится недопустимым. Но, даже став недопустимым, на многих машинах он продолжает содержать адрес уже освобожденной области динамической памяти. После освобождения области памяти указатель на нее становится потерянным указателем (dangling pointer). Потерянный указатель указывает на ту область памяти, которая когда-то содержала объект, но больше не содержит.
Потерянным указателям присущи все проблемы неинициализированных указателей (см. раздел 2.3.2). Проблем с потерянными указателями можно избежать, освободив связанную с ними память непосредственно перед выходом из области видимости самого указателя. Так не появится шанса использовать указатель уже после того, как связанная с ним память будет освобождена. Если указатель необходимо сохранить, то после применения оператора delete ему можно присвоить значение nullptr. Это непосредственно свидетельствует о том, что указатель не указывает на объект.
…обеспечивает лишь частичную защиту
Фундаментальная проблема с динамической памятью в том, что может быть несколько указателей на ту же область памяти. Переустановка значения указателя при освобождении памяти позволяет проверять допустимость данного конкретного указателя, но никак не влияет на все остальные указатели, все еще указывающие на уже освобожденную область памяти. Рассмотрим пример:
int *p(new int(42)); // p указывает на динамическую память
auto q = p; // p и q указывают на ту же область памяти
delete p; // делает недопустимыми p и q
p = nullptr; // указывает, что указатель p больше не связан с объектом
Здесь указатели p и q указывают на тот же динамически созданный объект. Удалим этот объект и присвоим указателю p значение nullptr, засвидетельствовав, что он больше не указывает на объект. Однако переустановка значения указателя p никак не влияет на указатель q, который стал недопустимым после освобождения памяти, на которую указывал указатель p (и указатель q!). В реальных системах поиск всех указателей на ту же область памяти зачастую на удивление труден.
Упражнения раздела 12.1.2
Упражнение 12.6. Напишите функцию, которая возвращает динамически созданный вектор целых чисел. Передайте этот вектор другой функции, которая читает значения его элементов со стандартного устройства ввода. Передайте вектор другой функции, выводящей прочитанные ранее значения. Не забудьте удалить вектор в подходящий момент.
Упражнение 12.7. Переделайте предыдущее упражнение, используя на сей раз указатель shared_ptr.
Упражнение 12.8. Объясните, все ли правильно в следующей функции:
bool b() {
int* p = new int;
// ...
return p;
}
Упражнение 12.9. Объясните, что происходит в следующем коде:
int *q = new int(42), *r = new int(100);
r = q;
auto q2 = make_shared<int>(42), r2 = make_shared<int>(100);
r2 = q2;