Адаптации растений и животных к жизни в горах: Большое значение для жизни организмов в горах имеют степень расчленения, крутизна и экспозиционные различия склонов...
Археология об основании Рима: Новые раскопки проясняют и такой острый дискуссионный вопрос, как дата самого возникновения Рима...
Топ:
История развития методов оптимизации: теорема Куна-Таккера, метод Лагранжа, роль выпуклости в оптимизации...
Отражение на счетах бухгалтерского учета процесса приобретения: Процесс заготовления представляет систему экономических событий, включающих приобретение организацией у поставщиков сырья...
Характеристика АТП и сварочно-жестяницкого участка: Транспорт в настоящее время является одной из важнейших отраслей народного...
Интересное:
Берегоукрепление оползневых склонов: На прибрежных склонах основной причиной развития оползневых процессов является подмыв водами рек естественных склонов...
Средства для ингаляционного наркоза: Наркоз наступает в результате вдыхания (ингаляции) средств, которое осуществляют или с помощью маски...
Отражение на счетах бухгалтерского учета процесса приобретения: Процесс заготовления представляет систему экономических событий, включающих приобретение организацией у поставщиков сырья...
Дисциплины:
2020-05-08 | 1384 |
5.00
из
|
Заказать работу |
|
|
Конструктор класса Туре с двумя аргументами выглядит следующим образом:
Туре (string di. string gr): dimensions (di). grade (gr)
Этот конструктор копирует строковые аргументы в поля класса dimensions и grade.
Конструктор класса Distance тот же, что и в предыдущей программе:
Distance (int ft. float in): feet (ft). inches (in) { }
В конструктор класса Lumber включены оба этих конструктора, которые получают значения для аргументов. Кроме того, класс Lumber имеет и свои аргументы: количество материала и его цена. Таким образом, конструктор имеет шесть аргументов. Он вызывает два конструктора, которые имеют по два аргумента, а затем инициализирует два собственных поля. Конструктор класса Lumber будет выглядеть следующим образом:
Lumber (string di. string gr. // параметры для Type
int ft. float in. // параметры для Distance
int qu. float pre): // наши собственные параметры
Type (di.gr). // вызов конструктора lype
Distance (ft. in). // вызов конструктора Distance
quantity (qu). price (pre) // вызов наших конструкторов
t)
Неопределенность при множественном наследовании
В определенных ситуациях могут появиться некоторые проблемы, связанные со множественным наследованием. Здесь мы рассмотрим наиболее общую. Допустим, что в обоих базовых классах существуют методы с одинаковыми именами, а в производном классе метода с таким именем нет. Как в этом случае объект производного класса определит, какой из методов базовых классов выбрать? Одного имени метода недостаточно, поскольку компилятор не сможет вычислить, какой из двух методов имеется в виду. Эту ситуацию мы разберем в примере AMBIGU:
// ambigu.cpp
//демонстрация неопределенности при множественном наследовании
#include <iostream>
using namespace std;
///////////////////////////////////////////////////////////
|
class A
{
public:
void show () { cout << "Класс A\n"; }
};
class В {
public:
void show () { cout << "Класс B\n"; }
};
class С: public A, public В {
};
///////////////////////////////////////////////////////////
int main ()
{
С objC; // объект класса С
// objC.show (); // так делать нельзя - программа не скомпилируется
objC.A::show (); // так можно
objC.A::show (); // так можно
return 0;
}
Проблема решается путем использования оператора разрешения, определяющего класс, в котором находится метод. Таким образом,
ObjC.A::show ():
направляет нас к версии метода showQ, принадлежащей классу А, а ObjC.В: -.show ():
направляет нас к методу, принадлежащему классу В. Б. Страуструп (см. приложение 3 «Библиография») называет это устранением неоднозначности.
Другой вид неопределенности появляется, если мы создаем производный класс от двух базовых классов, которые, в свою очередь, являются производными одного класса. Это создает дерево наследования в форме ромба. В программе DIAMOND показано, как это выглядит.
// diamond.срр
// демонстрация наследования в форме ромба
#include <iostream>
using namespace std;
///////////////////////////////////////////////////////////
class A
{
public:
void func ();
};
class B: public A { };
class C: public A { };
class D: public B, public C {};
///////////////////////////////////////////////////////////
int main ()
{
D ObjD;
//ObjD.func (); // неоднозначность: программа не скомпилируется
return 0;
}
Классы В и С являются производными класса А, а класс D является производным классов В и С. Трудности начинаются, когда объект класса D пытается воспользоваться методом класса А. В этом примере объект objD использует метод func(). Однако классы В и С содержат в себе копии метода func(), унаследованные от класса А. Компилятор не может решить, какой из методов использовать, и сообщает об ошибке.
Существует множество вариаций этой проблемы, поэтому многие эксперты советуют избегать множественного наследования. Не следует использовать его в своих важных программах, пока у вас нет достаточного опыта.
Порядок выполнения работы
1. Изучите теоретический материал.
2. Напишите программу, согласно заданному в варианте задания условию.
|
3. Ответьте на контрольные вопросы.
ВАРИАНТЫ ЗАДАНИЙ
Вариант. 1
Представьте себе издательскую компанию, которая торгует книгами и аудио-записями этих книг. Создайте класс publication, в котором хранятся название (строка) и цена (типа float) книги. От этого класса наследуются еще два класса: book, который содержит информацию о количестве страниц в книге (типа int), и type, который содержит время записи книги в минутах (тип float). В каждом из этих трех классов должен быть метод getdata(), через который можно получать данные от пользователя с клавиатуры, и putdata(), предназначенный для вывода этих данных.
Напишите функцию main() программы для проверки классов book и type. Создайте их объекты в программе и запросите пользователя ввести и вывести данные с использованием методов getdataQ и putdata(). *2. Вспомните пример STRC0NV из главы 8. Класс String в этом примере имеет дефект: у него нет защиты на тот случай, если его объекты будут инициализированы слишком длинной строкой (константа SZ имеет значение 80). Например, определение
String s = "Эта строка имеет очень большую длину и мы можем быть уверены, что она не уместится в отведенный буфер, что приведет к непредсказуемым последствиям.";
будет причиной переполнения массива str строкой s с непредсказуемыми последствиями вплоть до краха системы.
Создадим класс Pstring, производный от класса String, в котором предотвратим возможность переполнения буфера при определении слишком длинной строковой константы. Новый конструктор производного класса будет копировать в str только SZ-1 символов, если строка окажется слишком длинной, и будет копировать строку полностью, если она будет иметь длину меньшую, чем SZ. Напишите функцию main() программы для проверки ее работы со строками разной длины.
Вариант. 2
Начните с классов book, type и publication из варианта 1. Добавьте базовый класс sales, в котором содержится массив, состоящий из трех значений тина float, куда можно записать общую стоимость проданных книг за последние три месяца. Включите в класс методы getdata() для получения значений стоимости от пользователя и putdata() для вывода этих цифр. Измените классы book и type так, чтобы они стали производными обоих классов: publications и sales. Объекты классов book и type должны вводить и выводить данные о продажах вместе с другими своими данными. Напишите функцию main() для создания объектов классов book и type, чтобы протестировать возможности ввода/вывода данных.
|
Вариант. 3
Предположим, что издатель из вариантов 1 и 2 решил добавить к своей продукции версии книг на компьютерных дисках для тех, кто любит читать книги на своих компьютерах. Добавьте класс disk, который, как book и type, является производным класса publication. Класс disk должен включать в себя те же функции, что и в других классах. Полем только этого класса будет тип диска: CD или DVD. Для хранения этих данных вы можете ввести тип enum. Пользователь должен выбрать подходящий тип, набрав на клавиатуре с или d.
Вариант. 4
Создайте производный класс employee2 от базового класса employee из программы EMPLOY этой главы. Добавьте в новый класс поле compensation типа double и поле period типа enum для обозначения периода оплаты работы служащего: почасовая, понедельная или помесячная. Для простоты вы можете изменить классы laborer, manager и scientist так, чтобы они стали производными нового класса employee2. Однако заметим, что во многих случаях создание отдельного класса compensation и трех его производных классов manager2, scientist2 и laborer2 более соответствовало бы духу ООП. Затем можно применить множественное наследование и сделать так, чтобы эти три новых класса стали производными класса compensation и первоначальных классов manager, scientist и laborer. Таким путем можно избежать модификации исходных классов.
Вариант. 5
Вспомним программу ARR0VER3. Сохраним класс safearray таким же и, используя наследование, добавим к нему возможность для пользователя определять верхнюю и нижнюю границы массива в конструкторе.
Вариант. 6
Вспомним программу C0UNTEN2. В ней можно увеличивать и уменьшать счетчик, используя префиксные операции. Используя наследование, добавьте возможность использования постфиксных операций для случаев увеличения и уменьшения.
Вариант. 7
В некоторых компьютерных языках, таких, как Visual Basic, есть операции, с помощью которых можно выделить часть строки и присвоить ее другой строке. (В стандартном классе string предложены различные подходы.) Используя наследование, добавьте такую возможность в класс Pstring из упражнения 2. В новом производном классе Pstring2 разместите три новых функции: left(), mid() и rightQ.
|
s2.left (si. n) // в строку s2 помещаются n самых левых
// символов строки si s2.mid (si. s. n) // в строку s2 помещаются n символов из строки
// начиная с символа номер s s2.right (si. n) //в строку s2 помещаются n самых правых // символов строки si
Вы можете использовать цикл for для копирования символ за символом подходящих частей строки si во временный объект класса Pstring2, который затем их возвратит. Для получения лучшего результата используйте в этих функциях возврат по ссылке, чтобы они могли быть использованы с левой стороны знака «равно» для изменения части существующей строки.
Вариант. 8
Вспомним классы publication, book и type из варианта 1. Предположим, что мы хотим добавить в классы book и type дату выхода книги. Создайте новый производный класс publication, который является производным класса publication и включает в себя поле, хранящее эту дату. Затем измените классы book и type так, чтобы они стали производными класса publication вместо publication. Сделайте необходимые изменения функций классов так, чтобы пользователь мог вводить и выводить дату выхода книги.
Вариант. 9
В программе EMPMULT этой главы существует только один тип должности менеджера. В любой серьезной компании кроме менеджеров существуют еще и управляющие. Создадим производный от класса manager класс executive. (Мы предполагаем, что управляющий — это главный менеджер.) Добавочными данными этого класса будут размер годовой премии и количество его акций в компании. Добавьте подходящие методы для этих данных, позволяющие их вводить и выводить.
Вариант. 10
В различных ситуациях иногда требуется работать с двумя числами, объединенными в блок. Например, каждая из координат экрана имеет горизонтальную составляющую (х) и вертикальную (у). Представьте такой блок чисел в качестве структуры pair, которая содержит две переменные типа int. Теперь предположим, что мы хотим иметь возможность хранить переменные типа pair в стеке. То есть мы хотим иметь возможность положить переменную типа pair в стек путем вызова метода push() с переменной типа pair в качестве аргумента и вынуть ее из стека путем вызова метода рор(), возвращающего переменную типа pair. Начнем с класса Stack2 программы STAKEN из этой главы. Создадим производный от него класс pairStack. В нем будут содержаться два метода: перегружаемый метод push() и перегружаемый метод рор(). Метод pairStack::push() должен будет сделать два вызова метода Stack2::push(), чтобы сохранить оба числа из пары, а метод pairStack::pop() должен будет сделать два вызова метода Stack2::pop().
Вариант. 11
Рассмотрим старую Британскую платежную систему фунты-стерлинги- пенсы. Пенни в дальнейшем делятся на фартинги и полупенни. Фартинг — это 1/4 пенни. Существовали монеты фартинг, полфартинга и пенни. Любые сочетания монет выражались через восьмые части пенни: 1/8 пенни — это полфартинга; 1/4 пенни — это фартинг; 3/8 пенни — это фартинг с половиной; 1/2 пенни — это полпенни; 5/8 пенни — это полфартинга плюс полпенни; 3/4 пенни — это полпенни плюс фартинг; 7/8 пенни — это полпенни плюс фартинг с половиной. Давайте предположим, что мы хотим добавить в класс sterling возможность пользоваться этими дробными частями пенни. Формат ввода/вывода может быть похожим на £1.1.1-1/4 или £9.19.11-7/8, где дефисы отделяют дробные части от пенни.
|
Создайте новый класс sterfrac, производный от класса sterling. В нем должна быть возможность выполнения четырех основных арифметических операций со стерлиш-ами, включая восьмые части пенни. Поле eighths типа int определяет количество восьмых. Вам нужно будет перегрузить методы класса sterling, чтобы они могли работать с восьмыми частями. Пользователь должен иметь возможность ввести и вывести дробные части пенни. Не обязательно использовать класс fraction полностью, но вы можете это сделать для большей точности.
Форма отчёта: отчет выполняется в текстовом редакторе в электронном виде.
Содержание отчета:
4. Тема работы.
5. Цель работы.
6. Выполненные задания.
Система оценки: пятибальная.
Контрольные вопросы
1. Назначение наследования состоит в том, чтобы:
а) создавать более общие классы в более специализированных;
б) передавать аргументы объектам классов;
в) добавлять возможности к существующим классам без их модификации;
г) улучшать сокрытие данных и их инкапсуляцию.
2. Класс-наследник называется_________от базового класса.
3. Преимущество использования наследования заключается в:
а) обеспечении развития класса путем естественного отбора;
б) облегчении создания классов;
в) избежании переписывания кода;
г) предоставлении полезной концептуальной основы.
4. Напишите первую строку описания класса Bosworth, который является public-производным класса Alphonso.
5. Будет ли правильным утверждение: создание производного класса требует коренных изменений в базовом классе?
6. Члены базового класса для доступа к ним методов производного класса должны быть объявлены как public или _____________.
7. Пусть базовый класс содержит метод basefunc(), а производный класс не имеет метода с таким именем. Может ли объект производного класса иметь доступ к методу basefunc()?
8. Допустим, что класс, описанный в вопросе 4, и класс Alphonso содержат метод alfunc(). Напишите выражение, позволяющее объекту BosworthObj класса Bosworth получить доступ к методу alfuncQ.
9. Истинно ли следующее утверждение: если конструктор производного класса не определен, то объекты этого класса будут использовать конструкторы базового класса?
10. Допустим, что базовый и производный классы включают в себя методы с одинаковыми именами. Какой из методов будет вызван объектом производного класса, если не использована операция разрешения имени?
11. Напишите объявление конструктора без аргументов для производного класса Bosworth из вопроса 4, который будет вызывать конструктор без аргументов класса Alphonso.
12. Оператор разрешения обычно:
а) ограничивает видимость переменных для определенных методов;
б) обозначает, от какого базового класса создан производный;
в) определяет отдельный класс;
г) разрешает неопределенности.
13. Истинно ли следующее утверждение: иногда полезно создать класс, объектов которого никогда не будет создано?
14. Предположим, что существует класс Derv, производный от базового класса Base. Напишите объявление конструктора производного класса, принимающего один аргумент и передающего его в конструктор базового класса.
15. Предположим, что класс Derv является частным производным класса Base. Мы определяем объект класса Derv, расположенный в функции main(). Через него мы можем получить доступ к:
а) членам класса Derv, объявленным как public;
б) членам класса Derv, объявленным как protected;
в) членам класса Derv, объявленным как private;
г) членам класса Base, объявленным как public;
д) членам класса Base, объявленным как protected;
е) членам класса Base, объявленным как private.
16. Истинно ли следующее утверждение: класс D может быть производным класса С, который в свою очередь является производным класса В, который производный класса А?
17. Иерархия классов:
а) показывает те же взаимоотношения, что и схема организации;
б) описывает взаимоотношение типа «имеет»;
в) описывает взаимоотношения типа «является частью»;
г) показывает те же взаимоотношения, что и наследственное дерево.
18. Напишите первую строку описания класса Tire, который является производным классов Wheel и Rubber.
19. Предположим, что класс Derv является производным класса Base. Оба класса содержат метод func() без аргументов. Напишите выражение, входящее в метод класса Derv, которое вызывает метод func() базового класса.
Список использованной литературы
1. Х.Дейтел Как программировать на C; Пер. с англ. под ред. В. Тимофеева. - М.: БИНОМ, 2000. - 1005 с.: ил.; 24
2. Язык программирования С / Брайан Керниган, Деннис Ритчи; [пер. с англ. и ред. В. Л. Бродового]. - 2-е изд., перераб. и доп. - Москва [и др.]: Вильямс, 2007. - 289 с.; 23 см.
3. Бьерн Страуструп Язык программирования C++; Пер. с англ. С. Анисимова и М. Кононова под ред. Ф. Андреева, А. Ушакова. - 3. изд. - М.: Binom Pablishers; СПб.: Нев. диалект, 2004. - 990 с.: ил.; 24 см.
ПРАКТИЧЕСКОЕ ЗАНЯТИЕ 12
Тема: «Разработка программ на Си++ с использованием свойств полиморфизма и виртуальных функций»
Цели:
- определить понятия полиморфизма и виртуальных функций;
- рассмотреть основные случаи применения виртуальных функций в С++;
- рассмотреть основные случаи применения чистых виртуальных функций в С++;
Характер занятия: репродуктивный.
Форма организации: индивидуальная.
Обеспечение занятия: тетрадь с лекциями, СИ, компьютер.
Требования к знаниям студентов
Перед выполнением практической работы студент должен
знать:
- понятия класс и объект, абстрактный класс;
- основные особенности абстрактных классов;
- понятия конструктор и деструктор.
после выполнения практической работы студент должен
уметь:
- создавать программы, используя частичные виртуальные функции и виртуальные функции в языке С++;
- определять необходимость создания абстрактных классов.
Теоретический материал
Виртуальный означает видимый, но не существующий в реальности. Когда используются виртуальные функции, программа, которая, казалось бы, вызывает функцию одного класса, может в этот момент вызывать функцию совсем другого класса. А для чего вообще нужны виртуальные функции? Представьте, что имеется набор объектов разных классов, но вам хочется, чтобы они все были в одном массиве и вызывались с помощью одного и того же выражения. Например, в графической программе MULTSHAP есть разные геометрические фигуры: треугольник, шар, квадрат и т. д. В каждом из этих классов есть функция draw(), которая прорисовывает на экране фигуры.
Теперь, допустим, вам захотелось создать картинку, сгруппировав некоторые из этих элементов. Как бы сделать это без лишних сложностей? Подход к решению этой задачи таков: создайте массив указателей на все неповторяющиеся элементы картинки:
shape * ptarr [100]: //массив из 100 указателей на фигуры
Если в этом массиве содержатся указатели на все необходимые геометрические фигуры, то вся картинка может быть нарисована в обычном цикле:
for(int j =0: j <N; j++) ptarr[j]->draw():
Абсолютно разные функции выполняются с помощью одного и того же вызова. Если указатель в массиве ptarr указывает на шарик, вызовется функция, рисующая шарик, если он указывает на треугольник, то рисуется треугольник. Вот это и есть полиморфизм, то есть различные формы. Функции выглядят одинаково — это выражение draw(), но реально вызываются разные функции, в зависимости от значения ptarr [ j ]. Полиморфизм — одна из ключевых особенностей объектно-ориентированного программирования (ООП) наряду с классами и наследованием.
Чтобы использовать полиморфизм, необходимо выполнять некоторые условия. Во-первых, все классы (все эти треугольнички, шарики и т. д.) должны являться наследниками одного и того же базового класса. В MULTSHAP этот класс называется shape. Во-вторых, функция draw() должна быть объявлена виртуальной (virtual) в базовом классе.
Все это выглядит абстрактно, поэтому давайте напишем несколько небольших программ, которые выявят некоторые практические вопросы, чтобы потом можно было собрать их воедино.
Доступ к обычным методам через указатели
Наш первый пример покажет, что бывает, когда базовый и производные классы содержат функции с одним и тем же именем, и к ним обращаются с помощью указателей, но без использования виртуальных функций.
// notvirt.cpp
// Доступ к обычным функциям через указатели
#include <iostream>
using namespace std;
///////////////////////////////////////////////
class Base // Базовый класс
{
public:
void show() // Обычная функция
{ cout << "Base\n"; } };
//////////////////////////////////////////////
class Dervl: public Base // Производный класс 1
{
public:
void show()
{ cout << "Dervl\n"; } };
//////////////////////////////////////////////
class Derv2: public Base // Производный класс 2
{
public: void show()
{ cout << "Derv2\n"; } };
//////////////////////////////////////////////
int main() {
Dervl dv1; //Объект производного класса 1
Derv2 dv2; //Объект производного класса 2
Base* ptr; //Указатель на базовый класс
ptr = &dv1; //Адрес dvl занести в указатель
ptr->show(); //Выполнить showO
ptr = &dv2; //Адрес dv2 занести в указатель
ptr->show(); // Выполнить showO
return 0;
}
Итак, классы Dervl и Derv2 являются наследниками класса Base. В каждом из этих трех классов имеется метод show(). В main() мы создаем объекты классов Dervl и Derv2, а также указатель на класс Base. Затем адрес объекта порожденного класса мы заносим в указатель базового класса:
ptr = &dvl: //Адрес объекта порожденного класса заносим в //указатель базового
Компилятор не выдаст ошибку потому, что указатели на объекты порожденных классов совместимы по типу с указателями на объекты базового класса.
Теперь хорошо бы понять, какая же, собственно, функция выполняется в этой строчке:
ptr->show();
Это функция Base::show() или Dervl::show()? Опять же, в последних двух строчках программы NOTVIRT мы присвоили указателю адрес объекта, принадлежащего классу Derv2, и снова выполнили
ptr -> show ():
Так какая же из функций show() реально вызывается? Результат выполнения программы дает простой ответ:
.Base Base
Как видите, всегда выполняется метод базового класса. Компилятор не смотрит на содержимое указателя ptr, а выбирает тот метод, который удовлетворяет типу указателя. Да, иногда именно это нам и нужно, но таким образом не решить поставленную в начале этой темы проблему доступа к объектам разных классов с помощью одного выражения.
Доступ к виртуальным методам через указатели
Давайте сделаем одно маленькое изменение в нашей программе: поставим ключевое слово virtual перед объявлением функции show() в базовом классе. Вот листинг того, что получилось — программы VIRT:
// virt.cpp
// Доступ к виртуальным функциям через указатели
#include <iostream>
using namespace std;
///////////////////////////////////////////////
class Base // Базовый класс
{
public:
virtual void show() // Виртуальная функция
{ cout << "Base\n"; }
};
//////////////////////////////////////////////
class Derv1: public Base // Производный класс 1
{
public: void show()
{ cout << "Dervl\n"; }
};
//////////////////////////////////////////////
class Derv2: public Base // Производный класс 2
{
public: void show()
{ cout << "Derv2\n"; }
};
//////////////////////////////////////////////
int main () {
Derv1 dv1; //Объект производного класса 1
Derv2 dv2; //Объект производного класса 2
Base* ptr; //Указатель на базовый класс
ptr = &dv1; //Адрес dvl занести в указатель
ptr->show(); //Выполнить showO
ptr = &dv2; //Адрес dv2 занести в указатель
ptr->show(); // Выполнить showO
return 0;
}
На выходе имеем:
Dervl Derv2
Теперь, как видите, выполняются методы производных классов, а не базового. Мы изменили содержимое ptr с адреса из класса Dervl на адрес из класса Derv2, и изменилось реальное выполнение showQ. Значит, один и тот же вызов
ptr->show()
ставит на выполнение разные функции в зависимости от содержимого ptr. Компилятор выбирает функцию, удовлетворяющую тому, что занесено в указатель, а не типу указателя, как было в программе N0TVIRT
В программе NOTVIRT у компилятора нет проблем с выражением
ptr->show{):
Он всегда компилирует вызов функции show() из базового класса. Однако в программе VIRT компилятор не знает, к какому классу относится содержимое ptr. Ведь это может быть адрес объекта как класса Dervl, так и класса Derv2. Какую именно версию draw() вызывает компилятор — тоже загадка. Па самом деле компилятор не очень понимает, что ему делать, поэтому откладывает принятие решения до фактического запуска программы. А когда программа уже поставлена на выполнение, когда известно, на что указывает ptr, тогда будет запущена соответствующая версия draw. Такой подход называется поздним связыванием или динамическим связыванием. (Выбор функций в обычном порядке, во время компиляции, называется ранним связыванием или статическим связыванием.) Позднее связывание требует больше ресурсов, но дает выигрыш в возможностях и гибкости.
Вскоре мы претворим эти идеи в жизнь, а сейчас вернемся к виртуальным функциям.
Абстрактные классы и чистые виртуальные функции
Базовый класс, объекты которого никогда не будут реализованы, называется абстрактным классом. Такой класс может существовать с единственной целью — быть родительским по отношению к производным классам, объекты которых будут реализованы. Еще он может служить звеном для создания иерархической структуры классов.
Как нам объяснить людям, использующим созданную нами структуру классов, что объекты родительского класса не предназначены для реализации? Мож-ао, конечно, заявить об этом в документации, но это никак не защитит наш базо-зый класс от использования не но назначению. Надо защитить его программно. Для этого достаточно ввести в класс хотя бы одну чистую виртуальную функцию. Чистая виртуальная функция — это функция, после объявления которой добавлено выражение =0. Продемонстрируем сказанное в примере VIRTPURE:
// virtpure. cpp
// Чистая вир!уальная функция
#include <iostream>
using namespace std;
/////////////////////////////////////////////////
class Base //базовый класс
{
public:
virtual void show () = 0;
//чистая виртуальная
//функция
};
//////////////////////////////////////////////////
class Derv1: public Base //порожденный класс 1
{
public:
void show() { cout << "Derv1\n"; }
};
//////////////////////////////////////////////////
class Derv2: public Base //порожденный класс 2
{
public: void show() {cout << "Derv2\n"; }
};
/////////////////////////////////////////////////
int main () {
// Base bad; //невозможно создать объект
//из абстрактного класса
Base * arr [2]; //массив указателей на
//базовый класс
Derv 1 dv 1; //Объект производного класса 1
Derv 2 dv 2; //Объект производного класса 2
arr [0] = & dv 1; //Занести адрес dvl в массив
arr [1] = & dv 2;//Занести адрес dv 2 в массив
arr [0]-> show (); //Выполнить функцию showO
arr [1]-> show (); //над обоими объектами
return 0; }
Здесь виртуальная функция show() объявляется так:
virtual void show() = 0; //чистая виртуальная функция
Знак равенства не имеет ничего общего с операцией присваивания. Нулевое значение ничему не присваивается. Конструкция =0 — это просто способ сообщить компилятору, что функция будет чистой виртуальной. Если теперь в main() попытаться создать объект класса Base, то компилятор будет недоволен тем, что объект абстрактного класса пытаются реализовать. Он выдаст даже имя чистой виртуальной функции, которая делает класс абстрактным. Помните, что хотя это только объявление, но определение функции show() базового класса не является обязательным. Впрочем, если вам надо его написать, это можно сделать.
Как только в базовом классе окажется чистая виртуальная функция, необходимо будет позаботиться о том, чтобы избежать ее употребления во всех производных классах, которые вы собираетесь реализовать. Если класс использует чистую виртуальную функцию, он сам становится абстрактным, никакие объекты из него реализовать не удастся (производные от него классы, впрочем, уже не имеют этого ограничения). Более из эстетических соображений, нежели из каких-либо иных, можно все виртуальные функции базового класса сделать чистыми.
Между прочим, мы внесли еще одно, не связанное с предыдущими, изменение в VIRTPURE: теперь адреса методов хранятся в массиве указателей и доступны как элементы этого массива. Обработка этого, впрочем, ничем не отличается от использования единственного указателя. VIRTPURE выдает результат, не отличающийся от VIRT:
Dervl Derv2
Виртуальные функции и класс person
Теперь, уже зная, что такое виртуальные функции, рассмотрим области их применения. Примером будет служить расширение программ PTR0BJ и PERS0RT. Новая программа использует все тот же класс person, но добавлены два новых класса: student и professor. Каждый из этих производных классов содержит метод isOutstanding(). С помощью этой функции администрация школы может создать список выдающихся педагогов и учащихся, которых следует наградить за их успехи. Листинг программы VIRTPERS:
// vitrpers.срр
// виртуальные функции и класс person
#include <iostream>
using namespace std;
///////////////////////////////////////////////
class person // класс person
{
protected:
char name[40];
public: void getName()
{ cout << " Введите имя: ";
cin >> name; }
void putName()
{ cout << " Имя: " << name << endl; }
virtual void getData() = 0; // чистые
virtual bool isOutstanding() =0; // виртуальные
// функции
};
////////////////////////////////////////////////
class student: public person // класс student
{
private:
float gpa; // средний балл
public:
void getData() //запросить данные об ученике у
{ //пользователя
person::getName();
cout << " Средний балл ученика: ";
cin >> gpa;}
bool isOutstanding() { return (gpa > 3.5)? true: false; }
};
////////////////////////////////////////////////
class professor: public person // класс professor
{
private:
int numPubs; // число публикаций
public:
void getData() //запросить данные о педагоге у
{ //пользователя
person::getName();
cout << " Число публикаций: ";
cin >> numPubs; }
bool isOutstanding() { return (numPubs > 100)? true: false; }
};
////////////////////////////////////////////////
int main() {
person* persPtr[100]; // массив указателей на person
int n = 0; //число людей, внесенных в список
char choice;
do {
cout << " Учащийся (s) или педагог (p): ";
cin >> choice;
if(choice=='s') // Занести нового ученика
persPtr[n] = new student; // в массив
else // Занести нового
persPtr[n] = new professor; // педагога в массив
persPtr[n++]->getData(); //Запрос данных о персоне
cout << " Ввести еще персону (у/п)? "; //создать еще
//персону
cin >> choice;
} while(choice=='y'); // цикл, пока ответ ' у '
for(int j=0; j<n; j++); {
persPtr[j]->putName(); //Вывести все имена,
if(persPtr[j]->isOutstanding()) // сообщать о
cout << " Это выдающийся человек!\n"; //выдающихся
}
return 0;
} //Конец main()
Классы
Класс person — абстрактный, так как содержит чистые виртуальные функции getData()n isOutstanding(). Никакие объекты класса person не могут быть созданы. Он существует только в качестве базового класса для student и professor. Эти порожденные классы добавляют новые экземпляры данных для базового класса. Student содержит переменную gpa типа float, представляющую собой средний балл учащегося. В классе Professor мы создали переменную numPubs типа int, которая представляет собой число публикаций педагога. Учащиеся со средним баллом свыше 3,5 и педагоги, опубликовавшие более 100 статей, считаются вылающимися.
Функция isOutstanding()
IsOutstandingO объявлена чистой виртуальной функцией в классе person. В классе student эта функция возвращает true, если gpa больше 3,5, в противном случае — false. В классе professor она возвращает true, если numPubs больше 100.
Функция GetData() запрашивает у пользователя GPA, если она запускается для обслуживания класса student, или число публикаций для professor.
Программа main()
В функции main() мы вначале даем пользователю возможность ввести несколько имен учащихся и педагогов. К тому же программа спрашивает средний балл учащихся и число публикаций педагогов. После того, как пользователь закончит ввод данных, программа выводит на экран имена всех учащихся и педагогов, помечая выдающихся. Приведем пример работы программы:
Учащийся (s) или педагог (р): s Введите имя: Сергеев Михаил Средний балл ученика: 1.2 Ввести еще персону (у/n)? у Учащийся (s) или педагог (р): s Введите имя: Пупкин Василий Средний балл ученика: 3.9 Ввести еще персону (у/n)? у Учащийся (s) или педагог (р): s Введите имя: Борисов Владимир Средний балл ученика: 4.9 Ввести еще персону (у/n)? у Учащийся (s) или педагог (р): р Введите имя: Михайлов Сергей Число публикаций: 714 Ввести еще персону (у/n)? у Учащийся (S) ипи педагог (р): р Введите имя: Владимиров Борис Число публикаций: 13 Ввести еще персону (у/n)? п
Имя: Сергеев Михаил Имя: Пупкин Василий Это выдающийся человек! Имя: Борисов Владимир Зто выдающийся человек! Имя: Михайлов Сергей Это выдающийся человек! Имя: Владимиров Борис
Виртуальные деструкторы
Деструкторы базового класса обязательно должны быть виртуальными. Допустим, чтобы удалить объект порожденного класса, вы выполнили delete над указателем базового класса, указывающим на порожденный класс. Если деструктор базового класса не является виртуальным, тогда delete, будучи обычным методом, вызовет деструктор для базового класса вместо того, чтобы запустить деструктор для порожденного класса. Это приведет к тому, что будет удалена только та часть объекта, которая относится к базовому классу. Программа VIRTDEST демонстрирует это.
//vertdest.срр
//Тест невиртуальных и виртуальных деструкторов
#include <iostream>
using namespace std;
///////////////////////////////////////////////////////////
class Base {
public:
~Base() // невиртуальный деструктор
//virtual ~Base()
// виртуальный деструктор
{ cout << "Base удален \n"; }
};
///////////////////////////////////////////////////////////
class Derv: public Base {
public: ~Derv()
{ cout << "Derv удален \n"; }
};
///////////////////////////////////////////////////////////
int main() {
Base* pBase = new Derv;
delete pBase;
return 0; }
Программа выдает такой результат:
Base удален
Это говорит о том, что деструктор для Derv не вызывается вообще! К такому результату привело то, что деструктор базового класса в приведенном листинге невиртуальный. Исправить это можно, закомментировав первую строчку определения деструктора и активизировав вторую. Теперь результатом работы программы является:
Derv удален Base удален
Только теперь обе части объекта порожденного класса удалены корректно. Конечно, если ни один из деструкторов ничего особенно важного не делает (например, просто освобождает память, занятую с помощью new), тогда их виртуальность перестает быть такой уж необходимой. Но в общем случае, чтобы быть уверенным в том, что объекты порожденных классов удаляются так, как нужно, следует всегда делать деструкторы в базовых классах виртуальными.
Большинство библиотек классов имеют базовый класс, в котором есть виртуальный деструктор, что гарантирует нам наличие виртуальных деструкторов в порожденных классах.
Виртуальные базовые классы
Рассмотрим ситуацию, представленную на рис. 12.1. Базовым классом является parent, есть два порожденных класса — childl, child2 и есть еще четвертый класс — Grandchild, порожденный одновременно классами Childl и Child2.
В такой ситуации проблемы могут возникнуть, если метод класса Grandchild захочет получить доступ к данным или функциям класса Parent. Что в этом случае будет происходить, показано в программе N0RMBASE.
Индивидуальные очистные сооружения: К классу индивидуальных очистных сооружений относят сооружения, пропускная способность которых...
Индивидуальные и групповые автопоилки: для животных. Схемы и конструкции...
Эмиссия газов от очистных сооружений канализации: В последние годы внимание мирового сообщества сосредоточено на экологических проблемах...
Организация стока поверхностных вод: Наибольшее количество влаги на земном шаре испаряется с поверхности морей и океанов (88‰)...
© cyberpedia.su 2017-2024 - Не является автором материалов. Исключительное право сохранено за автором текста.
Если вы не хотите, чтобы данный материал был у нас на сайте, перейдите по ссылке: Нарушение авторских прав. Мы поможем в написании вашей работы!