Когда нам нужно определить деструкторов?

Я читал, что деструкторы должны быть определены, когда у нас есть элементы указателя, и когда мы определяем базовый класс, но я не уверен, полностью ли я понимаю. Одна из вещей, о которых я не уверен, заключается в том, является ли определение конструктора по умолчанию бесполезным или нет, поскольку по умолчанию мы всегда получаем конструктор по умолчанию. Кроме того, я не уверен, нужно ли нам определять конструктор по умолчанию для реализации принципа RAII (нужно ли просто назначать распределение ресурсов в конструкторе и не определять какой-либо деструктор?).

class A
{

public:
    ~Account()
    {
        delete [] brandname;
        delete b;

        //do we need to define it?

    };

    something(){} =0; //virtual function (reason #1: base class)

private:
    char *brandname; //c-style string, which is a pointer member (reason #2: has a pointer member)
    B* b; //instance of class B, which is a pointer member (reason #2)
    vector<B*> vec; //what about this?



}

class B: public A
{
    public something()
    {
    cout << "nothing" << endl;
    }

    //in all other cases we don't need to define the destructor, nor declare it?
}

Ответы

Ответ 1

Правило тройки и правило нуля

Хороший способ обработки ресурсов заключался в Правиле трех (теперь это правило из пяти из-за перемещения семантики), но в последнее время приходит еще одно правило: Правило нуля.

Идея, но вы действительно должны прочитать статью, заключается в том, что управление ресурсами должно быть передано другим конкретным классам.

В этой связи стандартная библиотека предоставляет хороший набор инструментов, таких как: std::vector, std::string, std::unique_ptr и std::shared_ptr, эффективно устраняя необходимость в настраиваемых деструкторах, перемещать/копировать конструкторы, перемещать/копировать назначение и конструкторы по умолчанию.

Как применить его к вашему коду

В вашем коде у вас много разных ресурсов, и это дает отличный пример.

Строка

Если вы заметили, что brandname фактически является "динамической строкой", стандартная библиотека не только сохраняет вас из строки стиля C, но автоматически управляет памятью строки с помощью std::string.

Динамически выделенный B

Второй ресурс представляет собой динамически выделенный B. Если вы динамически выделяете по другим причинам, отличным от "Я хочу факультативного члена", вы обязательно должны использовать std::unique_ptr, который позаботится о ресурс (освобождение при необходимости) автоматически. С другой стороны, если вы хотите, чтобы он был необязательным членом, вы можете вместо этого использовать std::optional.

Коллекция Bs

Последний ресурс - это всего лишь массив B s. Это легко справиться с std::vector. Стандартная библиотека позволяет вам выбирать из множества различных контейнеров для различных потребностей; Просто упомянем некоторые из них: std::deque, std::list и std::array.

Заключение

Чтобы добавить все предложения, вы получите:

class A {
private:
    std::string brandname;
    std::unique_ptr<B> b;
    std::vector<B> vec;
public:
    virtual void something(){} = 0;
};

Это безопасно и доступно для чтения.

Ответ 2

Как подчеркивает @nonsensickle, вопросы слишком широки... так что я собираюсь попытаться справиться со всем, что знаю...

Первой причиной повторного определения деструктора будет Правило трех, которое частично является элементом 6 в Scott Meyers Эффективный С++, но не полностью. Правило из трех говорит, что если вы повторно определили деструктор, скопируйте конструктор или операции копирования, то это означает, что вы должны переписать все три из них. Причина в том, что если вам пришлось переписать свою версию для одного, то значения по умолчанию для компилятора больше не будут действительны для остальных.

Другим примером может служить тот, на который указал Скотт Майерс в Эффективном С++

При попытке удалить объект производного класса с помощью указателя базового класса, а базовый класс имеет не виртуальный деструктор, результаты undefined.

И затем он продолжает

Если класс не содержит никаких виртуальных функций, это часто свидетельствует о том, что он не предназначен для использования в качестве базового класса. Когда класс не предназначен для использования в качестве базового класса, создание виртуального деструктора обычно является плохим.

Его вывод о деструкторах для виртуального

Суть в том, что безвозмездное объявление всех виртуальных деструкторов столь же неверно, как и объявление их виртуальными. Фактически, многие люди суммируют ситуацию таким образом: объявляют виртуальный деструктор в классе тогда и только тогда, когда этот класс содержит хотя бы одну виртуальную функцию.

И если это не правило из трех случаев, возможно, у вас есть указатель внутри вашего объекта, и, возможно, вы выделили ему память внутри вашего объекта, тогда вам нужно управлять этой памятью в деструкторе, это пункт 6 в его книге

Обязательно проверьте @Jefffrey ответ на Правило нуля

Ответ 3

Есть только две вещи, которые требуют определения деструктора:

  • Когда ваш объект разрушается, вам нужно выполнить какое-то действие, кроме уничтожения всех членов класса.

    Подавляющее большинство этих действий когда-то освобождало память, с принципом RAII эти действия переместились в деструкторы контейнеров RAII, которые компилятор заботится о вызове. Но эти действия могут быть любыми, например, закрытием файла или записью некоторых данных в журнал или.... Если вы строго следуете принципу RAII, вы будете писать контейнеры RAII для всех этих других действий, так что только контейнеры RAII имеют деструкторы.

  • Когда вам нужно уничтожить объекты с помощью указателя базового класса.

    Если вам нужно это сделать, вы должны определить деструктор как virtual в базовом классе. В противном случае ваши производные деструкторы не будут вызваны, независимо от того, определены они или нет, и являются ли они virtual или нет. Вот пример:

    #include <iostream>
    
    class Foo {
        public:
            ~Foo() {
                std::cerr << "Foo::~Foo()\n";
            };
    };
    
    class Bar : public Foo {
        public:
            ~Bar() {
                std::cerr << "Bar::~Bar()\n";
            };
    };
    
    int main() {
        Foo* bar = new Bar();
        delete bar;
    }
    

    Эта программа печатает только Foo::~Foo(), деструктор Bar не вызывается. Нет предупреждений или сообщений об ошибках. Только частично разрушенные объекты со всеми вытекающими последствиями. Поэтому убедитесь, что вы сами определяете это условие, когда оно возникает (или укажите, что нужно добавить virtual ~Foo() = default; к каждому нераспределенному классу, который вы определяете.

Если ни одно из этих двух условий не выполняется, вам не нужно определять деструктор, для этого будет использоваться конструктор по умолчанию.


Теперь к вашему примеру код:
Когда ваш член является указателем на что-либо (либо как указатель, либо ссылка), компилятор не знает...

  • ... есть ли другие указатели на этот объект.

  • ... указывает ли указатель на один объект или на массив.

Следовательно, компилятор не может определить, как и как уничтожить все, на что указывает указатель. Поэтому деструктор по умолчанию никогда не уничтожает ничего за указателем.

Это относится как к brandname, так и к b. Следовательно, вам нужен деструктор, потому что вам нужно сделать освобождение самостоятельно. В качестве альтернативы вы можете использовать для них контейнеры RAII (std::string и вариант интеллектуального указателя).

Это рассуждение не относится к vec, потому что эта переменная напрямую включает в себя std::vector<> внутри объектов. Следовательно, компилятор знает, что vec должен быть разрушен, что, в свою очередь, уничтожит все его элементы (это контейнер RAII, в конце концов).

Ответ 4

Мы знаем, что если деструктор не предусмотрен, компилятор будет генерировать его.

Это означает, что для чего-либо, кроме простой очистки, например примитивных типов, потребуется деструктор.

Во многих случаях динамическое распределение или приобретение ресурсов во время строительства имеет этап очистки. Например, возможно, потребуется удалить динамически выделенную память.

Если класс представляет собой аппаратный элемент, элемент может быть отключен или помещен в безопасное состояние.

Контейнерам, возможно, потребуется удалить все их элементы.

В заключение, если класс приобретает ресурсы или требует специальной очистки (скажем, в определенном порядке), должен быть деструктор.

Ответ 5

Если вы динамически выделяете память и хотите, чтобы эта память была освобождена только тогда, когда сам объект "завершен", вам необходимо создать деструктор.

Объект может быть "завершен" двумя способами:

  • Если он был статически выделен, то он "закрывается" неявно (компилятором).
  • Если он был динамически распределен, он явно "оканчивается" (вызывая delete).

Когда "завершено" явно с использованием указателя типа базового класса, деструктор должен быть virtual.