Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
C__lab_11.doc
Скачиваний:
3
Добавлен:
12.11.2019
Размер:
266.75 Кб
Скачать

Лабораторная работа № 11 Наследование классов

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

          Создать класс-наследник можно при помощи объявления класса с указанием его родительского класса (в С++ можно наследовать от нескольких родительских классов, но для простоты будем ограничиваться одним). Такое объявление имеет вид:

class tchild : public tparent{        // Указывается уровень доступа к //родительскому классу

private:// Приватная секция дочернего класса

protected: // Защищенная секция дочернего класса

public: // Открытая секция дочернего класса

}; // Конец объявления дочернего класса

          В заголовке класса указан уровень доступа public (почти всегда он и указывается). Это означает, что все секции родительского класса, определяющие уровень доступа к своим полям и методам из программного кода программы, остаются прежними. Другие спецификаторы доступа private и protected понижают уровень секции родительского класса до своего уровня или (если он ниже) оставляют неизменным. Таким образом, public - оставляет в родительском классе все, как было; protected – понижает в родительском классе уровень секции public до protected; а private – делает все секции родительского класса закрытыми в дочернем классе. Это означает (в случае private), что из программного кода нельзя будет обратиться к открытым полям и методам родительского класса, используя объектную переменную дочернего класса (а используя объектную переменную самого родительского класса - можно).

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

class classto //Объявление включаемого класса

{        // Поля и методы включаемого класса

void methodto();

};

class classone // Объявление класса контейнера

{ public:

classto fd; // Объявление поля объектного типа

};

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

1.   объявить объектную переменную класса-контейнера classone ob;

2.   использовать двойную квалификацию      ob.fd.methodto().

Отметим, что при наследовании двойной квалификации не требуется и обращение к методу родительского класса осуществляется так же (если он был в секции public или protected), как и к собственному методу:

          tchild ob;

          ob.methodparent().

Наследование и встраивание (включение) нельзя рассматривать как альтернативы: при создании библиотек классов нельзя обойтись без того и другого. Что касается прикладных программ, то наследование используют, если это позволяет более адекватно передать специфику предметной области (предметная область имеет иерархию сущностей) или необходимо полиморфное поведение объектов. Но, как правило, использовать встраивание проще.

          Понятие полиморфизма есть еще более высокоуровневый, чем перегрузка функций и шаблоны, механизм абстрагирования от особенностей реализации. Этот механизм работает только при динамическом создании объектов и позволяет повысить функциональность за счет использования виртуальных функций.

          Указатели на объекты обладают очень важным свойством: указатель на объект базового класса может указывать на любой объект в иерархии классов, порожденных от базового класса. При этом, к сожалению, через такой указатель можно получить доступ только к полям и методам базового класса (из секций private и protected). Можно попытаться для решения этой проблемы сделать динамическое приведение типа к типу нужного класса. Но истинный тип динамического объекта не так то и просто узнать. А, кроме того, есть более простой и надежный путь, не требующий никаких усилий со стороны программиста – использование виртуальных методов. Метод класса становится виртуальным, если в начале его объявления вставить ключевое слово virtual. Использование виртуальных методов имеет ряд ограничений:

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

2.   не допускается изменение заголовка метода;

3.   нежелательно перегружать виртуальный метод в том же классе;

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

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

// Простой пример использования виртуальной функции

#include <iostream>

using namespace std;

class base { // Базовый класс

public:

    int i;

    base(int x) { i = x; } // Конструктор с параметрами

    virtual void func() // Виртуальный метод

    {

        cout<<"Выполнение функции func() базового класса: ";

        cout << i << '\n';

    }

};

class derived1 : public base {

public:

derived1(int x) : base(x) { } // В конструкторе производного класса

//вызывается конструктор базового класса

    void func()

    {

         cout<<"Выполнение функции func() класса derived1:";

         cout << i * i << '\n';

    }

};

class derived2 : public base { // Второй производный от базового класс

public:

          derived2(int x) : base(x) { } // В конструкторе производного класса //вызывается конструктор базового класса

void func()

{ cout << "Выполнение функции func() класса derived2: ";

cout << i + i << '\n';    }

};

int main()

{ base *p; // Указатель на базовый класс будет использован для 

//производных классов

base ob(10);

derived1 d_ob1(10);

derived2 d_ob2(10);

p = &ob;

p->func(); // функция func() класса base

p = &d_ob1;// Указатель на базовый класс используется для                                            //производного класса

p->func(); // функция func() производного класса derived1

p = &d_ob2; // Указатель на базовый класс используется для

//производного класса

p->func(); // функция func() производного класса derived2

int n; // Для задержки закрытия

cin>>n; // окна консоли

return 0;}

          Выведенный на рисунке текст отличается от того, что в листинге, переводом с английского, что связано с разными кодировками символов в windows и dos.

Рисунок. Применение виртуальных методов

          В программе демонстрируется также вызов в конструкторе производного класса методика вызова конструктора базового класса:

          derived1(int x) : base(x) { }

          Здесь вызов конструктора базового класса осуществляется не в теле конструктора производного класса, а специальным методом: через двоеточие и перед телом конструктора производного класса. Еще следует обратить внимание на то, что derived1(int x) – это объявление конструктора производного класса, а base(x) – это вызов конструктора базового класса (и поэтому х, а не int x).

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

          Следует обратить внимание на то, как происходит вызов конструкторов и деструкторов в иерархии классов:

1.   конструкторы вызываются вниз по иерархии классов, начиная от базового класса (это понятно: сначала надо заполнить поля базового класса, потом те, что добавлялись);

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

          Это иллюстрирует следующая программа:

// Пример вызова конструкторов и деструкторов

#include <iostream>

using namespace std;

class base {// Объявление базового класса

public:

   int  x;

    base(int x) {this->x=x; // this – неявно передаваемый указатель на объект, //вызвавший метод; здесь нужен, что отличить поле //от переменной; вместо точки для указателя //ставится «->».

    cout<<"the designer of a base class is caused  \n";

    cout<<"x="<<x<<endl;}

    ~base() {cout<<"it is caused destructor a base class  \n"; }// Деструктор

};// Конец объявления базового класса

class derived1 : public base { // Объявление класса-дочки

public:

    double y;

    derived1(int x,double y) : base(x) {this->y=y;

    cout<<"the designer of a daughter is caused:\n";

    cout<<"y="<<y<<endl;}

    ~derived1() {cout<<"it is caused destructor daughters:\n";}

}; // Конец объявления класса-дочки

class derived2 : public derived1 { // Объявление класса-внучки

public:

    char ch;

    derived2(int x,double y,char ch) : derived1(x,y){this->ch=ch;

     cout<<"the designer of the grand daughter is caused:\n";

     cout<<"ch="<<ch<<endl;}

    ~derived2() {cout<<"it is caused destructor grand daughters:\n";}

}; // Конец объявления класса-внучки

int main()

{

    derived2 *ob= new derived2(12, 2.7, 'a'); // Объявление объекта-внучки

    delete ob; // уничтожение объекта-внучки

    int n; //    Для задержки вывода

    cin>>n; // в окно консоли

    return 0;

} }

Рисунок. Пример вызова конструкторов и деструкторов

          Программа имеет ряд особенностей, которые необходимо отметить:

1.   удобно передаваемые в конструкторе параметры называть именами поля (труднее напутать, если параметров несколько);

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

3.   если внутри main использовать не указатель и динамическое создание переменной

derived2 *ob= new derived2(12, 2.7, 'a');                                                                                                                                  а обычную объектную переменную

derived2 ob(12, 2.7, 'a');     

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

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

         Следующая простая программа демонстрирует множественное наследование:

#include <iostream>

using namespace std;

class b1 { // Первый базовый класс

    int x; // Поле х закрытое, поэтому нужен открытый метод get_x для доступа

public:

    b1(int x) {this->x = x; } // Конструктор использует указатель на объект this

    int get_x() { return x; } // Открытый метод доступа к х

};

class b2 { // Второй базовый класс

    double y; // Поле у закрытое, поэтому нужен открытый метод get_y для //доступа

public:

    b2(double y){this->y = y;} // Конструктор использует указатель this

   double get_y() { return y; } // Открытый метод доступа к у

};

class d : public b1, public b2 { // Наследуются два класса

    char c;

public:

    d(int x, double y, char c) : b1(x), b2(y) { this->c = c; }

/* Конструктор производного класса вызывает два конструктора базовых классов в том же порядке, в каком наследует */

    void show() {

        cout << get_x() << ' ' << get_y() << ' '; // Вывод в окно консоли

        cout << c << '\n';

    }

};

int main()

{

    d ob(1, 2.5, 's'); // Объявляется объект производного класса

    ob.show(); // Выводятся поля объекта производного класса

    int i; // Задержка закрытия

    cin>>i; // окна консоли

    return 0;

}

Рисунок. Пример множественного наследования

         Множественное наследование иногда необходимо, но чаще это следствие неудачного проектирования.

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