Чому базовий клас повинен мати тут віртуальний деструктор, якщо похідний клас не виділяє необробленої динамічної пам'яті?


12

Наступний код викликає витік пам'яті:

#include <iostream>
#include <memory>
#include <vector>

using namespace std;

class base
{
    void virtual initialize_vector() = 0;
};

class derived : public base
{
private:
    vector<int> vec;

public:
    derived()
    {
        initialize_vector();
    }

    void initialize_vector()
    {
        for (int i = 0; i < 1000000; i++)
        {
            vec.push_back(i);
        }
    }
};

int main()
{
    for (int i = 0; i < 100000; i++)
    {
        unique_ptr<base> pt = make_unique<derived>();
    }
}

Для мене це мало особливого сенсу, оскільки похідний клас не виділяє жодної необробленої динамічної пам’яті, а jedinstveний_ptr мається на увазі. Я розумію, що неявний деструктор бази класів викликається замість похідних, але я не розумію, чому це проблема тут. Якби я писав явний деструктор для похідних, я б нічого не писав для vec.


4
Ви припускаєте, що деструктор існує лише тоді, коли він написаний вручну; це припущення є помилковим: мова забезпечує ~derived()делегування деструктора vec. Як варіант, ви припускаєте, що unique_ptr<base> ptзнав би похідний деструктор. Без віртуального методу це не може бути. Хоча унікальному_ptr може бути надана функція видалення, яка є параметром шаблону без будь-якого представлення часу виконання, і ця функція не використовує цього коду.
амон

Чи можемо ми поставити дужки на одному рядку, щоб скоротити код? Тепер мені потрібно прокрутити.
laike9m

Відповіді:


14

Коли компілятор виконує неявну delete _ptr;внутрішню unique_ptrдеструктор 's (де _ptrвказівник зберігається в unique_ptr), він точно знає дві речі:

  1. Адреса об'єкта, який потрібно видалити.
  2. Тип вказівника, який _ptrє. Оскільки вказівник знаходиться unique_ptr<base>, цей засіб _ptrмає тип base*.

Це все, що знає компілятор. Отже, враховуючи, що він видаляє об’єкт типу base, він буде викликати ~base().

Отже ... де частина, де він руйнує derviedпредмет, на який він насправді вказує? Тому що якщо компілятор не знає, що це знищує a derived, він не знає, що derived::vec існує взагалі , не кажучи вже про те, що він повинен бути знищений. Таким чином, ви зламали предмет, залишивши половину його знищеною.

Компілятор не може припустити, що будь- base*яке знищене насправді є derived*; зрештою, походження може бути з будь-якої кількості класів base. Звідки можна знати, на який саме тип base*вказується цей конкретний ?

Що повинен зробити компілятор - це з’ясувати правильний деструктор для виклику (так, він derivedмає деструктор. Якщо ви = deleteне деструктор, кожен клас має деструктор, пишете ви його чи ні). Для цього доведеться використовувати деяку інформацію, що зберігається в ньому, baseдля отримання потрібної адреси коду деструктора для виклику інформації, яку встановлює конструктор фактичного класу. Тоді він повинен використовувати цю інформацію для перетворення base*покажчика на адресу відповідного derivedкласу (який може бути, а може і не бути за іншою адресою. Так, справді). І тоді він може викликати цього руйнівника.

Цей механізм я щойно описав? Його зазвичай називають "віртуальна відправка": ака, це те, що відбувається кожного разу, коли ви викликаєте функцію, позначену, virtualколи у вас є вказівник / посилання на базовий клас.

Якщо ви хочете викликати похідну функцію класу, коли все, що у вас є, є вказівником / посиланням базового класу, цю функцію потрібно оголосити virtual. Деструктори принципово не відрізняються в цьому плані.


0

Спадщина

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

У C ++ успадкування також приносить із собою деталі реалізації, маркування (або не маркування) деструктора як віртуального є однією з таких деталей реалізації.

Функція Прив’язка

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

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

Якщо у нас є базовий клас з функцією (може бути будь-яка функція, включаючи конструктор або деструктор) і ваш код називає функцію на ньому, що це означає?

Виходячи зі свого прикладу, якщо ви зателефонували, initialize_vector()компілятор повинен вирішити, чи дійсно ви хочете викликати реалізацію, знайдену в Base, або реалізацію, знайдену в Derived. Є два способи вирішити це:

  1. Перший - це вирішити, що, оскільки ви телефонували від Baseтипу, ви мали на увазі реалізацію в Base.
  2. Другий - вирішити, що тому, що тип виконання, який зберігається у Baseнабраному значенні, може бути Base, або Derivedщо рішення про те, який дзвінок робити, повинно прийматися під час виконання під час виклику (кожен раз, коли він викликається).

У цьому місці компілятор плутається, обидва варіанти однаково дійсні. Це коли virtualпотрапляє в суміш. За наявності цього ключового слова компілятор вибирає варіант 2, затримуючи рішення між усіма можливими реалізаціями, поки код не запуститься з реальним значенням. Якщо це ключове слово відсутнє, компілятор вибирає варіант 1, оскільки це інакше нормальна поведінка.

У випадку виклику віртуальної функції компілятор може все ж вибрати варіант 1. Але лише якщо це може довести, що це завжди так.

Конструктори та деструктори

То чому б ми не вказали віртуального конструктора?

Як інтуїтивніше, як компілятор вибиратиме між однаковими реалізаціями конструктора для Derivedта Derived2? Це досить просто, не може. Немає попереднього значення, з якого компілятор може дізнатися, що було дійсно призначено. Немає попереднього значення, оскільки це завдання конструктора.

То чому нам потрібно вказати віртуального деструктора?

Більш інтуїтивно, як компілятор вибиратиме між реалізаціями для Baseта Derived? Вони є лише функціональними дзвінками, тому поведінка виклику функції відбувається. Без оголошеного віртуального деструктора компілятор вирішить прив’язати безпосередньо до Baseдеструктора незалежно від типу значень часу виконання.

У багатьох компіляторах, якщо похідне не оголошує жодних членів даних і не успадковує від інших типів, поведінка у ~Base()заповіті буде придатною, але це не гарантується. Це спрацювало б лише випадково, подібно до того, щоб стояти перед вогнемет, який ще не був запалений. Ви деякий час добре.

Єдиний правильний спосіб оголошення будь-якого базового або інтерфейсного типу в C ++ - це оголошення віртуальної деструктора, щоб правильний деструктор викликався для будь-якого даного екземпляра ієрархії типу цього типу. Це дозволяє функції, що володіє найбільш відомими примірниками, правильно очистити цей екземпляр.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.