Слід зазначити, що у випадку 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
пункті), або вони породили спеціалізовані механізми, які вирішують цю проблему, але не особливо елегантним способом (як правило, пізніше в їхньому житті, коли достатньо людей страждав від недоліку). Такі механізми є пробними ресурсами на Java та використовуючим оператором в C #, обидва з яких є наближеннями RAII C ++.
Отже, підсумовуючи це, все це було дуже поверхневим обліковим записом RAII в C ++, але я сподіваюся, що це допомагає читачам зрозуміти, що управління пам’яттю та навіть управління ресурсами в C ++ зазвичай не є «ручним», а насправді здебільшого автоматичним.