Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

2vcTnguyvU

.pdf
Скачиваний:
6
Добавлен:
15.04.2023
Размер:
955.27 Кб
Скачать

int k; public:

derived(int x){k=x;}

void showk(){cout<<k<<"\n";} }; int main(int argc, char *argv[]) {

derived ob(3);

ob.set(1,2); //ошибка, доступ к функции set() запрещен ob.show();//ошибка, доступ к функции show() запрещен system("PAUSE");

return EXIT_SUCCESS; }

Защищенное наследование

К базовому классу можно применить механизм защищенного наследования. При этом все открытые и защищенные члены базового класса становятся защищенными членами производного класса.

#include <cstdlib> #include <iostream> using namespace std; class base{ protected:

int i,j; public:

void setij(int a, int b){i=a; j=b;}

void showij(){ cout<<i<<" "<<j<<"\n";} }; class derived: protected base{

int k; public:

// класс derived имеет доступ к членам i, j и setij() из класса base void setk( ) { setij(10, 12}; k=i*j;}

void showall(){cout<<k<<" "; showij();}

};

int main(int argc, char *argv[]) { derived ob; ob.setij (1,2);

// ошибка, функция setij ( ) является закрытым членом класса derived ob.setk( ); //верно, вызывается открытый член класса derived ob.showall ( ); // верно, вызывается открытый член класса derived ob.showij ( );

// ошибка, функция setij ( ) является закрытым членом класса derived system("PAUSE");

return EXIT_SUCCESS; }

30

Множественное наследование

Производный класс может одновременно наследовать нескольким базовым классам. Пример множественного наследования:

#include <cstdlib> #include <iostream> using namespace std; class base1{ protected:

int i; public:

base1(int x) {i=x; cout<<”Создание объекта класса base1\n”; } ~base1( ) {cout<<”Уничтожение объекта класса base1 “; } };

class base2{ protected:

int k; public:

base2(int x) {k=x; cout<<”Создание объекта класса base2\n”; } ~base2( ) {cout<<”Уничтожение объекта класса base2 “; }

};

class derived: public base1, public base2{ int j;

public:

derived (int x, int y, int z): base1(x), base2(y) (j=z; cout<<”Создание объекта класса derived\n”; }

~derived( ) {cout<<”Уничтожение объекта класса derived\n”; } void show ( ) {cout<< i<<” “<<j<<” “<<k<<”\n”; } };

int main( ) {

deived ob (4, 7, 9);

ob.show ( ); //Выводит на экран числа 4, 7, 9 system("PAUSE");

return EXIT_SUCCESS; }

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

При множественном наследовании может возникнуть неоднозначность. Пример неправильной программы:

#include <cstdlib> #include <iostream> using namespace std; class base{

31

public:

int i; };

class derived1: public base{ public:

int j; };

class derived2: public base{ public:

int k; };

class derived3: public derived1, public derived2{ public:

int sum; }; int main() {

derived3 ob;

ob.i=10; // Неоднозначность ob.j=20; ob.k=30; ob.sum=ob.i+ ob.j+ ob.k; cout<<ob.i<<” “; cout<<ob.j<<” “<<ob.k<<” “; cout<<ob.sum; system("PAUSE");

return EXIT_SUCCESS; }

Эту программу можно исправить двумя способами. Во-первых, к переменной i можно применить оператор разрешения области видимости. Например,

int main() { derived3 ob;

ob.derived1::i=10; // Неоднозначность устранена ob.j=20;

ob.k=30;

ob.sum= ob.derived1::i + ob.j+ ob.k; cout<< ob.derived1::i <<” “; cout<<ob.j<<” “<<ob.k<<” “; cout<<ob.sum;

system("PAUSE"); return EXIT_SUCCESS; }

Однако это решение порождает новые проблемы. Возможно дублирование объектов базового класса при множественном наследовании. Можно предотвратить дублирование объектов класса base в объекте класса derived3, для этого в объявлении производного класса перед именем базового класса

32

следует

поставить

ключевое слово virtual.

Например, предыдущую

программу можно исправить следующим образом:

 

#include <cstdlib>

 

 

#include <iostream>

 

 

using namespace std;

 

 

class base{

 

 

 

public:

 

 

 

 

int i;

};

 

 

 

class derived1: virtual public base{

 

public:

 

 

 

 

int j;

};

 

 

 

class derived2: virtual public base{

 

public:

 

 

 

 

int k;

};

 

 

 

class derived3: public derived1, public derived2{

 

public:

 

 

 

 

int sum;

};

 

 

int main()

{

derived3 ob;

 

ob.i=10;

// Неоднозначность устранена

 

ob.j=20;

ob.k=30;

 

ob.sum=ob.i+ ob.j+ ob.k;

 

cout<<ob.i<<” “;

cout<<ob.j<<” “<<ob.k<<” “;

 

cout<<ob.sum;

 

 

system("PAUSE"); return EXIT_SUCCESS; }

Как видно, перед именем класса в спецификации производного класса стоит ключевое слово virtual. Теперь оба класса derived1 и derived2 являются наследниками виртуального базового класса base, и любые их наследники будут содержать копию объекта класса base. Следует иметь в виду, что хотя классы derived1 и derived2 объявили класс base виртуальным, его объекты будут по-прежнему частью объектов любого из этих типов.

Шаблоны и наследование

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

33

Лабораторная работа № 6 Тема: Наследование в языке С++

Цель: Изучить парадигму ООП : наследование Задание

№1. Напишите программу, в которой реализована деятельность издательской компании. Эта компания торгует книгами и аудиозаписями этих книг. Создайте класс publication , в котором хранятся название (строка) и цена (типа float) книги. От этого класса наследуют еще два класса: book, который содержит информацию о количестве страниц в книге (типа int), и tape, который содержит время записи книги в минутах (типа float). В каждом из этих трех классов должен быть метод getdata(), через который можно получить данные от пользователя с клавиатуры, и putdata(), предназначенный для вывода этих данных.

2. Скопируйте программу из упр. №1. Добавьте базовый класс sales,

вкотором содержится массив, состоящий из элементов обозначающих месяцы или торговые точки, куда можно записывать общую стоимость проданных книг за последнее время или для определенной торговой точки. Включите в класс методы getdata( ) для получения значений стоимости от пользователя и putdata( ) для вывода этих цифр. Измените классы book и tape так, чтобы стали производными обоих классов: publications и sales. Объекты классов book и tape должны вводить и выводить данные о продажах вместе с другими своими данными. Напишите функцию для создания объектов классов book и tape, чтобы протестировать возможности ввода/вывода данных.

3. Определите базовый класс person, который содержит информацию, являющуюся универсальной для всех людей. Включите в него такую информацию как имя, адрес, дату рождения и пол. Породите от этого класса следующие классы:

class student : virtual public person

{ // соответствующие дополнительные состояния и поведение }; class employee : virtual public person

{ // соответствующие дополнительные состояния и поведение }; class student_employee : public student, public employee

{ // }; № 4. Напишите программу, которая читает файл информации и создает

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

34

служащими-студентами, а также список всех студентов, которые не относятся к служащим.

№ 5. Найдите ошибки в следующей программе, запустите ее. Объясните, когда имеет место замещение, а когда перегрузка?

#include <cstdlib> #include <iostream> using namespace std; class B{ public:

B (int j=3): i(j) { }

virtual void print () const { cout<< "base " << i<< endl;} private:

int i; };

class D: public B{ public :

D (int j =0 ): B (5), i (j) { }

void print ( ) const { cout << " i = " << i <<endl; }

int print (char * s ) const { cout << s << i << endl; return i; }

private:

 

 

 

int i; };

 

 

 

int main(int argc, char *argv[]) {

 

B b1, b2(12), *pb;

 

 

D d1, d2 (17), *pd=&d2;

 

b1.print(); b2.print ();

 

d1.print ();

d2.print ();

 

b1.print("b1.i = ");

b2.print("b2.i =

");

d1.print ("d1.i = ");

d2.print (" d2.i =

");

pb=pd; pb->print(); pb->print("d2.i=

");

pd->print();

pd->print (" d2.i= ");

 

system("PAUSE");

return EXIT_SUCCESS; }

Контрольные вопросы

Истинно ли следующее утверждение: невозможно сделать объект одного класса членом другого класса?

Истинно ли следующее утверждение: если конструктор производного класса не определен, то объекты этого класса будут использовать конструкторы базового класса?

Допустим, что базовый и производный классы включают в себя методы с одинаковыми именами. Какой из методов будет вызван объектом производного класса, если не использована операция разрешения имени?

35

Глава 7. Абстрактные классы, динамическая информация о типе

Полиморфизм — один из четырёх важнейших механизмов объектноориентированного программирования (наряду с абстракцией, инкапсуляцией

инаследованием). Полиморфизм позволяет писать более абстрактные программы и повысить коэффициент повторного использования кода.

Вобъектно-ориентированных языках класс является абстрактным типом данных. Полиморфизм реализуется с помощью наследования классов

ивиртуальных функций. Класс-потомок наследует сигнатуры методов класса-родителя, а реализация, в результате переопределения этих методов может быть другой, соответствующей специфике класса-потомка. Функции могут работать с объектом класса-родителя, но при этом вместо него во время исполнения будет подставляться один из классов-потомков. Это называется поздним связыванием.

Чтобы производные классы были удобной формой краткого описания, в реализации языка должен быть решен вопрос: к какому из производных классов относится объект, на который смотрит указатель base*? Существует три основных способа ответа:

поместить в базовый класс поле типа, которое смогут проверять функции;

обеспечить, чтобы указатель мог ссылаться на объекты только одного типа;

использовать виртуальные функции.

Указатели на базовые классы обыкновенно используются при проектировании контейнерных классов: множество, вектор, список и т.д. Тогда в первом случае получим однородные списки, т.е. списки объектов одного типа. Второй и третий способы позволяют создавать разнородные списки, т.е. списки объектов нескольких различных типов (на самом деле, списки указателей на эти объекты). Третий способ наиболее предпочтителен. Вначале обсудим простой способ с полем типа. Пример с классами manager и employee можно переопределить так:

class employee { public:

enum empl_type { M, E }; empl_type type; employee* next;

char*

name;

short

department;

// ...

 

};

 

class

manager : public employee {

 

36

public: employee* group; short level;

// ...

}; Имея эти определения, можно написать функцию, печатающую данные о

произвольном служащем:

void print_employee(const employee* e) { switch (e->type) {

case E:

cout << e->name << '\t' << e->department << '\n';

//...

break; case M:

cout << e->name << '\t' << e->department << '\n';

//...

manager* p = (manager*) e;

cout << "level" << p->level << '\n'; // ...

break; }

}

Напечатать список служащих можно так: void f(const employee* elist)

{for (; elist; elist=elist->next) print_employee(elist); }

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

void print(const employee* e)

{

cout << e->name << '\t' << e->department << '\n'; // ...

if (e->type == M) { manager* p = (manager*) e;

cout << "level" << p->level << '\n';

37

// ...

} }

Операторы if, подобные приведенным в примере, сложно найти в большой функции, работающей со многими производными классами. Но даже когда они найдены, нелегко понять, что происходит на самом деле. Кроме того, при всяком добавлении нового вида служащих требуются изменения во всех важных функциях программы, т.е. функциях, проверяющих поле типа. В результате приходится править важные части программы, увеличивая тем самым время на отладку этих частей. Иными словами, использование поля типа чревато ошибками и трудностями при сопровождении программы. Трудности резко возрастают по мере роста программы, ведь использование поля типа противоречит принципам модульности и сокрытия данных. Каждая функция, работающая с полем типа, должна знать представление и специфику реализации всякого класса, являющегося производным для класса, содержащего поле типа.

Виртуальные функции

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

class employee { char* name; short department; // ...

employee* next; static employee* list; public:

employee(char* n, int d); // ...

static void print_list(); virtual void print() const; };

Как известно, служебное слово virtual (виртуальная) показывает, что функция print() может иметь разные версии в разных производных классах, а выбор нужной версии при вызове print( ) - это задача компилятора. Тип функции указывается в базовом классе и не может быть переопределен в производном классе. Определение виртуальной функции должно даваться для того класса, в котором она была впервые описана (если только она не является чисто виртуальной функцией). Например:

void employee::print() const {

38

cout << name << '\t' << department << '\n'; // ...

}

Виртуальную функцию можно использовать, даже если нет производных классов от ее класса. В производном же классе не обязательно переопределять виртуальную функцию, если она там не нужна. При построении производного класса надо определять только те функции, которые в нем действительно нужны:

class manager : public employee { employee* group;

short level; // ...

public:

manager(char* n, int d); // ...

void print() const;

};

Место функции print_employee() заняли функции-члены print().

void employee::print_list()

{

for ( employee* p = list; p; p=p->next) p->print(); }

Очевидно, что для правильной работы виртуальной функции нужно в каждом объекте класса employee хранить некоторую служебную информацию о типе. Как правило, реализации в качестве такой информации используют просто указатель. Этот указатель хранится только для объектов класса с виртуальными функциями, но не для объектов всех классов, и даже не для всех объектов производных классов. Дополнительная память отводится только для классов, в которых описаны виртуальные функции. Заметим, что при использовании поля типа, для него все равно нужна дополнительная память.

Если в вызове функции явно указана операция разрешения области видимости ::, например, в вызове manager::print(), то механизм вызова виртуальной функции не действует. Иначе подобный вызов привел бы к бесконечной рекурсии. Уточнение имени функции дает еще один положительный эффект: если виртуальная функция является подстановкой (в этом нет ничего необычного), то в вызове с операцией :: происходит подстановка тела функции. Это эффективный способ вызова, который можно применять в важных случаях, когда одна виртуальная функция обращается к другой с одним и тем же объектом. Пример такого случая - вызов функции manager::print(). Поскольку тип объекта явно задается в самом вызове manager::print(), нет нужды определять его в динамике для функции employee::print(), которая и будет вызываться.

39

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]