Следует отметить, что в случае с C ++ распространенным заблуждением является то, что «вам необходимо выполнить ручное управление памятью». Фактически, вы обычно не управляете памятью в своем коде.
Объекты фиксированного размера (с продолжительностью жизни)
В подавляющем большинстве случаев, когда вам нужен объект, он будет иметь определенное время жизни в вашей программе и будет создан в стеке. Это работает для всех встроенных примитивных типов данных, но также для экземпляров классов и структур:
class MyObject {
public: int x;
};
int objTest()
{
MyObject obj;
obj.x = 5;
return obj.x;
}
Стековые объекты автоматически удаляются при завершении функции. В Java объекты всегда создаются в куче, и поэтому должны быть удалены каким-либо механизмом, например сборкой мусора. Это не проблема для объектов стека.
Объекты, которые управляют динамическими данными (с временем жизни области)
Использование пространства в стеке работает для объектов фиксированного размера. Когда вам нужен переменный объем пространства, например, массив, используется другой подход: список инкапсулируется в объект фиксированного размера, который управляет динамической памятью для вас. Это работает, потому что объекты могут иметь специальную функцию очистки, деструктор. Он гарантированно вызывается, когда объект выходит из области видимости и делает противоположное конструктору:
class MyList {
public:
// a fixed-size pointer to the actual memory.
int* listOfInts;
// constructor: get memory
MyList(size_t numElements) { listOfInts = new int[numElements]; }
// destructor: free memory
~MyList() { delete[] listOfInts; }
};
int listTest()
{
MyList list(1024);
list.listOfInts[200] = 5;
return list.listOfInts[200];
// When MyList goes off stack here, its destructor is called and frees the memory.
}
В коде, где используется память, управление памятью вообще отсутствует. Единственное, что нам нужно, это убедиться, что у написанного нами объекта есть подходящий деструктор. Независимо от того, как мы покидаем область действия listTest
, будь то через исключение или просто возвращаясь из него, вызывается деструктор ~MyList()
, и нам не нужно управлять какой-либо памятью.
(Я думаю, что это забавное дизайнерское решение использовать двоичный оператор NOT~
для обозначения деструктора. При использовании над числами он инвертирует биты; по аналогии, здесь он указывает, что то, что сделал конструктор, инвертировано.)
В основном все объекты C ++, которым требуется динамическая память, используют эту инкапсуляцию. Он был назван RAII («получение ресурсов - инициализация»), что является довольно странным способом выразить простую идею о том, что объекты заботятся о своем собственном содержимом; что они приобретают, так это их убирать.
Полиморфные объекты и время жизни за пределами видимости
Теперь оба этих случая касались памяти с четко определенным временем жизни: время жизни совпадает с областью действия. Если мы не хотим, чтобы срок действия объекта истек при выходе из области действия, существует третий механизм, который может управлять памятью для нас: умный указатель. Умные указатели также используются, когда у вас есть экземпляры объектов, тип которых меняется во время выполнения, но которые имеют общий интерфейс или базовый класс:
class MyDerivedObject : public MyObject {
public: int y;
};
std::unique_ptr<MyObject> createObject()
{
// actually creates an object of a derived class,
// but the user doesn't need to know this.
return std::make_unique<MyDerivedObject>();
}
int dynamicObjTest()
{
std::unique_ptr<MyObject> obj = createObject();
obj->x = 5;
return obj->x;
// At scope end, the unique_ptr automatically removes the object it contains,
// calling its destructor if it has one.
}
Существует еще один вид интеллектуального указателя std::shared_ptr
, предназначенный для совместного использования объектов несколькими клиентами. Они удаляют свой содержащийся объект только тогда, когда последний клиент выходит из области видимости, поэтому их можно использовать в ситуациях, когда совершенно неизвестно, сколько будет клиентов и как долго они будут использовать этот объект.
Таким образом, мы видим, что вы на самом деле не делаете никакого ручного управления памятью. Все заключено в капсулу, а затем об этом заботятся с помощью полностью автоматического управления памятью на основе области действия. В случаях, когда этого недостаточно, используются интеллектуальные указатели, которые инкапсулируют необработанную память.
Считается крайне плохой практикой использовать необработанные указатели в качестве владельцев ресурсов где-либо в коде C ++, необработанные выделения вне конструкторов и необработанные delete
вызовы вне деструкторов, поскольку ими практически невозможно управлять при возникновении исключений и, как правило, трудно безопасно использовать.
Лучшее: это работает для всех типов ресурсов
Одним из самых больших преимуществ RAII является то, что он не ограничивается памятью. На самом деле он предоставляет очень естественный способ управления ресурсами, такими как файлы и сокеты (открытие / закрытие) и механизмами синхронизации, такими как мьютексы (блокировка / разблокировка). По сути, каждый ресурс, который может быть получен и должен быть освобожден, управляется точно так же в C ++, и ни одно из этого управления не остается на усмотрение пользователя. Все это заключено в классы, которые получают в конструкторе и освобождают в деструкторе.
Например, функция, блокирующая мьютекс, обычно пишется так в C ++:
void criticalSection() {
std::scoped_lock lock(myMutex); // scoped_lock locks the mutex
doSynchronizedStuff();
} // myMutex is released here automatically
Другие языки делают это намного более сложным, требуя, чтобы вы делали это вручную (например, в finally
предложении), или они порождают специализированные механизмы, которые решают эту проблему, но не особенно изящно (обычно позже в их жизни, когда достаточно людей пострадал от недостатка). Такие механизмы - это try-with-resources в Java и оператор using в C #, оба они являются приближением RAII C ++.
Подводя итог, можно сказать, что все это было очень поверхностным описанием RAII в C ++, но я надеюсь, что это поможет читателям понять, что управление памятью и даже ресурсами в C ++ обычно не «ручное», а фактически автоматическое.