Можно ли Конструктор и Деструктор вызывать как метод класса?
Как правильно вызывать конструктор шаблонного класса?
Как правильно вызывать конструктор класса? template <class T> class A< T *v; int dim;.
Можно ли явным образом вызывать деструктор?
Например. Имеется перегруженный в классе оператор присваивания: square_matrix.
Конструктор и деструктор анонимного класса
Здравствуйте. Есть ли в С++ такая возможность? Очень нужна именно такая реализация класса, но если.
Зачем нужны конструктор и деструктор класса?
вот задание: Пользовательский класс Х должен содержать необходимые элементы-данные, которые.
603 / 923 / 149
Регистрация: 10.08.2015
Сообщений: 4,864
а зачем? вызывая конструктор вы создаете объект, который потом уничтожит деструктор создавать объекты можно в любом месте
Комп_Оратор)
8927 / 4684 / 626
Регистрация: 04.12.2011
Сообщений: 13,941
Записей в блоге: 16
Сообщение от Artem_Pv
Можно ли Конструктор и Деструктор вызывать как метод класса?
Они так и вызываются. Иначе их не вызовешь. Разве что, конструктор можно без полной квалификации вызывать.
А если говорить о методе экземпляра, то:
конструктор нельзя, а деструктор можно.
Это потому, что методы вызываются на объектах, а при вызове конструктора ещё нет объекта. Для порождения объекта объектом пишут методы (которые обычно используют конструктора), такие как Clone и пр.
Регистрация: 31.03.2019
Сообщений: 144
Сообщение от vlisp
вызывая конструктор вы создаете объект,
Пожалуйста, поясните.
Вроде, у Страуструпа написано, что конструктор НЕ создает объект, а лишь инициирует его поля.
Хотелось бы разобраться.
Заранее спасибо.
Добавлено через 6 минут
Спасибо что ответили.
Сообщение от IGPIGP
конструктор можно без полной квалификации вызывать.
Пожалуйста, поясните на примере.
Предположим, один из конструкторов содержит полезный функционал. И после того, как уже объект создан, как вручную вызвать этот конструктор, чтоб он отработал как обычный метод класса?
Добавлено через 4 минуты
Сообщение от IGPIGP
при вызове конструктора ещё нет объекта.
Речь идет, когда объект уже создан (один из его конструкоров отработал).
Вопрос: Далее(имея объект), можно ли конструкторы вызывать как обычные методы?
P.S.
Извиняюсь, я сразу не объснил ситуацию. Моя ошибка.
Комп_Оратор)
8927 / 4684 / 626
Регистрация: 04.12.2011
Сообщений: 13,941
Записей в блоге: 16
Сообщение от Artem_Pv
Пожалуйста, поясните на примере.
1 2 3 4
struct A{} /////////// A::A(); Foo(A::A(a,b,c);
Это возможно, хотя и излишество. Я написал об этом, что бы вы поняли, что конструктор это метод класса. Именно поэтому он вызывается как метод класса.
Сообщение от Artem_Pv
после того, как уже объект создан, как вручную вызвать этот конструктор
Вы создадите новый объект и от этого ни куда не деться.
Сообщение от Artem_Pv
Предположим, один из конструкторов содержит полезный функционал.
В принципе, это возможно как для конструкторов, так и для деструкторов. Однако, не применяется широко из-за того, что серьёзный функционал трудно создать без возможности генерации исключений. Для конструкторов это не великая радость, а для деструкторов исключения недопустимы.
603 / 923 / 149
Регистрация: 10.08.2015
Сообщений: 4,864
Сообщение от Artem_Pv
у Страуструпа написано, что конструктор НЕ создает объект, а лишь инициирует его поля.
потому что нет никаких объектов. и полей никаких нет. есть область в памяти, на которую тебе дается ссылка при вызове конструктора. вот называй ее объектом
Комп_Оратор)
8927 / 4684 / 626
Регистрация: 04.12.2011
Сообщений: 13,941
Записей в блоге: 16
Сообщение от Artem_Pv
у Страуструпа написано, что конструктор НЕ создает объект, а лишь инициирует его поля
Вообще, это можно по разному сформулировать. Страуструп имеет приоритет и авторитет вне обсуждений. Однако, создание, это процесс, а конструктор это чертёж. Конечно чертёж не создаёт. Система создаёт. Для размещающего new это видно явно:
void * ptr = new char[sizeof(MyClass)];//выделяем MyClass * myClassPtr=new(ptr) MyClass;// инициализируем
мы пахали — я и компилятор.
На стеке объект создаётся так, что фаза выделения не видна. Но в целом, объект создаётся механизмами программно-аппаратного комплекса и представляет собой набор зарядов в полупроводниковом наборе ячеек.
Ни кто не может сказать, что Б. Страуструп не прав. -Прав.
Регистрация: 31.03.2019
Сообщений: 144
Сообщение от IGPIGP
Вы создадите новый объект и от этого ни куда не деться.
Пожалуйста, проверьте пример:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
class my_Class { public: my_Class() { cout "Работает конструктор"; } }; int main() { my_Class o_Class(); // здесь создается объект o_Class.my_Class(); // у созданного объекта(o_Class) запускаем конструктор(как обычный метод). // Это корректно? // Второй объект не создается? }
603 / 923 / 149
Регистрация: 10.08.2015
Сообщений: 4,864
Сообщение от Artem_Pv
// здесь создается объект
открой книжки по с++
18413 / 9584 / 2341
Регистрация: 30.01.2014
Сообщений: 16,742
Сообщение от Artem_Pv
Это корректно?
Сообщение от Artem_Pv
здесь создается объект
603 / 923 / 149
Регистрация: 10.08.2015
Сообщений: 4,864
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
#include class my_Class { public: my_Class() { std::cout "Работает конструктор\n"; } ~my_Class() { std::cout "Работает деструктор\n"; } public: void my_metod () { std::cout "Работает метод\n"; } }; int main() { my_Class* o1_Class = new my_Class(); // здесь создается указатель на первый объект o1_Class->my_metod(); delete o1_Class; my_Class o2_Class = my_Class(); // здесь создается объект o2_Class.my_metod(); // тут неявно вызывается деструктор для второго объекта }
18413 / 9584 / 2341
Регистрация: 30.01.2014
Сообщений: 16,742
Сообщение было отмечено Artem_Pv как решение
Решение
Сообщение от Artem_Pv
И после того, как уже объект создан, как вручную вызвать этот конструктор, чтоб он отработал как обычный метод класса?
После того как уже создан — никак. В C++11 можно вызывать один конструктор из другого в списке его инициализации. Попадает это под ваш вопрос или нет — не знаю, но как обычный метод точно нигде и никогда его вызывать нельзя.
Вызов деструктора в конструкторе
Кошеrно ли вызывать деструктор объекта из его же конструктора? Если нет, то как нужно поступить, если нужно создать объект, заставить его выполнить действия и по окончанию их убиться?
MyClass myObject = MyClass(someParam); // Тут объект уже должен быть мертвым, выполнив свои действия
inn
22.07.13 21:25:17 MSK
Забабахай статический метод, будь мужиком.
cdshines ★★★★★
( 22.07.13 21:26:20 MSK )
Ответ на: комментарий от cdshines 22.07.13 21:26:20 MSK
Эээ.. Сделать статическим деструктор или все остальные методы? Просто в конструкторе вызывается ряд методов, которые готовят данные. На любом из этапов может быть вызван exit(0) (ну или исключение, сейчас кидается фуллстоп для простоты). Если все «подготовительные этапы» прошли успешно, выполняется действие и объект должен самоуничтожиться.
inn
( 22.07.13 21:32:50 MSK ) автор топика
MyClass(someParam);
Begemoth ★★★★★
( 22.07.13 21:35:46 MSK )
Воспользуйся функцией вместо объекта.
anonymous
( 22.07.13 21:36:19 MSK )
void Foo() < < MyClass myObject = MyClass(someParam); >// some code >
Amp ★★★
( 22.07.13 21:36:55 MSK )
Ответ на: комментарий от inn 22.07.13 21:32:50 MSK
Если время жизни экземпляра не превышает время жизни его конструктора, задумайся над персмотром архитектуры. Если суть методов объекта сводится к выполнению какой-то работы один раз при конструировании, сделай их статическими и вызывай из конструктора (хотя это, опять жеЮ ниочинь подход, но я видел такие случаи — клас — просто обертка для набора функций). Тебе же объект не нужен по сути.
cdshines ★★★★★
( 22.07.13 21:38:17 MSK )
Последнее исправление: cdshines 22.07.13 21:39:35 MSK (всего исправлений: 1)
Ответ на: комментарий от anonymous 22.07.13 21:36:19 MSK
В комплекте идут хэш-данные, списки и рекурсивное создание таких же объектов внутри него. Так что просто функцией тут не обойтись.
inn
( 22.07.13 21:38:44 MSK ) автор топика
Ответ на: комментарий от inn 22.07.13 21:38:44 MSK
Если нужен доступ к внутренностям этого класса, то эту функцию можно зафрендить с этим классом.
anonymous
( 22.07.13 21:42:42 MSK )
Ответ на: комментарий от cdshines 22.07.13 21:38:17 MSK
Хорошо. О пересмотре архитертуры. Вот сейчас оно примерно так:
// xxx.h #include . class Xxx : public QObject
// xxx.h #include . Xxx::Xxx(QString FileName) < Func1(. ); // May be killed here Func2 . // Or here . // Or even here Func5 // But the most probably here run(void); >Xxx::run(void) < // cycle for commands // and if we've find the command we didn't load yet - load it: child = new Xxx("Path/To/Command/File/And/Its/File/Name.xxx"); // May be recursive for more than N levels. >
inn
( 22.07.13 21:50:31 MSK ) автор топика
Ответ на: комментарий от inn 22.07.13 21:50:31 MSK
Ты не показал не одной причины использовать объект вместо функции.
anonymous
( 22.07.13 22:11:05 MSK )
Ответ на: комментарий от anonymous 22.07.13 22:11:05 MSK
То, что для его работы потребуется
private: QList * commands;
А для каждого экземпляра объекта они свои. поэтому сделать глобальными — не айс.
inn
( 22.07.13 22:13:12 MSK ) автор топика
Ответ на: комментарий от inn 22.07.13 22:13:12 MSK
На стеке этот список определи в рекурсивной функции. Еще причины есть?
anonymous
( 22.07.13 22:19:20 MSK )
Ответ на: комментарий от anonymous 22.07.13 22:19:20 MSK
1) Стек не лопнет, если я её рекурсивно overдофига раз вызову?
2) В C++ НЕТ вложенных функций (это костыль), поэтому одиночную функцию придется сопровождать еще добрым десятком того, что сейчас в private.
inn
( 22.07.13 22:27:11 MSK ) автор топика
Ответ на: комментарий от inn 22.07.13 22:27:11 MSK
Стек не лопнет, потому что данные все равно будут в куче. Функции положи в файл реализации и сделай их static.
anonymous
( 22.07.13 22:30:55 MSK )
Ответ на: комментарий от anonymous 22.07.13 22:30:55 MSK
А суровые бородатые дяди в свитерах не будут тыкать меня вилами с надписью ООП за такую реализацию?
inn
( 22.07.13 22:34:23 MSK ) автор топика
Ответ на: комментарий от inn 22.07.13 22:34:23 MSK
Но если кто-то откроет рот, то сразу засунь их в какой нибудь namespace. 🙂
anonymous
( 22.07.13 22:37:00 MSK )
Ответ на: комментарий от inn 22.07.13 21:32:50 MSK
обьясните мне плз что здесь имеется в виду?
Как вызвать деструктор для обобщенного типа
Здравствуйте. У меня проблема с вызовом деструктора для обобщенного типа. Такой код:
1 2 3 4 5 6 7 8
class MyClassTData> { public void DestroyMyClass() { ~MyClassTData>(); // пишет, что тут ошибка //~MyClass(); // на вызов такого рода тоже ругается } {
подскажите, пожалуйста, в чем проблема, и как выполнить какие-либо операции непосредственно перед удалением экземпляра класса MyClass?
94731 / 64177 / 26122
Регистрация: 12.04.2006
Сообщений: 116,782
Ответы с готовыми решениями:
Вызов оператора false для объекта обобщенного типа
Добрый вечер. По заданию нужно было перегрузить оператор false(как понимаю, с этим я справился). Но.
Объявление обобщённого типа (Generic) для стандартных целочисленных типов
Здравствуйте. Есть класс для вычисления факториала: public class Factorial < .
Как вернуть из метода экземпляр обобщенного типа
У меня есть фабрика которая наследует абстрактную обобщенную фабрику. class.
Как вызвать из одного класса (для метода типа void) метод типа boolean другого класса?
ребят, только учусь и конечно, вопросов возникает много. ситуация вот в чем. есть массив. я должна.
2136 / 1505 / 565
Регистрация: 05.04.2010
Сообщений: 2,881
Программист не может управлять моментом вызова деструктора, потому что момент вызова определяется сборщиком мусора. Сборщик мусора проверяет наличие объектов, которые больше не используются приложением. Если считает, что какой-либо объект требует уничтожения, то он вызывает деструктор (при наличии) и освобождает память, используемую для хранения этого объекта. Деструкторы также вызываются при выходе из программы.
Существует возможность принудительно выполнить сборку мусора, вызвав метод Collect, но в большинстве случаев этого следует избегать, потому что это может привести с проблемам с производительностью.
Сообщение от Zarc
как выполнить какие-либо операции непосредственно перед удалением экземпляра класса MyClass?
Описать эти действия в деструкторе класса:
1 2 3 4 5 6 7
class MyClassTData> { ~MyClass() { // Нужные действия } }
Деструкторы / FAQ C++
Деструкторы используются для высвобождения любых ресурсов, выделенных объектом. Например, класс Lock может заблокировать семафор, и деструктор освободит этот семафор. Самый распространенный пример – когда конструктор использует new , а деструктор использует delete .
Деструкторы – это функция-член «подготовки к смерти». Часто их называют сокращенно «dtor».
В каком порядке разрушаются локальные объекты?
В порядке, обратном созданию: первым создан – последним разрушен.
В следующем примере сначала будет выполнен деструктор b , а затем деструктор a :
void userCode() < Fred a; Fred b; // . >
В каком порядке разрушаются объекты в массиве?
В порядке, обратном созданию: первым создан – последним разрушен.
В следующем примере порядок деструкторов будет a[9] , a[8] ,…, a[1] , a[0] :
void userCode() < Fred a[10]; // . >
В каком порядке разрушаются подобъекты объекта?
В порядке, обратном созданию: первым создан – последним разрушен.
Выполняется тело деструктора объекта, за которым следуют деструкторы членов данных объекта (в обратном порядке их появления в определении класса), за которыми следуют деструкторы базовых классов объекта (в обратном порядке их появления в определении класса).
В следующем примере порядок вызовов деструктора, когда d выходит за пределы области видимости, будет ~local1() , ~local0() , ~member1() , ~member0() , ~base1() , ~base0() :
struct base0 < ~base0(); >; struct base1 < ~base1(); >; struct member0 < ~member0(); >; struct member1 < ~member1(); >; struct local0 < ~local0(); >; struct local1 < ~local1(); >; struct derived: base0, base1 < member0 m0_; member1 m1_; ~derived() < local0 l0; local1 l1; >> void userCode()
Могу ли я перегрузить деструктор для своего класса?
У вас может быть только один деструктор для класса Fred . Он всегда называется Fred::~Fred() . Он никогда не принимает никаких параметров и ничего не возвращает.
В любом случае вы не можете передавать параметры деструктору, поскольку вы никогда явно его не вызываете (ну, почти никогда).
Должен ли я явно вызывать деструктор локальной переменной?
Деструктор будет вызван снова при закрытии > блока, в котором был создан локальный объект. Это гарантия языка; это происходит автоматически; нет никакого способа предотвратить это. Но вы можете получить действительно плохие результаты, вызвав деструктор для того же объекта второй раз! Взрыв! Вы мертвы!
Что, если я хочу, чтобы локальный объект «умер» до закрытия > области, в которой он был создан? Могу ли я вызвать деструктор локального объекта, если действительно хочу это сделать?
Предположим, что (желательным) побочным эффектом разрушения локального объекта File является закрытие файла. Теперь предположим, что у вас есть объект f класса File , и вы хотите, чтобы файл f был закрыт до конца области (т.е. до > ) объекта f :
void someCode() < File f; // . код, который должен выполниться, пока f еще открыт. ← Здесь нам нужны побочные эффекты выполнения деструктора f! // . код, который должен выполниться после закрытия f. >
Ладно, ладно, уже; я не буду явно называть деструктор локального объекта; но как мне справиться с ситуацией из предыдущего вопроса?
Просто оберните время жизни локального объекта в искусственный блок <. >:
void someCode() < < File f; // . код, который должен выполниться, пока f еще открыт. >↑ // Здесь автоматически вызывается деструктор f! // . код, который должен выполниться после закрытия f. >
Что делать, если я не могу обернуть локальный объект в искусственный блок?
В большинстве случаев вы можете ограничить время жизни локального объекта, заключив его в искусственный блок ( <. >). Но если по какой-то причине вы не можете этого сделать, добавьте функцию-член, которая имеет тот же эффект, что и деструктор. Но не вызывайте сам деструктор!
Например, в случае класса File вы можете добавить метод close() . Обычно деструктор просто вызывает этот метод close() . Обратите внимание, что метод close() должен будет пометить объект File , чтобы последующий вызов не закрывал уже закрытый File . Например, он может установить член данных fileHandle_ на какое-то бессмысленное значение, например -1, и может вначале проверить, равен ли fileHandle_ уже -1:
class File < public: void close(); ~File(); // . private: int fileHandle_; // fileHandle_ >= 0 если /только если файл открыт >; File::~File() < close(); >void File::close() < if (fileHandle_ >= 0) < // . код, который вызывает ОС, чтобы закрыть файл. fileHandle_ = -1; >>
Обратите внимание, что другим методам File также может потребоваться проверить, имеет ли fileHandle_ значение -1 (т.е. проверить, закрыт ли файл).
Также обратите внимание, что любые конструкторы, которые на самом деле не открывают файл, должны установить для fileHandle_ значение -1.
Но могу ли я явно вызвать деструктор, если я разместил свой объект с помощью new ?
Если вы не использовали размещение new , вам следует просто удалить объект, используя delete , а не явно вызывать деструктор. Например, предположим, что вы разместили объект с помощью типового выражения new :
Fred* p = new Fred();
Тогда деструктор Fred::~Fred() будет автоматически вызван, когда вы удалите его через:
delete p; // автоматически вызывает p->~Fred()
Вы не должны явно вызывать деструктор, поскольку это не освобождает память, выделенную для самого объекта Fred . Помните: delete p выполняет две функции: вызывает деструктор и освобождает память.
Что такое «размещение new » и зачем его использовать?
Есть много применений размещения new . Самый простой способ – поместить объект в определенном месте в памяти. Это делается путем предоставления места в качестве параметра указателя части new выражения new :
#include // чтобы использовать "размещение new", необходимо включить этот файл #include "Fred.h" // объявление класса Fred void someCode() < char memory[sizeof(Fred)]; // строка #1 void* place = memory; // строка #2 Fred* f = new(place) Fred(); // строка #3 (смотрите "ОПАСНОСТЬ" ниже) // указатели f и place будут равны // . >
Строка #1 создает массив размером sizeof(Fred) байтов памяти, который достаточно велик для размещения объекта Fred . Строка #2 создает указатель place , который указывает на первый байт этой памяти (опытные программисты на C заметят, что этот шаг не нужен; он нужен только для того, чтобы сделать код более очевидным). Строка #3 просто вызывает конструктор Fred::Fred() . Указатель this в конструкторе Fred будет равен указателю place . Таким образом, возвращаемый указатель f будет равен place .
СОВЕТ. Без необходимости не используйте синтаксис «размещения new ». Используйте его только тогда, когда вам действительно важно, чтобы объект был помещен в определенное место в памяти. Например, если ваше оборудование имеет устройство таймера ввода-вывода с отображением в памяти, и вы хотите разместить объект Clock в этом месте памяти.
ОПАСНОСТЬ. Вы несете исключительную ответственность за то, чтобы указатель, который вы передаете оператору «размещение new », указывал на область памяти, которая достаточно велика и правильно выровнена для типа объекта, который вы создаете. Ни компилятор, ни система времени выполнения не пытаются проверить, правильно ли вы это сделали. Если ваш класс Fred нужно выровнять по 4-байтовой границе, но вы указали местоположение, которое не выровнено должным образом, у вас может возникнуть серьезная катастрофа (если вы не знаете, что означает «выравнивание», пожалуйста, не используйте синтаксис размещения new ). Вы предупреждены.
Вы также несете полную ответственность за уничтожение размещенного объекта. Это делается явным вызовом деструктора:
void someCode() < char memory[sizeof(Fred)]; void* p = memory; Fred* f = new(p) Fred(); // . f->~Fred(); // явно вызывает деструктор для размещенного объекта >
Это примерно единственный раз, когда вы явно вызываете деструктор.
Примечание: существует более чистый, но более изощренный способ справиться с задачей уничтожения/удаления.
Существует ли размещение delete ?
Нет, но если оно вам необходимо, вы можете написать свое собственное.
Рассмотрим размещение new , используемое для размещения объектов в наборе арен:
class Arena < public: void* allocate(size_t); void deallocate(void*); // . >; void* operator new(size_t sz, Arena& a) < return a.allocate(sz); >Arena a1(some arguments); Arena a2(some arguments);
Учитывая это, мы можем написать
X* p1 = new(a1) X; Y* p2 = new(a1) Y; Z* p3 = new(a2) Z; // .
Но как потом правильно удалить эти объекты? Причина отсутствия встроенного «размещения delete », соответствующего размещению new , заключается в том, что нет общего способа гарантировать, что оно будет использоваться правильно. Ничто в системе типов C++ не позволяет нам сделать вывод, что p1 указывает на объект, размещенный в Arena a1 . Указатель на любой X , размещенный где угодно, может быть назначен указателю p1 .
Однако иногда программист знает, способ есть:
template void destroy(T* p, Arena& a) < if (p) < p->~T(); // явно вызывает деструктор a.deallocate(p); > >
Теперь мы можем написать:
destroy(p1,a1); destroy(p2,a2); destroy(p3,a3);
Если Arena отслеживает, какие объекты она содержит, вы даже можете написать destroy() , чтобы защитить себя от ошибок.
Также возможно определить пары операторов operator new() и operator delete() для иерархии классов TC++PL(SE) 15.6. Смотрите также D&E 10.4 и TC++PL(SE) 19.4.5.
Когда я пишу деструктор, нужно ли мне явно вызывать деструкторы для моих объектов-членов?
Нет. Вам никогда не нужно явно вызывать деструктор (за исключением размещения new ).
Деструктор класса (вне зависимости от того, определили вы его явно или нет) автоматически вызывает деструкторы для объектов-членов. Они уничтожаются в порядке, обратном тому, в котором были указаны в объявлении класса.
class Member < public: ~Member(); // . >; class Fred < public: ~Fred(); // . private: Member x_; Member y_; Member z_; >; Fred::~Fred() < // Компилятор автоматически вызывает z_.~Member() // Компилятор автоматически вызывает y_.~Member() // Компилятор автоматически вызывает x_.~Member() >
Когда я пишу деструктор производного класса, нужно ли мне явно вызывать деструктор для моего базового класса?
Нет. Вам никогда не нужно явно вызывать деструктор (за исключением размещения new ).
Деструктор производного класса (вне зависимости от того, определили вы его явно или нет) автоматически вызывает деструкторы для подобъектов базового класса. Базовые классы уничтожаются после объектов-членов. В случае множественного наследования прямые базовые классы уничтожаются в порядке, обратном их появлению в списке наследования.
class Member < public: ~Member(); // . >; class Base < public: virtual ~Base(); // виртуальный деструктор // . >; class Derived : public Base < public: ~Derived(); // . private: Member x_; >; Derived::~Derived() < // Компилятор автоматически вызывает x_.~Member() // Компилятор автоматически вызывает Base::~Base() >
Примечание. Зависимости порядка с виртуальным наследованием сложнее. Если вы полагаетесь на зависимости порядка в иерархии виртуального наследования, вам понадобится гораздо больше информации, чем в этом FAQ.
Должен ли мой деструктор генерировать исключение при обнаружении проблемы?
Осторожно. Подробности смотрите в ответе на этот вопрос (ссылка скоро появится).
Есть ли способ заставить new выделять память из определенной области памяти?
Да. Хорошей новостью является то, что эти «пулы памяти» полезны в ряде ситуаций. Плохая новость в том, что мне придется протащить вас через болото того, как это работает, прежде чем мы обсудим все применения. Но если вы не знаете о пулах памяти, возможно, стоит потратить время на изучение ответа на этот вопрос – вы можете узнать кое-что полезное!
Прежде всего, напомним, что распределитель памяти должен просто возвращать неинициализированные биты памяти; он не должен создавать «объекты». В частности, распределитель памяти не должен устанавливать виртуальный указатель или любую другую часть объекта, так как это задача конструктора, который запускается после распределителя памяти. Начав с простой функции выделения памяти, allocate() , вы должны использовать размещение new для создания объекта в этой памяти. Другими словами, следующий код эквивалентен new Foo() :
void* raw = allocate(sizeof(Foo)); // строка 1 Foo* p = new(raw) Foo(); // строка 2
Предполагая, что вы использовали размещение new и выжили после двух приведенных выше строк кода, следующим шагом будет превращение вашего распределителя памяти в объект. Такие объекты называют «пулом памяти» или «ареной памяти». Это позволяет вашим пользователям иметь более одного «пула» или «арены», из которых будет выделяться память. Каждый из этих объектов пула памяти будет выделять большой кусок памяти, используя определенный системный вызов (например, разделяемую память, постоянную память, стековую память и т.д.; смотрите ниже), и при необходимости распределяет его небольшими порциями. Ваш класс пула памяти может выглядеть примерно так:
class Pool < public: void* alloc(size_t nbytes); void dealloc(void* p); private: // . члены данных, используемые в вашем объекте пула. >; void* Pool::alloc(size_t nbytes) < // . здесь идет ваш алгоритм. >void Pool::dealloc(void* p) < // . здесь идет ваш алгоритм. >
Теперь у одного из ваших пользователей может быть Pool с именем pool , из которого он может выделять объекты примерно так:
Pool pool; // . void* raw = pool.alloc(sizeof(Foo)); Foo* p = new(raw) Foo();
Foo* p = new(pool.alloc(sizeof(Foo))) Foo();
Превратить Pool в класс полезно потому, что он позволяет пользователям создавать N разных пулов памяти вместо того, чтобы иметь один массивный пул, используемый совместно всеми пользователями. Это позволяет пользователям делать много забавных вещей. Например, если у них есть кусок системы, который как сумасшедший выделяет память, а затем уходит; они могут выделить всю свою память из Pool , а затем даже не беспокоиться об удалении небольших кусков: можно просто освободить весь пул сразу. Или они могут создать область «совместно используемой (shared) памяти» (где операционная система специально предоставляет память, которая совместно используется несколькими процессами) и сделать так, чтобы пул выделял фрагменты общей памяти, а не локальную память процесса. Другой аспект: многие системы поддерживают нестандартную функцию, часто называемую alloca() , которая выделяет блок памяти из стека, а не из кучи. Естественно, этот блок памяти автоматически удаляется при возврате функции, устраняя необходимость в явном удалении. Кто-то может использовать alloca() , чтобы предоставить пулу большой кусок памяти, тогда все маленькие кусочки, выделенные из этого объекта Pool , будут действовать как локальные: они автоматически исчезают, когда происходит возврат из функции. Конечно, в некоторых из этих случаев деструкторы не вызываются, и если деструкторы делают что-то нетривиальное, вы не сможете использовать эти методы, но в случаях, когда деструктор просто освобождает память, такие методы могут быть полезны.
Предположим, что вы пережили 6 или 8 строк кода, необходимых для обертывания вашей функции выделения памяти как метода класса Pool , следующим шагом будет изменение синтаксиса для выделения памяти для объектов. Цель состоит в том, чтобы перейти от довольно неуклюжего синтаксиса new(pool.alloc(sizeof(Foo))) Foo() к более простому синтаксису new(pool) Foo() . Чтобы это произошло, вам нужно добавить следующие две строки кода чуть ниже определения вашего класса Pool :
inline void* operator new(size_t nbytes, Pool& pool)
Теперь, когда компилятор видит new(pool) Foo() , он вызывает вышеуказанный оператор new и передает sizeof(Foo) и pool в качестве параметров, и единственная функция, которая в конечном итоге использует метод pool.alloc(nbytes) , – это ваш собственный оператор new .
Теперь к вопросу о том, как разрушить/высвободить объекты Foo . Напомним, что метод грубой силы, который иногда используется при размещении new , заключается в явном вызове деструктора, а затем явном освобождении памяти:
void sample(Pool& pool) < Foo* p = new(pool) Foo(); // . p->~Foo(); // явно вызывает деструктор pool.dealloc(p); // явно освобождает память >
Здесь есть несколько проблем, и все они решаемы:
- Если Foo::Foo() вызовет исключение, произойдет утечка памяти.
- Синтаксис уничтожения/освобождения отличается от того, к чему привыкло большинство программистов, поэтому они, вероятно, всё испортят.
- Пользователи должны каким-то образом запоминать, какой пул с каким объектом связан. Поскольку код, который выделяет память, часто находится в другой функции, чем код, который освобождает выделенную памяти, программистам придется передавать два указателя ( Foo* и Pool* ), что быстро становится ужасным (например, что, если бы у них был массив объектов Foo , все из которых потенциально пришли из разных объектов Pool ).
Мы исправим эти проблемы в этом же порядке.
Проблема №1: устранение утечки памяти. Когда вы используете «обычный» оператор new , например, Foo* p = new Foo() , компилятор генерирует специальный код для обработки случая, когда конструктор генерирует исключение. Реальный код, сгенерированный компилятором, функционально похож на этот:
// Это то, что функционально происходит при Foo* p = new Foo() Foo* p; // не отлавливает исключения, выкинутые самим распределителем памяти void* raw = operator new(sizeof(Foo)); // отлавливает любые исключения, выкинутые конструктором try < p = new(raw) Foo(); // вызывает конструктор с raw в качестве this >catch (. ) < // упс, конструктор выкинул исключение operator delete(raw); throw; // повторно выкидывает исключение конструктора >
Дело в том, что компилятор освобождает память, если конструктор генерирует исключение. Но в случае синтаксиса « new с параметром» (обычно называемого «размещением new ») компилятор не знает, что делать, если возникает исключение, поэтому по умолчанию он ничего не делает:
// Это то, что функционально происходит при Foo* p = new(pool) Foo(): void* raw = operator new(sizeof(Foo), pool); // функция выше просто возвращает "pool.alloc(sizeof(Foo))" Foo* p = new(raw) Foo(); // если строка выше "выкидывает" исключение, pool.dealloc(raw) НЕ вызывается
Итак, цель состоит в том, чтобы заставить компилятор делать что-то похожее на то, что он делает с глобальным оператором new . К счастью, это просто: когда компилятор видит new(pool) Foo() , он ищет соответствующий оператор delete . Если он его находит, он выполняет эквивалент обертывания вызова конструктора в блок try , как показано выше. Поэтому мы просто предоставим оператор delete со следующей сигнатурой (будьте осторожны, чтобы сделать всё правильно; если второй параметр имеет тип, отличный от второго параметра operator new(size_t, Pool&) , компилятор не пожалуется; он просто обойдет блок try , когда ваши пользователи скажут new(pool) Foo() ):
void operator delete(void* p, Pool& pool)
После этого компилятор автоматически обернет вызовы конструкторов ваших выражений new в блок try :
// Это то, что функционально происходит при Foo* p = new(pool) Foo() Foo* p; // не отлавливает исключения, выкинутые самим распределителем памяти void* raw = operator new(sizeof(Foo), pool); // функция выше просто возвращает "pool.alloc(sizeof(Foo))" // отлавливает любые исключения, выкинутые конструктором try < p = new(raw) Foo(); // вызывает конструктор с raw в качестве this >catch (. ) < // упс, конструктор выкинул исключение operator delete(raw, pool); // вот волшебная строчка!! throw; // повторно выкидывает исключение конструктора >
Другими словами, однострочная функция operator delete(void* p, Pool& pool) заставляет компилятор автоматически закрывать утечку памяти. Конечно, эта функция может быть, но не обязательно, встраиваемой.
Проблемы №2 («уродливо, поэтому подвержено ошибкам») и №3 («пользователи должны вручную связывать указатели пула с объектом, который выделил для них память, что подвержено ошибкам») решаются одновременно с помощью дополнительных 10-20 строк кода в одном место. Другими словами, мы добавляем 10-20 строк кода в одном месте (заголовочный файл Pool ) и упрощаем сколь угодно большое количество других мест (каждый фрагмент кода, который использует ваш класс Pool ).
Идея состоит в том, чтобы неявно связывать Pool* с каждым выделением. Pool* , связанный с глобальным распределителем, будет иметь значение NULL , но, по крайней мере, концептуально можно сказать, что каждое выделение памяти имеет связанный Pool* . Затем вы заменяете глобальный оператор delete , чтобы он находил связанный Pool* , и, если тот не равен NULL , вызывал функцию освобождения объекта Pool . Например, если (!) обычный деаллокатор использует free() , замена глобального оператора delete будет выглядеть примерно так:
void operator delete(void* p) < if (p != NULL) < Pool* pool = /* как-то получить связанный 'Pool*' */; if (pool == NULL) free(p); else pool->dealloc(p); > >
Если вы не уверены, был ли обычный деаллокатор функцией free() , самый простой способ – также заменить глобальный оператор new чем-то, что использует malloc() . Замена глобального оператора new будет выглядеть примерно так (обратите внимание: это определение игнорирует некоторые детали, такие как цикл new_handler и выкидывание исключения throw std::bad_alloc() , которое происходит, если у нас заканчивается память):
void* operator new(size_t nbytes) < if (nbytes == 0) nbytes = 1; // поэтому все выделения памяти получают отдельный адрес void* raw = malloc(nbytes); // . как-то связать NULL 'Pool*' с 'raw'. return raw; >
Единственная оставшаяся проблема – связать Pool* с выделением памяти. Один из подходов, используемых, по крайней мере, в одном коммерческом продукте, заключается в использовании std::map . Другими словами, создайте таблицу поиска, ключи которой – это указатели выделения памяти, а значения – связанные Pool* . По причинам, которые я опишу чуть позже, важно, чтобы вы вставляли пару ключ/значение в карту только в операторе new(size_t, Pool&) . В частности, вы не должны вставлять пару ключ/значение из глобального оператора new (например, вы не должны указывать poolMap[p] = NULL в глобальном операторе new ). Причина: это создало бы неприятную проблему с курицей и яйцом – поскольку std::map , вероятно, использует глобальный оператор new , он заканчивает тем, что вставляет новую запись каждый раз, когда вставляет новую запись, что приводит к бесконечной рекурсии – бах, вы мертвы.
Несмотря на то, что этот метод требует поиска в std::map для каждого освобождения памяти, кажется, что он имеет приемлемую производительность, по крайней мере, во многих случаях.
Другой подход, который быстрее, но может использовать больше памяти и немного сложнее, – это добавить Pool* непосредственно перед всеми выделениями. Например, если nbytes было равно 24, то есть вызывающий запрашивал выделение 24 байтов, мы бы выделили 28 (или 32, если вы думаете, что машине требуется 8-байтовое выравнивание для таких вещей, как double и/или long long ), записали бы значение указателя Pool* в первые 4 байта и вернули бы указатель на 4 (или 8) байта справа от начала того, что выделили. Затем ваш глобальный оператор delete возвращается на 4 (или 8) байта, находит Pool* , и, если тот равен NULL , использует free() , в противном случае вызывает pool->dealloc() . Параметр, переданный в free() и pool->dealloc() , будет указателем на 4 (или 8) байта слева от исходного параметра p . Если (!) вы выберете 4-байтовое выравнивание, ваш код будет выглядеть примерно так (хотя, как и раньше, следующий код оператора new исключает обычные обработчики нехватки памяти):
void* operator new(size_t nbytes) < if (nbytes == 0) nbytes = 1; // поэтому все выделения памяти получают отдельные адреса void* ans = malloc(nbytes + 4); // увеличить выделяемую память на 4 байта *(Pool**)ans = NULL; // использует NULL в глобальном new return (char*)ans + 4; // не позволяет пользователям увидеть Pool* >void* operator new(size_t nbytes, Pool& pool) < if (nbytes == 0) nbytes = 1; // поэтому все выделения памяти получают отдельные адреса void* ans = pool.alloc(nbytes + 4); // увеличить выделяемую память на 4 байта *(Pool**)ans = &pool; // поместить сюда Pool* return (char*)ans + 4; // не позволяет пользователям увидеть Pool* >void operator delete(void* p) < if (p != NULL) < p = (char*)p - 4; // возвращается к Pool* Pool* pool = *(Pool**)p; if (pool == NULL) free(p); // примечание: 4 байта слева от исходного p else pool->dealloc(p); // примечание: 4 байта слева от исходного p > >
Естественно, последние несколько абзацев этого FAQ применимы только в том случае, если вам разрешено изменять глобальные operator new и operator delete . Если вам не разрешено изменять эти глобальные функции, действуют первые три четверти ответа на этот вопрос.