Язык программирования C Пятое издание
Скачать 1.85 Mb.
|
Page 754/1103Поскольку эти функции-члены получают ссылки, преобразование производного класса в базовый позволяет передавать функциям копирования и перемещения базового класса объект производного класса. Эти функции не являются виртуальными. При передаче объекта производного класса конструктору базового выполняется конструктор, определенный в базовом классе. Этому конструктору известно только о членах самого базового класса. Точно так же, если объект производного класса присваивается объекту базового, выполняется оператор присвоения, определенный в базовом классе. Этот оператор также знает только о членах самого базового класса. Например, классы приложения книжного магазина используют синтезируемые версии операторов копирования и присвоения (см. раздел 13.1.1 и раздел 13.1.2). Более подробная информация об управлении копированием и наследовании приведена в разделе 15.7.2, а пока достаточно знать, что синтезируемые версии осуществляют почленное копирование или присвоение переменных-членов класса тем же способом, что и у любого другого класса: Bulk_quote bulk; // объект производного типа Quote item(bulk); // используется конструктор // Quote::Quote(const Quote&) item = bulk; // вызов Quote::operator=(const Quote&) При создании объекта item выполняется конструктор копий класса Quote. Этот конструктор знает только о переменных-членах bookNo и price. Он копирует эти члены из части Quote объекта bulk и игнорирует члены, являющиеся частью Bulk_quote объекта bulk. Аналогично при присвоении объекта bulk объекту item ему присваивается только часть Quote объекта bulk. Поскольку часть Bulk_quote игнорируется, говорят, что она была отсечена (sliced down). При инициализации объекта базового типа (или присвоении) объектом производного типа копируется, перемещается или присваивается только часть базового класса производного объекта. Производная часть объекта игнорируется. Ключевая концепция. Преобразования между типами, связанными наследованием Есть три правила преобразования связанных наследованием классов, о которых следует помнить. • Преобразование из производного класса в базовый применимо только к указателю или ссылке. • Нет неявного преобразования из типа базового класса в тип производного. Page 755/1103 При переопределении виртуальной функции производный класс может, но не обязан, повторить ключевое слово virtual. Как только функция объявляется виртуальной, она остается виртуальной во всех производных классах. У функции производного класса, переопределяющей унаследованную виртуальную функцию, должны быть точно такие же типы параметров, как и у функции базового класса, которую она переопределяет. За одним исключением тип возвращаемого значения виртуальной функции в производном классе также должен соответствовать типу возвращаемого значения функции в базовом классе. Исключение относится к виртуальным функциям, возвращающим ссылку (или указатель) на тип, который сам связан наследованием. Таким образом, если тип D происходит от типа В, то виртуальная функция базового класса может возвратить указатель на тип B*, а ее версия в производном классе может возвратить указатель на тип D*. Но такие типы возвращаемого значения требуют, чтобы преобразование производного класса в базовый из типа D в тип В было доступно. Доступность базового класса рассматривается в разделе 15.5. Пример такого вида виртуальной функции рассматривается в разделе 15.8.1. Функция, являющаяся виртуальной в базовом классе, неявно остается виртуальной в его производных классах. Когда производный класс переопределяет виртуальную функцию, ее параметры в базовом и производных классах должны точно совпадать. Спецификаторы final и override Как будет продемонстрировано в разделе 15.6, производный класс вполне может определить функцию с тем же именем, что и виртуальная функция в его базовом классе, но с другим списком параметров. Компилятор полагает, что такая функция независима от функции базового класса. В таких случаях версия в производном классе не переопределяет версию в базовом. На практике такие объявления зачастую являются ошибкой — автор класса намеревался переопределить виртуальную функцию базового класса, но сделал ошибку в определении списка параметров. Поиск таких ошибок может быть на удивление трудным. По новому стандарту можно задать переопределение виртуальной функции в производном классе. Это дает ясно понять наше намерение и (что еще более важно) позволяет компилятору самому находить такие проблемы. Компилятор отвергнет программу, если функция, отмеченная как override, не переопределит существующую виртуальную функцию: struct В { virtual void f1(int) const; virtual void f2(); void f3(); }; struct D1 : B { void f1(int) const override; // ok: f1() соответствует f1() базового void f2(int) override; // Page 758/1103 }; В структуре D1 спецификатор override для функции f1() вполне подходит; и базовые, и производные версии функции-члена f1() константы, они получают тип int и возвращают void. Версия f1() в структуре D1 правильно переопределяет виртуальную функцию, которую она унаследовала от структуры B. Объявление функции f2() в структуре D1 не соответствует объявлению функции f2() в структуре B — она не получает никаких аргументов, а определенная в структуре D1 получает аргумент типа int. Поскольку объявления не совпадают, функция f2() в структуре D1 не переопределяет функцию f2() структуры В; это новая функция со случайно совпавшим именем. Как уже упоминалось, это объявление должно было быть переопределено, но этого не произошло и компилятор сообщил об ошибке. Поскольку переопределена может быть только виртуальная функция, компилятор отвергнет также функцию f3() в структуре D1. Эта функция не виртуальна в структуре В, поэтому нечего и переопределять. Точно так же ошибочна и функция f4(), поскольку в структуре В даже нет такой функции. Функцию можно также определить как final. Любая попытка переопределения функции, которая была определена со спецификатором final, будет помечена как ошибка: struct D2 : В { // наследует f2() и f3() из В и переопределяет f1(int) void f1(int) const final; // последующие классы не могут // переопределять f1(int) }; struct D3 : D2 { void f2(); // ok: переопределение f2() унаследованной от косвенно // базовой структуры В Page 759/1103 }; Спецификаторы final и override располагаются после списка параметров (включая квалификаторы ссылки или const) и после замыкающего типа (см. раздел 6.3.3). Виртуальные функции и аргументы по умолчанию Подобно любой другой функции, виртуальная функция может иметь аргументы по умолчанию (см. раздел 6.5.1). Если вызов использует аргумент по умолчанию, то используемое значение определяется статическим типом, для которого вызвана функция. Таким образом, при вызове через ссылку или указатель на базовый класс аргумент (аргументы) по умолчанию будет определен в базовом классе. Аргументы базового класса будут использоваться даже тогда, когда выполняется версия функции производного класса. В данном случае функции производного класса будут переданы аргументы по умолчанию, определенные для версии функции базового класса. Если функция производного класса будет полагаться на передачу других аргументов, то программа не будет выполняться, как ожидалось. Виртуальные функции с аргументами по умолчанию должны использовать те же значения аргументов в базовом и производных классах. Хитрость виртуального механизма В некоторых случаях необходимо предотвратить динамическое связывание вызова виртуальной функции; нужно вынудить вызов использовать конкретную версию этой виртуальной функции. Для этого используется оператор области видимости. Рассмотрим, например, этот код: // вызов версии базового класса независимо от динамического типа baseP double undiscounted = baseP->Quote::net_price(42); Здесь происходит вызов версии функции net_price() класса Quote независимо от типа объекта, на который фактически указывает baseP. Этот вызов будет распознан во время компиляции. Обычно только код функций-членов (или друзей) должен использовать оператор области видимости для обхода виртуального механизма. Зачем обходить виртуальный механизм? Наиболее распространен случай, когда виртуальная функция производного класса вызывает версию базового класса. В таких случаях версия базового класса могла бы выполнять действия, общие для всей иерархии типов. Версии, определенные в производных классах, осуществляли бы любые дополнительные действия, специфичные для их собственного типа. Если виртуальная функция производного класса, намереваясь вызвать свою версию из базового класса, пропустит оператор области видимости, то вызов будет распознан во время выполнения как вызов самой версии производного класса, что приведет к бесконечной рекурсии. Упражнения раздела 15.3 Упражнение 15.11. Добавьте в иерархию класса Quote виртуальную функцию debug(), отображающую переменные-члены соответствующих классов. Page 760/1103 }; Подобно прежнему классу Bulk_item, класс Disc_quote определяет стандартный конструктор и конструктор, получающий четыре параметра. Хотя объекты этого типа нельзя создавать непосредственно, конструкторы в классах, производных от класса Disc_quote, будут использовать конструкторы Disc_quote() для построения части Disc_quote своих объектов. Конструктор с четырьмя параметрами передает первые два конструктору Quote(), а двумя последними непосредственно инициализирует собственные переменные-члены discount и quantity. Стандартный конструктор инициализирует эти члены значениями по умолчанию. Следует заметить, что определение для чистой виртуальной функции предоставить нельзя. Однако тело функции следует определить вне класса. Поэтому нельзя предоставить в классе тело функции, для которой использована часть = 0. Классы с чистыми виртуальными функциями являются абстрактными Класс, содержащий (или унаследовавший без переопределения) чистую виртуальную функцию, является абстрактным классом (abstract base class). Абстрактный класс определяет интерфейс для переопределения последующими классами. Нельзя (непосредственно) создавать объекты абстрактного класса. Поскольку класс Disc_quote определяет функцию net_price() как чистую виртуальную, нельзя определить объекты типа Disc_quote. Можно определить объекты классов, производных от Disc_quote, если они переопределят функцию net_price(): // Disc_quote объявляет чистые виртуальные функции, которые // переопределит Bulk_quote Disc_quote discounted; // ошибка: нельзя определить объект Disc_quote Bulk_quote bulk; // ok: у Bulk_quote нет чистых виртуальных функций Классы, унаследованные от класса Disc_quote, должны определить функцию net_price(), иначе они также будут абстрактными. Нельзя создать объекты абстрактного класса. Конструктор производного класса инициализирует только свой прямой базовый класс Теперь можно повторно реализовать класс Bulk_quote так, чтобы он происходил от класса Disc_quote, а не непосредственно от класса Quote: // скидка прекращается при продаже определенного количества экземпляров // Page 763/1103 Bulk_quote() = default; Bulk_quote(const std::string& book, double price, std::size_t qty, double disc): Disc_quote(book, price, qty, disc) { } // переопределение базовой версии для реализации политики скидок double net_price(std::size_t) const override; }; У этой версии класса Bulk_quote есть прямой базовый класс (direct base class), Disc_quote, и косвенный базовый класс (indirect base class), Quote. У каждого объекта класса Bulk_quote есть три внутренних объекта: часть Bulk_quote (пустая), часть Disc_quote и часть Quote. Как уже упоминалось, каждый класс контролирует инициализацию объектов своего типа. Поэтому, даже при том, что у класса Bulk_quote нет собственных переменных-членов, он предоставляет тот же конструктор на четыре аргумента, что и первоначальный класс. Новый конструктор передает свои аргументы конструктору класса Disc_quote. Этот конструктор, в свою очередь, запускает конструктор Quote(). Конструктор Quote() инициализирует переменные-члены bookNo и price объекта bulk. Когда конструктор Quote() завершает работу, начинает работу конструктор Disc_quote(), инициализирующий переменные-члены quantity и discount. Теперь возобновляет работу конструктор Bulk_quote(). Он не делает ничего и ничего не инициализирует. Ключевая концепция. Рефакторинг Добавление класса Disc_quote в иерархию Quote является примером рефакторинга (refactoring). Рефакторинг подразумевает переделку иерархии классов с передачей некоторых функций и/или данных из одного класса в другой. Рефакторинг весьма распространен в объектно-ориентированных приложениях. Примечательно, что, несмотря на изменение иерархии наследования, код, который использует классы Bulk_quote и Quote, изменять не придется. Но после рефакторинга классов (или любых других измененный) следует перекомпилировать весь код, который использует эти классы. Упражнения раздела 15.4 Упражнение 15.15. Определите собственные версии классов Disc_quote и Bulk_quote. Упражнение 15.16. Перепишите класс из упражнения 15.2.2 раздела 12.1.6, представляющий ограниченную стратегию скидок, так, чтобы он происходил от класса Disc_quote. Упражнение 15.17. Попытайтесь определить объект типа Disc_quote и посмотрите, какие сообщения об ошибке выдал компилятор. Page 764/1103 // ошибка: clobber не может обращаться к защищенным членам Base void clobber(Base &b) { b.prot_mem = 0; } Если производные классы (и друзья) смогут обращаться к защищенным членам в объекте базового класса, то вторая версия функции clobber (получающая тип Base&) будет корректна. Хоть эта функция и не дружественна классу Base, она все же сможет изменить объект типа Base; для обхода защиты спецификатором protected любого класса достаточно определить новый класс по линии Sneaky. Для предотвращения такого способа применения члены и друзья производного класса могут обращаться к защищенным членам только тех объектов базового класса, которые встроены в объект производного; к обычным объектам базового типа у них никакого доступа нет.Открытое, закрытое и защищенное наследование Доступ к члену наследуемого класса контролируется комбинацией спецификатора доступа этого члена в базовом классе и спецификатором доступа в списке наследования производного класса. Для примера рассмотрим следующую иерархию: class Base { public: void pub_mem(); // открытый член protected: int prot_mem; // защищенный член private: char priv_mem; // закрытый член }; struct Pub_Derv : public Base { // ok: производный класс имеет доступ к защищенным членам int f() { return prot_mem; } // ошибка: закрытые члены недоступны производным классам Page 766/1103 }; struct Priv_Derv : private Base { // закрытое наследование не затрагивает доступ в производном классе int f1() const { return prot_mem; } }; Спецификатор доступа наследования никак не влияет на возможность членов (и друзей) производного класса обратиться к членам его собственного прямого базового класса. Доступ к членам базового класса контролируется спецификаторами доступа в самом базовом классе. Структуры Pub_Derv и Priv_Derv могут обращаться к защищенному члену prot_mem, но ни одна из них не может обратиться к закрытому члену priv_mem. Задача спецификатора доступа наследования — контролировать доступ пользователей производного класса, включая другие классы, производные от него, к членам, унаследованным от класса Base: Pub_Derv d1; // члены, унаследованные от Base, являются открытыми Priv_Derv d2; // члены, унаследованные от Base, являются закрытыми d1.pub_mem(); // ok: pub_mem является открытой в производном класс d2.pub_mem(); // ошибка: pub_mem является закрытой в производном классе Структуры Pub_Derv и Priv_Derv унаследовали функцию pub_mem(). При открытом наследовании члены сохраняют свой спецификатор доступа. Таким образом, объект d1 может вызвать функцию pub_mem(). В структуре Priv_Derv члены класса Base являются закрытыми; пользователи этого класса не смогут вызвать функцию pub_mem(). Спецификатор доступа наследования, используемый производным классом, также контролирует доступ из классов, унаследованных от этого производного класса: struct Derived_from_Public : public Pub_Derv { // ok: Base::prot_mem остается защищенной в Pub_Derv int use_base() { return prot_mem; } }; struct Derived_from_Private : public Priv_Derv { Page 767/1103 доступные для производных классов и доступные только для базового класса и его друзей. Член класса, относящийся к реализации, должен быть защищен, если он предоставляет функцию или данные, которые производный класс должен будет использовать в собственной реализации. В противном случае члены реализации должны быть закрытыми. Дружественные отношения и наследование Подобно тому, как дружественные отношения не передаются (см. раздел 7.3.4), они также не наследуются. У друзей базового класса нет никаких специальных прав доступа к членам его производных классов, а у друзей производного класса нет специальных прав доступа к базовому классу: class Base { // добавлено объявление; другие члены, как прежде friend class Pal; // у Pal нет доступа к классам, производным от Base }; class Pal { public: int f(Base b) { return b.prot_mem; } // ok: Pal дружествен Base int f2(Sneaky s) { return s.j; } // ошибка: Pal не // дружествен Sneaky // доступ к базовому классу контролируется базовым классом, даже в // объекте производного int f3(Sneaky s) { return s.prot_mem; } // ok: Pal дружествен }; Факт допустимости функции f3() может показаться удивительным, но он непосредственно следует из правила, что все классы контролируют доступ к собственным членам. Класс Pal — друг класса Base, поэтому класс Pal может обращаться к членам объектов класса Base. Это относится и к встроенным в объект класса Base объектам классов, производных от него. Page 769/1103 Объявление using в классе может использовать имя любого доступного (не закрытого) члена прямого или косвенного базового класса. Доступность имени, указанного в объявлении using, зависит от спецификатора доступа, предшествующего объявлению using. Таким образом, если объявление using расположено в разделе private класса, то имя будет доступно только для членов и друзей. Если объявление находится в разделе public, имя доступно для всех пользователей класса. Если объявление находится в разделе protected, имя доступно только для членов, друзей и производных классов. Производный класс может предоставить объявление using только для тех имен, доступ к которым разрешен. Уровни защиты наследования по умолчанию В разделе 7.2 упоминалось о том, что у классов, определенных с использованием ключевых слов struct, и class разные спецификаторы доступа по умолчанию. Точно так же заданный по умолчанию спецификатор наследования зависит от ключевого слова, используемого при определении производного класса. По умолчанию у производного класса, определенного с ключевым словом class, будет закрытое наследование (private inheritance), а с ключевым словом struct — открытое (public inheritance): class Base { /* ... */ }; struct D1 : Base { /* ... */ }; // открытое наследование по умолчанию class D2 : Base { /* ... */ }; // закрытое наследование по умолчанию Весьма распространенно заблуждение, что между классами и структурами есть иные, более глубокие различия. Единственное различие — заданные по умолчанию спецификаторы доступа для членов и наследования. Никаких других различий нет. Для закрытого наследования производный класс должен быть явно определен как private, не следует полагаться на поведение по умолчанию. Это ясно дает понять, что закрытое наследование применено преднамеренно, а не по оплошности. Упражнения раздела 15.5 Упражнение 15.18. С учетом классов Base и производных от него, и типов объектов, приведенных в комментариях, укажите, какие из следующих присвоений допустимы. Объясните, почему некорректны недопустимые. Base *p = &d1; // d1 имеет тип Pub_Derv p = &d2; // d2 имеет тип Priv_Derv p = &d3; // d3 имеет тип Prot_Derv p = &dd1; // dd1 имеет тип Derived_from_Public Page 771/1103 Упражнение 15.19. Предположим, у каждого из классов: Base и производных от него, есть функция-член в формате void memfcn(Base &b) { b = *this; } Укажите, была ли эта функция допустима для каждого класса. Упражнение 15.20. Напишите код проверки ответов на предыдущие два упражнения. Упражнение 15.21. Выберите одну из следующих общих абстракций, содержащих семейство типов (или любую собственную). Организуйте типы в иерархию наследования. (a) Форматы графических файлов (например: gif, tiff, jpeg, bmp) (b) Геометрические примитивы (например: box, circle, sphere, cone) (c) Типы языка С++ (например: class, function, member function) Упражнение 15.22. Укажите имена некоторых из наиболее вероятных виртуальных функций, а также открытых и защищенных членов для класса, выбранного в предыдущем упражнении. 15.6. Область видимости класса при наследовании Каждый класс определяет собственную область видимости (scope) (см. раздел 7.4), в рамках которой определены его члены. При наследовании область видимости производного класса (см. раздел 2.2.4) вкладывается в области видимости его базовых классов. Если имя не найдено в области видимости производного класса, поиск его определения продолжается в областях видимости базовых классов. Тот факт, что область видимости производного класса вложена в область видимости его базовых классов, может быть удивителен. В конце концов, базовые и производные классы определяются в разных частях текста программы. Но именно это иерархическое вложение областей видимости класса позволяет членам производного класса использовать члены его базового класса, как будто они являются частью производного класса. Рассмотрим пример: Bulk_quote bulk; cout << bulk.isbn(); В этом коде поиск определения имени isbn() осуществляется следующим образом. • Поскольку вызывается функция isbn() объекта типа Bulk_quote, поиск начинается в классе Bulk_quote. В этом классе имя isbn() не найдено. Page 772/1103 2.2.4): struct Base { Base() : mem(0) { } protected: int mem; }; struct Derived : Base { Derived(int i): mem(i) { } // Derived::mem инициализируется i // Base::mem инициализируется по умолчанию int get_mem() { return mem; } // возвращает Derived::mem protected: int mem; // скрывает mem в Base }; Ссылка на переменную mem в функции get_mem() распознается как имя в классе Derived. Таким образом, код Derived d(42); cout << d.get_mem() << endl; // выводит 42 выведет значение 42. Член производного класса, имя которого совпадает с именем члена базового класса, скрывает член базового класса и предотвращает прямой доступ к нему. Применение оператора области видимости для доступа к скрытым членам Для доступа к скрытому члену базового класса можно использовать оператор области видимости. struct Derived : Base { int get_base_mem() { return Base::mem; } }; Page 774/1103 // не существующая в Base }; class D2 : public D1 { public: int fcn(int); // невиртуальная функция скрывает D1::fcn(int) int fcn(); // переопределяет виртуальную функцию fcn() из Base void f2(); // переопределяет виртуальную функцию f2() из D1 }; Функция fcn() в классе D1 не переопределяет виртуальную функцию fcn() из класса Base, поскольку у них разные списки параметров. Вместо этого она скрывает функцию fcn() из базового класса. Фактически у класса D1 есть две функции по имени fcn(): класс D1 унаследовал виртуальную функцию fcn() от класса Base, а также определяет собственную невиртуальную функцию-член по имени fcn(), получающую параметр типа int.Вызов скрытой виртуальной функции через базовый класс С учетом классов, описанных выше, рассмотрим несколько разных способов вызова этих функций: Base bobj; D1 d1obj; D2 d2obj; Base *bp1 = &bobj, *bp2 = &d1obj, *bp3 = &d2obj; bp1->fcn(); // виртуальный вызов Base::fcn() во время выполнения bp2->fcn(); // виртуальный вызов Base::fcn() во время выполнения bp3->fcn(); // виртуальный вызов D2::fcn() во время выполнения D1 *d1p = &d1obj; D2 *d2p = &d2obj; bp2->f2(); // Page 777/1103 Все три первых вызова сделаны через указатели на базовый класс. Поскольку функция fcn() является виртуальной, компилятор создает код, способный во время выполнения решить, какую версию вызвать. Это решение будет принято на основании фактического типа объекта, с которым связан указатель. В случае указателя bp2 основной объект имеет тип D1. Этот класс не переопределит функцию fcn() без параметров. Таким образом, вызов через указатель bp2 распознается (во время выполнения) как версия, определенная в классе Base. Следующие три вызова осуществляются через указатели с отличными типами. Каждый указатель указывает на один из типов в этой иерархии. Первый вызов некорректен, так как в классе Base нет функции f2(). Тот факт, что указатель случайно указывает на производный объект, является несущественным. И наконец, рассмотрим вызовы невиртуальной функции fcn(int): Base *p1 = &d2obj; D1 *p2 = &d2obj; D2 *p3 = &d2obj; p1->fcn(42); // ошибка: Base не имеет версии fcn(), получающей int p2->fcn(42); // статическое связывание, вызов D1::fcn(int) p3->fcn(42); // статическое связывание, вызов D2::fcn(int) В каждом вызове указатель случайно указывает на объект типа D2. Но динамический тип не имеет значения, когда происходит вызов невиртуальной функции. Вызываемая версия зависит только от статического типа указателя. Переопределение перегруженных функций Подобно любой другой функции, функция-член (виртуальная или нет) может быть перегружена. Производный класс способен переопределить любое количество экземпляров перегруженных функций, которые он унаследовал. Если производный класс желает сделать все перегруженные версии доступными через свой тип, то он должен переопределить их все или ни одну из них. Иногда класс должен переопределить некоторые, но не все функции в наборе перегруженных. В таких случаях было бы весьма утомительно переопределять каждую версию базового класса, чтобы переопределить только те, которые должен специализировать класс. Вместо переопределения каждой версии базового класса, которую он унаследовал, производный класс может предоставить объявление using (см. раздел 15.5) для перегруженного члена. Объявление using определяет только имя; оно не может определить Page 778/1103 Обычные правила объявления using в классе относятся и к именам перегруженных функций (см. раздел 15.5); каждый перегруженный экземпляр функции в базовом классе должен быть доступен в производном классе. Доступ к перегруженным версиям, которые в противном случае не переопределяются производным классом, будет возможен в точке объявления using. Упражнения раздела 15.6 Упражнение 15.23. Предположим, что класс D1 намеревается переопределить свою унаследованную функцию fcn(). Как исправить этот класс? Предположим, что класс исправлен так, что функция fcn() соответствует определению в классе Base. Как бы распознавались вызовы в этом разделе? 15.7. Конструкторы и функции управления копированием Подобно любому другому классу, класс в иерархии наследования контролирует происходящее при создании, копировании, перемещении, присвоении или удалении объектов его типа. Как и у любого другого класса, если класс (базовый или производный) сам не определяет одну из функций управления копированием, ее синтезирует компилятор. Кроме того, как обычно, синтезируемая версия любой из этих функций-членов может быть удаленной функцией. 15.7.1. Виртуальные деструкторы Основное воздействие, которое наследование оказывает на управление копированием для базового класса, заключается в том, что базовый класс обычно должен определять виртуальный деструктор (см. раздел 15.2.1). Деструктор должен быть виртуальной функцией, чтобы обеспечить объектам в иерархии наследования возможность динамического создания. Помните, что деструктор выполняется при удалении указателя на динамически созданный объект (см. раздел 13.1.3). Если это указатель на тип в иерархии наследования, вполне возможно, что статический тип указателя может отличаться от динамического типа удаляемого объекта (см. раздел 15.2.2). Например, при удалении указателя типа Quote* может оказаться, что он указывал на объект класса Bulk_quote. Если он указывает на объект типа Bulk_quote, компилятор должен знать, что следует выполнить деструктор именно класса Bulk_quote. Подобно любой другой функции, чтобы был выполнен надлежащий деструктор, в базовом классе его следует определить как виртуальную функцию: class Quote { public: // виртуальный деструктор необходим при удалении указателя на Page 779/1103 |