Кодерскіе тіпси і Трікс

Правила кодинга на C ++ для справжніх спеців Продовжуємо вивчати тонкощі управління пам'яттю в C ++. Наступні пара сторінок будуть присвячені поглибленому вивченню операторів new і delete. З них ти дізнаєшся, які вимоги пред'являє стандарт C ++ до призначених для користувача реалізацій цих операторів.

У минулій статті ми встигли розібратися, для чого взагалі потрібно замінювати оператори new і delete своїми версіями, та в яких випадках без цього можна обійтися. Також ми трохи торкнулися теми функцііобработчіка new і обговорили проблему вирівнювання повертаються покажчиків.

Про неї ми ще поговоримо трохи нижче. Крім того, самопісний new повинен повертати правильне значення і коректно обробляти запити на виділення нуля байтів. Також при реалізації власних функцій управління пам'яттю ми повинні подбати про те, щоб не приховати їх «нормальні» форми. Ну, а тепер про все це докладніше.

Угоди при написанні оператора new Насамперед призначений для користувача new повинен повертати правильне значення. У разі успішного виділення пам'яті оператор повинен повернути покажчик на неї. Якщо ж щось пішло не так, слід порушити виняток типу bad_alloc.

Але не все так просто, як здається. Перед винятком new повинен в циклі викликати функцію-обробник, яка спробує розв'язати проблемну ситуацію. Що повинна робити ця функція, ми докладно розглянули в минулій статті. Зараз я лише нагадаю, що вона може вивільнити заздалегідь заготовлений резерв пам'яті в разі її браку, сама порушити виняток або зовсім завершити програму. Вкрай важливо, щоб функція-обробник коректно відпрацювала, оскільки цикл її виклику буде виконуватися до тих пір, поки не буде дозволена конфліктна ситуація. Наступний важливий момент, який ми повинні враховувати - це обробка запитів на виділення нуля байт пам'яті. Як не дивно це звучить, але стандарт C ++ вимагає в цьому випадку коректної роботи оператора. Така поведінка спрощує реалізацію деяких речей в інших місцях мови. Беручи до уваги все це, можна спробувати накидати псевдокод призначеного для користувача new:

Псевдокод користувальницької реалізації оператора new

void * operator new (std :: size_t size)
throw (std :: bad_alloc)
using namespace std;
// обробити запит на 0 байтів,
// вважаючи, що потрібно виділити 1 байт
if (size == 0)
size = 1;
while (true)
// спроба виділити size байтів;
if (виділити вдалося)
return (покажчик на пам'ять);
// виділити пам'ять не вдалося
// перевірити, чи встановлена ​​функція-обробник
new_handler globalHandler = set_new_handler (0);
set_new_handler (globalHandler);
if (globalHandler)
(* GlobalHandler) ();
else
throw std :: bad_alloc ();
>
>

Особливо чутливих може збентежити виділення одного байта пам'яті тоді, коли у нас запитують нуль. Так, це грубо, але зате працює. Справа в тому, що ми повинні повернути коректний покажчик навіть тоді, коли у нас просять 0 байтів, тому доводиться винаходити. І чим простіше буде винахід, тим воно надійніше.

Ще раз повторюся, що оператор new в разі проблем з виділенням пам'яті в нескінченному циклі викликає функцію-обробник. Дуже важливо, щоб код цієї функції коректно вирішував проблему: зробив доступною пам'яті більше, порушив виняток типу, похідного від bad_alloc, встановив інший обробник, прибрав поточний обробник або не повертав управління зовсім. В іншому випадку програма, яка викликала нашу версію new, зависне.

Окремо слід розглянути випадок, коли new є функціейчленом будь-якого класу. Зазвичай призначені для користувача версії операторів роботи з пам'яттю пишуться для більш оптимізованого розподілу пам'яті. Наприклад, new для класу Base заточений під виділення пам'яті об'ємом sizeof (Base) - ні більше, ні менше.

Але що буде, якщо ми створимо клас, який успадковується від Base? У дочірньому класі також буде використовуватися версія оператора new, визначеного в Base. Але розмір успадкованого класу (назвемо його Derived), швидше за все, буде відрізнятися від розміру базового: sizeof (Derived)! = Sizeof (Base). Через це вся користь від власної реалізації new може зійти нанівець. Про що, до речі, багато хто забуває і відчувають потім нелюдські страждання.

Проблема успадкування оператора new

class Base public:
static void * operator new (std :: size_t size)
throw (std :: bad_alloc);
.
>;
// в підкласі не оголошений оператор new
class Derived: public Base
;
// викликається Base :: operator new
Derived * p = new Derived;

Вирішити проблему досить просто, але робити це треба заздалегідь. Досить в базовому класі, в тілі оператора new, виконувати перевірку розміру виділеної пам'яті. Якщо кількість запитуваних байтів не збігається з кількістю байтів в об'єкті класу Base, то роботу по виділенню пам'яті найкраще передати стандартної реалізації new. До того ж, так ми відразу ж вирішуємо питання з обробкою запиту на виділення нуля байтів пам'яті - цим вже займатиметься штатна версія оператора.

Рішення проблеми спадкування оператора new

void * operator new (std :: size_t size)
throw (std :: bad_alloc)
// якщо size неправильний, викликати стандартний new
if (size! = sizeof (Base))
return. operator new (size);
// в іншому випадку обробити запит
.
>

На рівні класу можна також визначити new для масивів (operator new []). Цей оператор не повинен нічого робити, крім як виділяти блок неформатований пам'яті. Ми не можемо здійснювати будь-які операції з ще не створеними об'єктами. Та й, до того ж, нам невідомий розмір цих об'єктів, адже вони можуть бути спадкоємцями класу, в якому визначено new []. Тобто кількість об'єктів в масиві необов'язково одно (запитане число байтів) / sizeof (Base). Більш того, для динамічних масивів може виділятися більшу кількість пам'яті, ніж займуть самі об'єкти, для забезпечення резерву.

Псевдокод користувальницької реалізації оператора delete

void * operator delete (void * rawMemory) throw ()
// якщо нульовий покажчик, нічого не робити
if (rawMemory == 0) return;
// звільнити пам'ять, на яку вказує
rawMemory;
>

Якщо оператор delete є функцією-членом класу, то, як і в випадку з new, слід подбати про перевірку розміру видаляється пам'яті. Якщо для користувача реалізація new для класу Base виділила sizeof (Base) байтів пам'яті, то і самопісний delete повинен звільнити рівно стільки ж байтів. В іншому випадку, якщо розмір видаляється пам'яті не збігається з розміром класу, в якому визначено оператор, слід передати всю роботу стандартному delete.

Псевдокод функції-члена delete

class Base public:
static void * operator new (std :: size_t size)
throw (std :: bad_alloc);
static void * operator delete
(Void * rawMemory, std :: size_t size) throw ();
.
>;
void * Base :: operator delete (void * rawMemory,
std :: size_t size) throw ()
// якщо нульовий покажчик, нічого не робити
if (rawMemory == 0) return;
if (size! = sizeof (Base)). operator delete (rawMemory);
return;
>
// звільнити пам'ять, на яку вказує
rawMemory;
>

Оператори new і delete з розміщенням

Функція operator new, приймаюча додаткові параметри, називається «оператором new з розміщенням». Зазвичай в якості додаткового параметра виступає змінна типу void *. Таким чином, визначення розміщує new виглядає приблизно так:

void * operator new (std :: size_t, void * pMemory)

У більш широкому сенсі new з розміщенням може приймати будь-яку кількість додаткових параметрів будь-якого типу. Оператор delete називається «розміщених» за таким же принципом - він теж повинен крім основних приймати і додаткові параметри.

Тепер давай розглянемо випадок, коли ми динамічно створюємо об'єкт будь-якого класу. Код такої операції повинен бути всім добре знайомий:

widget * pw = new Widget

Створення об'єкта відбувається в два етапи. На першому виділяється необхідний обсяг пам'яті стандартним оператором new, а на другому викликається конструктор класу Widget, який ініціалізує об'єкт. Може виникнути ситуація, коли пам'ять на першому кроці буде виділена, а конструктор порушить виняток, і покажчик * pw залишиться неініціалізованих. Ахтунг! Таким чином ми отримаємо потенційну витік пам'яті. Щоб цього не сталося, за справу повинна взятися система часу виконання C ++. Вона зобов'язана викликати оператор delete для виділеної пам'яті на першому етапі створення об'єкта. Але є один маленький нюанс, який може все зіпсувати. C ++ викличе delete, сигнатура якого збігається з сигнатурою new, використовуваного для виділення пам'яті. Коли ми користуємося стандартними формами new і delete, проблем не виникає, але якщо ми напишемо власний new з розміщенням і забудемо накодо відповідну форму delete, то ми практично зі стовідсотковою ймовірністю отримаємо витік пам'яті при порушенні виключення в конструкторі класу.

Такий код може викликати витоку пам'яті

class Widget public:
.
static void * operator new (std :: size_t size,
std :: ostream logStream) throw (std :: bad_alloc);
static void * operator delete (void * pMemory,
std :: size_t size) throw ();
.
>;
Widget * pw = new (std :: cerr) Widget;

Вирішення цієї проблеми полягає в написанні оператора delete з сигнатурою, відповідної сигнатуре new з розміщенням. У разі необхідності скасувати виділення пам'яті саме цей operator delete буде викликаний системою часу виконання C ++. У коді це може виглядати так:

Тепер витоків не повинно бути

class Widget public:
.
static void * operator new (std :: size_t size,
std :: ostream logStream)
throw (std :: bad_alloc);
static void * operator delete (void * pMemory,
std :: size_t size)
throw ();
static void * operator delete (void * pMemory,
std :: ostream logStream)
throw ();
.
>;
Widget * pw = new (std :: cerr) Widget;

Не слід забувати, що сконструйований об'єкт може бути видалений стандартною формою delete. Щоб повністю уникнути всіх можливих проблем, пов'язаних з виділенням пам'яті, слід перевизначити і цей варіант функції звільнення пам'яті.

Ще один важливий момент пов'язаний з приховуванням імен функцій. Якщо ми визначимо якусь форму new, то всі інші стандартні форми цього оператора стануть недоступні.

приховування імен

class Base public:
static void * operator new (std :: size_t size,
std :: ostream logStream)
throw (std :: bad_alloc);
...
>;
// Помилка! Звичайна форма new прихована
Base * pb = new Base;
// Правильно, викликається розміщений new з Base
Base * pb = new (std :: cerr) Base;

висновок

На цьому ми закінчили розбиратися з особливостями менеджменту пам'яті в C ++ і отримали порцію корисних знань, які знадобляться будь-якій поважає себе кодеру. До нових зустрічей в ефірі!

Покажи цю статтю друзям:

  • 42 хвилини тому

VPN-провайдер PureVPN пояснив, як допоміг ФБР заарештувати одного зі своїх користувачів

Google розкритикувала систему оновлень Microsoft. У відповідь Microsoft розповіла про RCE-баг в Chrome