Для чего нужен виртуальный деструктор
Деструкторы (C++)
). Например, деструктор для класса String объявляется следующим образом:
Если деструктор не определен, компилятор будет предоставлять его по умолчанию. для многих классов это достаточно. Необходимо определить пользовательский деструктор, если класс хранит дескрипторы для системных ресурсов, которые необходимо освободить, или указатели, владеющие памятью, на которую они указывают.
Рассмотрим следующее объявление класса String :
В предыдущем примере деструктор String::
String использует delete оператор для освобождения пространства, динамически выделяемого для хранения текста.
Объявление деструкторов
Деструкторы — это функции с тем же именем, что и класс, но с добавленным в начало знаком тильды (
При объявлении деструкторов действуют несколько правил. Деструкторы:
Не могут иметь аргументов.
Не возвращают значение (или void ).
Использование деструкторов
Деструкторы вызываются, когда происходит одно из следующих событий:
Локальный (автоматический) объект с областью видимости блока выходит за пределы области видимости.
Время существования временного объекта заканчивается.
Программа заканчивается, глобальные или статические объекты продолжают существовать.
Деструктор явно вызываться с использованием полного имени функции деструктора.
Деструкторы могут свободно вызывать функции-члена класса и осуществлять доступ к данным членов класса.
Существуют два ограничения на использование деструкторов.
Вы не можете получить его адрес.
Производные классы не наследуют деструктор своего базового класса.
Порядок уничтожения
Когда объект выходит за пределы области или удаляется, последовательность событий при его полном уничтожении выглядит следующим образом:
Вызывается деструктор класса, и выполняется тело функции деструктора.
Деструкторы для объектов нестатических членов вызываются в порядке, обратном порядку их появления в объявлении класса. Необязательный список инициализации элементов, используемый при создании этих элементов, не влияет на порядок создания или уничтожения.
Деструкторы для невиртуальных базовых классов вызываются в обратную последовательность объявления.
Деструкторы для виртуальных базовых классов вызываются в порядке, обратном порядку их объявления.
Виртуальные базовые классы
Деструкторы для виртуальных базовых классов вызываются в порядке, обратном их указанию в направленном ациклическом графе (в глубину, слева направо, обход в обратном порядке). На следующем рисунке представлен граф наследования.
Граф наследования, показывающий виртуальные базовые классы
Ниже перечислены заголовки классов, представленных на рисунке.
Просмотрите левую часть графа, начиная с самой глубокой точки графа (в данном случае E ).
Просматривайте граф справа налево, пока не будут пройдены все узлы. Запомните имя текущего узла.
Пересмотрите предыдущий узел (вниз и вправо), чтобы определить, является ли рассматриваемый узел виртуальным базовым классом.
Если рассматриваемый узел является виртуальным базовым классом, просмотрите список, чтобы проверить, был ли он введен ранее. Если он не является виртуальным базовым классом, игнорируйте его.
Если рассматриваемого узла еще нет в списке, добавьте его вниз списка.
Просмотрите граф вверх и вдоль следующего пути вправо.
Перейдите к шагу 2.
Если путь последний путь вверх исчерпан, запомните имя текущего узла.
Перейдите к шагу 3.
Выполняйте этот процесс, пока нижний узел снова не станет текущим узлом.
Таким образом, для класса E порядок удаления будет следующим.
В ходе этого процесса создается упорядоченный список уникальных записей. Имя класса никогда не отображается дважды. После создания список просматривается в обратном порядке, и вызывается деструктор для каждого класса в списке от последнего к первому.
Порядок построения или удаления очень важен, когда конструкторы и деструкторы в одном классе полагаются на другой компонент, который создается первым или сохраняется дольше, например если деструктор A (на рисунке выше) полагается на то, что B будет по-прежнему присутствовать после выполнения кода, или наоборот.
Такие взаимозависимости между классами в графе наследования опасны, поскольку классы, наследуемые впоследствии, могут изменить крайний левый путь, тем самым изменив порядок построения и удаления.
Не являющиеся виртуальными базовыми классами
Деструкторы для невиртуальных базовых классов вызываются в порядке, в котором объявляются имена базовых классов. Рассмотрим следующее объявление класса.
Явные вызовы деструктора
Редко возникает необходимость в явном вызове деструктора. Однако может быть полезно выполнить удаление объектов, размещенных по абсолютным адресам. Обычно эти объекты выделяются с помощью определяемого пользователем new оператора, принимающего аргумент размещения. delete Оператор не может освободить эту память, так как она не выделена из бесплатного хранилища (Дополнительные сведения см. delete ). Вызов деструктора, однако, может выполнить соответствующую очистку. Для явного вызова деструктора для объекта ( s ) класса String воспользуйтесь одним из следующих операторов.
Нотация для явных вызовов деструкторов, показанная в предыдущем примере, может использоваться независимо от того, определяет ли тип деструктор. Это позволяет выполнять такие явные вызовы, не зная, определен ли деструктор для типа. Явный вызов деструктора, если ни один из них не определен, не имеет никакого эффекта.
Отказоустойчивость
Классу требуется деструктор, если он получает ресурс, и для безопасного управления ресурсом, вероятно, потребуется реализовать конструктор копии и назначение копирования.
Если эти специальные функции не определены пользователем, они неявно определяются компилятором. Неявно созданные конструкторы и операторы присваивания выполняют поверхностную почленном копию, которая почти наверняка неверно, если объект управляет ресурсом.
Явное определение деструктора, конструктора копирования или оператора присваивания копирования предотвращает неявное определение конструктора перемещения и оператора присваивания перемещения. В этом случае не удастся предоставить операции перемещения, если копирование занимает много ресурсов, но пропущенная возможность оптимизации.
18.4 – Виртуальные деструкторы, виртуальное присваивание и игнорирование виртуализации
Однако на самом деле мы хотим, чтобы функция удаления вызывала деструктор Derived (который, в свою очередь, вызывает деструктор Base ), иначе m_array не будет удален. Мы можем сделать это, сделав деструктор Base виртуальным:
Теперь эта программа дает следующий результат:
Правило
Всякий раз, когда вы имеете дело с наследованием, вы должны сделать любые явные деструкторы виртуальными.
Как и в случае с обычными виртуальными функциями-членами, если функция базового класса является виртуальной, все производные переопределения будут считаться виртуальными независимо от того, указаны ли они как таковые. Нет необходимости создавать пустой деструктор производного класса только для того, чтобы пометить его как виртуальный.
Обратите внимание, что если вы хотите, чтобы в вашем базовом классе был виртуальный деструктор, который в противном случае был бы пустым, вы можете определить свой деструктор следующим образом:
Виртуальное присваивание
Оператор присваивания можно сделать виртуальным. Однако, в отличие от случая с деструктором, где виртуализация всегда является хорошей идеей, виртуализация оператора присваивания на самом деле открывает ящик Пандоры, и затрагивает некоторые сложные темы, выходящие за рамки данного руководства. Следовательно, мы собираемся порекомендовать вам пока оставить свои присваивания не виртуальными, для простоты.
Игнорирование виртуализации
Хотя и редко, но вы можете захотеть проигнорировать виртуализацию функции. Например, рассмотрим следующий код:
Вероятно, вы не будете использовать это очень часто, но хорошо знать, что это, по крайней мере, возможно.
Должны ли мы делать все деструкторы виртуальными?
Это частый вопрос, который задают начинающие программисты. Как отмечено в примере выше, если деструктор базового класса не помечен как виртуальный, то программа подвергается риску утечки памяти, если программист позже удалит указатель базового класса, указывающий на объект производного класса. Один из способов избежать этого – пометить все ваши деструкторы как виртуальные. Но должны ли вы это делать?
Легко сказать «да», и в дальнейшем вы сможете использовать любой класс в качестве базового, но это приведет к снижению производительности (к каждому экземпляру вашего класса добавляется виртуальный указатель). Таким образом, вы должны найти компромисс между этими затратами со своими намерениями.
Традиционная мудрость (как первоначально была высказана Хербом Саттером, уважаемым гуру C++) предлагала избегать ситуации утечки памяти, связанной с невиртуальным деструктором, следующим образом: «Деструктор базового класса должен быть либо открытым и виртуальным, либо защищенным и невиртуальным». Класс с защищенным деструктором нельзя удалить с помощью указателя, что предотвращает случайное удаление объекта производного класса с помощью указателя базового класса, когда базовый класс содержит невиртуальный деструктор. К сожалению, это также означает, что и базовый класс нельзя удалить с помощью указателя базового класса, что, по сути, означает, что объект этого класса не может быть динамически размещен или удален, кроме как производным классом. Это также исключает для таких классов использование умных указателей (таких как std::unique_ptr и std::shared_ptr ), что ограничивает полезность этого правила (мы рассмотрим умные указатели в следующей главе). Это также означает, что объект базового класса не может быть размещен в стеке. Это довольно жесткий набор ограничений.
Зачем нам нужен чистый виртуальный деструктор на C++?
Я понимаю необходимость виртуального деструктора. Но зачем нам нужен чистый виртуальный деструктор? В одной из статей на C++ автор упомянул, что мы используем чистый виртуальный деструктор, когда хотим сделать абстрактный класс.
но мы можем сделать абстрактный класс, сделав любую из функций-членов чисто виртуальной.
когда мы действительно делаем деструктор чисто виртуальным? Может ли кто-нибудь дать хорошее Реальное время пример?
когда мы создаем абстрактные классы, это хорошая практика, чтобы сделать деструктор чисто виртуальным? Если да..тогда почему?
12 ответов
вероятно, реальная причина того, что чистые виртуальные деструкторы разрешены, заключается в том, что запретить их означало бы добавить еще одно правило к языку, и нет необходимости в этом правиле, поскольку никакие негативные последствия не могут возникнуть из разрешения чистого виртуального деструктора.
нет, простой старый виртуальный достаточно.
Если вы создаете объект с реализации по умолчанию для виртуальных методов, и хотите сделать его абстрактным, не заставляя кто-нибудь, чтобы переопределить любой конкретные способ, вы можете сделать деструктор чисто виртуальным. Я не вижу в этом особого смысла, но это возможно.
обратите внимание, что, поскольку компилятор создаст неявный деструктор для производных классов, если автор класса этого не сделает, любые производные классы будут не быть абстрактным. Поэтому наличие чистого виртуального деструктора в базовом классе не будет иметь никакого значения для производных классов. Это только сделает базу абстрактный класс (спасибо за @kappaкомментарий).
можно также предположить, что каждый производящий класс, вероятно, должен иметь определенный код очистки и использовать чистый виртуальный деструктор в качестве напоминания для его записи, но это кажется надуманным (и не подкрепленным).
Примечание: деструктор-единственный метод, который, даже если он is чисто виртуальные и иметь реализацию для создания экземпляров производных классов (да, чистые виртуальные функции могут иметь реализации).
все, что вам нужно для абстрактного класса, это по крайней мере одна чистая виртуальная функция. Любая функция будет делать; но, как это происходит, деструктор-это то, что любой класса-так это всегда есть в качестве кандидата. Кроме того, создание деструктора чисто виртуальным (в отличие от просто виртуального) не имеет поведенческих побочных эффектов, кроме как сделать класс абстрактным. Таким образом, многие руководства по стилю рекомендуют использовать чистый виртуальный destuctor последовательно, чтобы указать, что класс является абстрактным-если только по какой-то причине он обеспечивает согласованное место, кто-то, читающий код, может посмотреть, является ли класс абстрактным.
Если вы хотите создать абстрактный базовый класс:
. проще всего сделать класс абстрактным, сделав деструктор чисто виртуальным и определения (тело метода) для него.
для нашей гипотетической азбуки:
вы гарантируете, что он не может быть создан (даже внутренний для самого класса, поэтому частный конструкторов может быть недостаточно), вы получаете виртуальное поведение, которое хотите для деструктора, и вам не нужно искать и помечать другой метод, который не нуждается в виртуальной отправке как «виртуальный».
из ответов, которые я прочитал на ваш вопрос, я не мог вывести вескую причину использовать чистый виртуальный деструктор. Например, меня совсем не убеждает следующая причина:
вероятно, реальная причина того, что чистые виртуальные деструкторы разрешены, заключается в том, что запретить их означало бы добавить еще одно правило к языку, и нет необходимости в этом правиле, поскольку никакие негативные последствия не могут возникнуть из разрешения чистого виртуального деструктора.
в моем мнение, чистые виртуальные деструкторы могут быть полезны. Например, предположим, что в коде есть два класса myClassA и myClassB и что myClassB наследуется от myClassA. По причинам, упомянутым Скоттом Мейерсом в его книге «более эффективный C++», пункт 33 «сделать не-листовые классы абстрактными», лучше фактически создать абстрактный класс myAbstractClass, от которого наследуют myClassA и myClassB. Это обеспечивает лучшую абстракцию и предотвращает некоторые проблемы, возникающие, например, с объектом копии.
в процессе абстракции (создания класса myAbstractClass) может быть, что ни один метод myClassA или myClassB не является хорошим кандидатом на то, чтобы быть чистым виртуальным методом (что является предпосылкой для того, чтобы myAbstractClass был абстрактным). В этом случае вы определяете деструктор абстрактного класса pure virtual.
Если вы хотите остановить создание экземпляра базового класса без каких-либо изменений в уже реализованном и протестированном производном классе, вы реализуете чистый виртуальный деструктор в своем базовом классе.
здесь я хочу сказать, когда нам нужно виртуальный деструктор и когда нам нужно чисто виртуальный деструктор
Если вы хотите, чтобы никто не мог создать объект базового класса напрямую, используйте чистый виртуальный деструктор virtual
когда вам не нужно над вещью, только вам нужен сейф уничтожение объекта производного класса
базы* сайте pbase = новый производный(); удалить сайте pbase; чистый виртуальный деструктор не требуется, только виртуальный деструктор будет выполнять эту работу.
основные отношения объектно-ориентированного дизайна два: Есть-А и есть-А. Я их не выдумывал. Вот как они называются.
IS-a указывает, что конкретный объект идентифицируется как класс, который находится над ним в иерархии классов. Объект банана-это фруктовый объект, если он является подклассом плода класс. Это означает, что везде, где можно использовать фруктовый класс, можно использовать банан. Однако это не рефлексивно. Вы не можете заменить базовый класс для определенного класса, если этот конкретный класс вызывается.
Has-a указывает, что объект является частью составного класса и что существует отношение собственности. Это означает, что в C++ это объект-член, и как таковой onus находится на классе-владельце, чтобы избавиться от него или передать право собственности перед разрушением себя.
эти два понятия легче реализовать в языках с одним наследованием, чем в модели множественного наследования, такой как C++, но правила по существу одинаковы. Осложнение возникает, когда идентификатор класса неоднозначен, например, передача указателя класса Banana в функцию, которая принимает указатель класса Fruit.
виртуальные функции, во-первых, вещь времени выполнения. Это часть полиморфизма в том, что он используется для решения, какую функцию запускать в то время, когда он вызывается запущенная программа.
ключевое слово virtual-это директива компилятора для привязки функций в определенном порядке, если существует неопределенность относительно идентификатора класса. Виртуальные функции всегда находятся в родительских классах (насколько мне известно) и указывают компилятору, что привязка функций-членов к их именам должна происходить сначала с функцией подкласса, а затем с функцией родительского класса.
фруктовый класс может иметь виртуальную функцию color (), которая возвращает » NONE» по умолчанию. Функция Banana class color() возвращает «желтый» или «коричневый».
но если функция, принимающая фруктовый указатель, вызывает color () в отправленном ей классе Banana-какая функция color () вызывается? Функция обычно вызывает Fruit:: color() для объекта Fruit.
это было бы 99% времени не то, что предполагалось. Но если Fruit:: color () был объявлен виртуальным, то Banana:color () будет вызван для объекта, потому что правильная функция color () будет будьте привязаны к указателю Fruit во время вызова. Среда выполнения проверяет, на какой объект указывает указатель, поскольку он был помечен как виртуальный в определении класса Fruit.
это отличается от переопределения функции в подклассе. В таком случае фруктовый указатель вызовет Fruit:: color() если все, что он знает, это то, что он-указатель на фрукты.
Итак, теперь возникает идея «чистой виртуальной функции». Это довольно неудачная фраза, поскольку чистота не имеет ничего общего с ним. Это означает, что предполагается, что метод базового класса никогда не называли. Действительно чисто виртуальная функция не может быть вызвана. Однако его все еще необходимо определить. Должна существовать сигнатура функции. Многие кодеры делают пустую реализацию <> для полноты, но компилятор будет генерировать ее внутри, если нет. В том случае, когда функция вызывается, даже если указатель на фрукты, Banana:: color() будет вызываться, поскольку это единственная реализация color () там есть.
теперь последняя часть головоломки: конструкторы и деструкторы.
чисто виртуальные конструкторы являются незаконными, полностью. Это просто.
но чисто виртуальный деструктор в случае, если вы хотите запретить создание экземпляра базового класса. Только подклассы могут быть созданы, если деструктор базового класса является чисто виртуальным. конвенция должна присвоить ему значение 0.
вам нужно создать реализацию в этот случай. Компилятор знает, что это то, что вы делаете, и гарантирует, что вы делаете это правильно, или он сильно жалуется, что не может связать все функции, которые ему нужно скомпилировать. Ошибки могут быть запутанными, если вы не на правильном пути относительно того, как вы моделируете иерархию классов.
таким образом, вам запрещено в этом случае создавать экземпляры фруктов, но разрешено создавать экземпляры банана.
вызов для удаления фруктового указателя, указывающего на экземпляр Банан сначала вызовет Banana::
Banana(), а затем вызовет Fuit::
Fruit (), всегда. Потому что, несмотря ни на что, когда вы вызываете деструктор подкласса, деструктор базового класса должен следовать.
это плохая модель? Это сложнее на этапе проектирования, да, но это может гарантировать, что правильная компоновка выполняется во время выполнения и что функция подкласса выполняется там, где есть неопределенность относительно того, к какому подклассу осуществляется доступ.
Если вы пишете C++ так, что вы только передайте точные указатели класса без общих или неоднозначных указателей, тогда виртуальные функции действительно не нужны. Но если вам требуется гибкость во время выполнения типов (как в Apple Banana Orange ==> Fruit), функции становятся проще и универсальнее с меньшим количеством избыточного кода. Вам больше не нужно писать функцию для каждого типа фруктов, и вы знаете, что каждый фрукт будет реагировать на color() со своей правильной функцией.
Я надеюсь, что это длинное объяснение укрепляет понятие, а не путает вещи. Есть много хороших примеров, чтобы посмотреть на, и посмотрите на достаточно и на самом деле запустить их и возиться с ними, и вы получите его.
вы попросили пример, и я считаю, что следующее дает причину для чистого виртуального деструктора. Я с нетерпением жду ответов на вопрос, является ли это хороший причина.
Я не хочу, чтобы кто-то мог кинуть error_base type, но типы исключений error_oh_shucks и error_oh_blast имеют идентичную функциональность, и я не хочу писать ее дважды. Сложность pImpl необходима, чтобы избежать разоблачения std::string моим клиентам и использование std::auto_ptr требуется конструктор копирования.
открытый заголовок содержит спецификации исключений, которые будут доступны клиенту для различения различных типов исключений, создаваемых моей библиотекой:
и вот общая реализация:
класс exception_string, закрытый, скрывает std:: string из моего открытого интерфейса:
мой код затем выдает ошибку как:
в Использование шаблона для error немного дармовой. Это экономит немного кода за счет требования клиентов ловить ошибки как:
может есть другой РЕАЛЬНЫЙ СЛУЧАЙ чистого виртуального деструктора, который я на самом деле не вижу в других ответах 🙂
во-первых, я полностью согласен с отмеченным ответом: это потому, что запрещение чистого виртуального деструктора потребует дополнительного правила в спецификации языка. Но это все еще не тот случай использования, который Марк призывает:)
сначала представьте себе это:
просто-у нас есть интерфейс Printable и какой-то» контейнер», содержащий что-либо с этим интерфейсом. Думаю, здесь вполне понятно почему print() метод чисто виртуальным. Он может иметь некоторое тело, но в случае отсутствия реализации по умолчанию, pure virtual является идеальной » реализацией «(=»должен быть предоставлен классом потомка»).
а теперь представьте себе то же самое, только не для печати, а для уничтожения:
и там же контейнер:
Это тема десятилетней давности 🙂 Прочитайте последние 5 абзацев пункта №7 в книге » Эффективный C++ «для деталей, начиная с» иногда может быть удобно дать классу чистый виртуальный деструктор. «
1) Если вы хотите, чтобы производные классы выполняли очистку. Это редкость.
2) нет,но вы хотите, чтобы он был виртуальным.
Урок №165. Виртуальные деструкторы и Виртуальное присваивание
Обновл. 15 Сен 2021 |
Хотя язык C++ автоматически предоставляет деструкторы для ваших классов, если вы не предоставляете их самостоятельно, все же иногда вы можете сделать это сами.
Виртуальные деструкторы
При работе с наследованием ваши деструкторы всегда должны быть виртуальными. Рассмотрим следующий пример:
Parent ( ) // примечание: Деструктор не виртуальный
Child ( ) // примечание: Деструктор не виртуальный
Поскольку parent является указателем класса Parent, то при его уничтожении компилятор будет смотреть, является ли деструктор класса Parent виртуальным. Поскольку это не так, то компилятор вызовет только деструктор класса Parent.
Результат выполнения программы:
Тем не менее, нам нужно, чтобы delete вызывал деструктор класса Child (который, в свою очередь, будет вызывать деструктор класса Parent), иначе m_array не будет удален. Это можно выполнить, сделав деструктор класса Parent виртуальным:
Parent ( ) // примечание: Деструктор виртуальный
Child ( ) // примечание: Деструктор виртуальный
Результат выполнения программы:
Правило: При работе с наследованием ваши деструкторы должны быть виртуальными.
Виртуальное присваивание
Оператор присваивания можно сделать виртуальным. Однако, в отличие от деструктора, виртуальное присваивание не всегда является хорошей идеей. Почему? Это уже выходит за рамки этого урока. Следовательно, для сохранения простоты в вашем коде, не рекомендуется использовать виртуальное присваивание.
Игнорирование виртуальных функций
В языке С++ мы можем игнорировать вызов переопределений. Например:
Здесь мы хотим, чтобы ссылка класса Parent на объект класса Child вызывала Parent::getName() вместо Child::getName(). Чтобы это сделать, нужно просто использовать оператор разрешения области видимости:
Вы, скорее всего, не будете использовать это очень часто, но знать об этом стоит.
Поделиться в социальных сетях:
Урок №164. Модификаторы override и final
Комментариев: 6
Не совсем понятно зачем делать деструктор класса Child виртуальным.
В данном примере у него нет наследников.
Если тут как с методами, то деструктор класса-наследника будет неявно виртуальным, так как является переопределением виртуального деструктора в классе-родителе, ключевое слово указывается, чтобы не забыть это.
Строго говоря, при удалении наследника при помощи delete с указателем на базовый класс будет неопределённое поведение. То, что вызывается деструктор базового класса — лишь один из вариантов проявления неопределённого поведения.
Есть альтернативное решение, касающееся деструкторов базовых классов — объявить их защищёнными невиртуальными, если базовый класс или интерфейс не предназначен для полиморфного удаления
Тогда на этапе компиляции будет ошибка:
1>error C2248: ‘Parent::
Parent’: cannot access protected member declared in class ‘Parent’
Т.е. это защита от неправильного использования указателя?