Цель работы: 1) изучить возможности наследования классов на языке С++; 2) получить основные навыки программирования с использованием наследования классов. Теоретические сведения
Язык С++ позволяет классу наследовать данные-элементы и функции-элементы одного или нескольких других классов. Новый класс называют производным классом. Класс, элементы которого наследуются производным классом, называется базовым классом. В свою очередь производный класс может служить базовым для другого класса. Наследование дает возможность заключить некоторое общее или схожее поведение различных объектов в одном базовом классе.
Наследование позволяет также изменить поведение существующего класса. Производный класс может переопределить некоторые функции-элементы базового, наследуя, тем не менее, основной объем свойств и атрибутов базового класса. Общий вид наследования:
class Base { // ... }; class Derived: <ключ доступа> Base { // ..... };
Ключ доступа может быть private, protected, public. Если ключ не указан, то по умолчанию он принимается private. Наследование позволяет рассматривать целые иерархии классов и работать со всеми элементами одинаково, приводя их к базовому. Правила приведения следующие: Наследуемый класс всегда можно привести к базовому;
Базовый класс можно привести к наследуемому только если в действительности это объект наследуемого класса. Ошибки приведения базового класса к наследуемому отслеживаются программистом.
При наследовании ключ доступа определяет уровень доступа к элементам базового класса внутри производного класса. В таблице описаны возможные варианты доступа.
Наследование | Доступ в базовом классе | Доступ в производном классе |
---|---|---|
public | public protected private | public protected private |
protected | public protected private | protected protected private |
private | public protected private | private private private |
Конструкторы не наследуются. Если конструктор базового класса требует спецификации одного или нескольких параметров, конструктор производного класса должен вызывать базовый конструктор, используя список инициализации элементов. Пример 1.
#include <string> class Base { public: Base(int, float); }; class Derived: Base { public: Derived(char* lst, float amt); }; Derived::Derived(char* lst, float amt) : Base(strlen(lst),amt) { }
В деструкторе производного класса компилятор автоматически генерирует вызовы базовых деструкторов, поэтому для удаления объекта производного класса следует сделать деструктор в базовых классах виртуальным. Для вызова используется delete this либо operator delete.
Функция-элемент может быть объявлена как virtual. Ключевое слово virtual предписывает компилятору генерировать некоторую дополнительную информацию о функции. Если функция переопределяется в производном классе и вызывается с указателем (или ссылкой) базового класса, ссылающимся на представитель производного класса, эта информация позволяет определить, какой из вариантов функции должен быть выбран: такой вызов будет адресован функции производного класса.
Для виртуальных функций существуют следующие правила: виртуальную функцию нельзя объявлять как static. спецификатор virtual необязателен при переопределении функции в производном классе. виртуальная функция должна быть определена в базовом классе и может быть переопределена в производном.
Пример программирования Пример 2. Написать программу с наследованием класса стек от класса массив.
#include <iostream> #include <cstdlib> class massiv { int *num; int kol; public: massiv(int n); void print(); virtual int kolich() { return kol; } void put(int k,int n) { num[k]=n; } ~massiv() { delete[] num; } }; massiv::massiv(int n) { num = new int[n]; kol =n; for (int i=0; i < kol; i++) num[i] = random(100) - 50; } void massiv::print() { for (int i = 0; i < kolich(); i++) std::cout << num[i] << " "; std::cout << std::endl; } class stec : public massiv { int top; public: stec(int); virtual int kolich() {return top;} void pop(int k); }; stec::stec(int n):massiv(n) { top=0; } void stec::pop(int k) { put(top++,k); } int main() { randomize(); massiv a(10); a.print(); stec b(10); b.pop(random(100)-50); b.pop(random(100)-50); b.pop(random(100)-50); b.print(); }
Главное отличие виртуальной функции от просто перегруженной в том, какая функция будет вызываться при рассмотрении производного класса как базового. Пример 3.
#include <iostream> class Base { public: Base() {} Print() { std::cout << "I'm a Base print" << std::endl; } virtual View(){ std::cout << "I'm a Base view" << std::endl; } }; class Derived: public Base { public: Derived(){}; Print(){ std::cout << "I'm a Derived print" << std::endl; } View(){ std::cout << "I'm a Derived view" << std::endl; } }; int main(void) { Base *A=new Base; Derived *B=new Derived; Base *C; A->Print(); A->View(); B->Print(); B->View(); C=(Base *)B; C->Print(); C->View(); return 0; }
Результат: "I'm a Base print" "I'm a Base view" "I'm a Derived print" "I'm a Derived view" "I'm a Base print" "I'm a Derived view"
Таким образом, мы видим, что виртуальные функции позволяют нам всегда работать с теми функциями, которые специфичны именно для используемого класса, даже когда мы рассматриваем его как базовый.