Скачиваний:
46
Добавлен:
15.06.2014
Размер:
487.94 Кб
Скачать

1

ООП : наследование

Наследование: это механизм, который позволяет определить новые классы в терминах существующих классов.

При разговоре о наследовании всегда выделяют класс, от которого наследуются его свойства – базовый класс, и класс, который наследуется –

производный класс.

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

Наследование подразумевает использование принципа иерархии и абстрагирования объектов.

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

2

Определение наследования

Как можно определить наследование одного класса другим в С++?

сlass имя_производного_класса : атрибут_наследования имя_базового_класса { … };

Атрибут наследования определяется тремя известными ключевыми словами: private, public и protected, которые использовались для доступа к внутренним компонентам класса.

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

внутри производного. Как атрибут

 

влияет на видимость компонент базового класса?

 

 

 

 

 

 

 

Атрибут

 

 

 

 

 

 

 

 

 

наследования

 

 

 

 

 

 

 

 

 

public

 

private

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Производны

 

 

 

Базовый

 

Производны

 

 

й класс

 

 

 

 

класс

 

й класс

 

 

public

 

 

 

public

 

private

недоступен private недоступен

protected protected private

protected эквивалентен private за одним исключением – все компоненты базового класса с атрибутом доступа protected доступны в производных классах.

3

Конструкторы и деструкторы при наследовании

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

рассмотренных.

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

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

Деструкторы выполняются в обратном порядке.

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

Конструкторы производных и базового классов могут иметь аргументы.

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

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

конструктор_производного_класса (аргументы) : конструктор_базового_класса (аргументы)

4

Наследование - пример

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

#include "iostream" using namespace std;

class A

{

protected: int n;

public:

A(int n=0){ this->n =n; cout << "Con A" <<endl;}

~A(){ cout << "Des A" <<endl;} };

class B : protected A

{

public:

void print(){cout << "n = " <<n <<endl;} B() :A(5) { cout << "Con B" <<endl;} ~B(){ cout << "Des B" <<endl;}

};

void f() { B obj; obj.print();

}

void main() { f();

char s; cin>>s;

}

Результат

выполнения

программы:

5

Наследование – пример 2

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

#include "iostream" using namespace std; class A

{

int n; public:

A(int n) { this->n =n;

cout << "Con A\nn = " <<n <<endl;

}

~A(){ cout << "Des A" <<endl;} };

class B : public A

{

int m; public:

B(int m, int n) : A(n) { this->m = m; cout << "Con B\nm = " <<m <<

endl;}

~B(){ cout << "Des B" <<endl;

}

void f()

 

{

Результат

B obj(1,2);

выполнения

}

программы:

 

void main()

 

{ f();

 

char s;

 

cin>>s;

 

}

 

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

6

Наследование – использование указателей

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

Если такой указатель указывает на экземпляр производного класса, то с его помощью нельзя получить доступ к тем компонентам, которые описаны только внутри производного класса.

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

7

Наследование – пример 3

#include "iostream“

 

void f()

 

using namespace std;

 

{

 

class A

 

A obj1(5);

 

{

 

B obj2(1,2);

ptr->print_A();

int n;

 

A* ptr = &obj1;

public:

 

ptr = &obj2;

ptr->print_A();

A(int n){ this->n =n;

n = " << n <<

B* ptr2 = &obj2; ptr2-

cout << "Constructor A\n

>print_A();

 

endl;

 

ptr2->print_B();

 

}

 

}

 

~A(){ cout << "Destructor A" <<endl; }

void main()

 

void print_A() { cout << "n = " << n

 

<<endl;}

 

{ f(); char s; cin>>s; }

};

 

Результат

class B : public A

 

 

выполнения

{

 

программы:

int m;

 

 

 

public:

 

 

 

B(int m, int n) :A(n) { this->m = m;

 

 

cout << "Constructor B\n

m = " << m <<

 

 

endl;

 

 

 

}

 

 

 

~B(){ cout << "Destructor B" <<endl; }

8

Наследование – пример 4

#include "stdafx.h"

int CShape::GetCoordX() { return x; }

#include <iostream>

int CShape::GetCoordY() { return y; }

#include <math.h>

void CShape::SetCoord(int xS, int yS)

class CShape {

{ x = xS; y = yS;}

public:

 

CShape();

 

//-------------Attributes

 

private:

void main()

int x,y;

public:

{

//------------- Implementation

CShape sh;

int GetCoordX();

sh.SetCoord(10,12);

int GetCoordY();

std::cout<< sh.GetCoordX() << " - "

void SetCoord(int x, int y);

<<

~CShape();

sh.GetCoordY() << std::endl;

};

char s; std::cin>>s;

CShape::CShape(){x = y =

0;}

}

CShape::~CShape() { }

 

9

Наследование (продолжение примера 4)

class CCircle: public CShape {

CCircle::CCircle( ) {R = 0;}

public:

CCircle::~CCircle() { }

CCircle();

int CCircle::GetRad( ) { return R; }

 

void CCircle::SetRad(int r) { R = r; }

//Attributes

int CCircle::GetLengthToCenter()

private:

{

int R;

return static_cast <int>

 

(sqrt(2.0*(R+GetCoordX())*

public:

(R+GetCoordY())));

// Implementation

}

int GetRad();

 

void SetRad(int r);

CCircle cir;

int GetLengthToCenter();

cir.SetCoord(3,3);

~CCircle();

cir.SetRad(4);

};

std::cout<< cir.GetRad() << " - "

 

<< cir.GetLengthToCenter() <<

 

std::endl;

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

10

обоснование необходимости

 

применения

 

 

#include <iostream>

void main()

 

using namespace std;

{

 

class A {

A obj1(1); D

 

obj2('W');

 

int n;

obj1.print();

 

public:

obj2.print();

 

A(int n){this->n = n;}

char s; std::cin>>s;

 

void print(){cout << " Base n=" << n <<

 

endl; }

}

 

};

Результат

 

 

выполнения

 

class D : public A {

программы:

char c;

 

public:

D(char c): A(1) { this->c = c; }

void print(){cout << " Derived c=" << c << endl; }

}; Здесь все выглядит хорошо и логично. В производном и базовом

классах определены функции, отвечающие за вывод внутренних компонент классов. Вызов данных функций происходит с явным указанием экземпляров этих классов.

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

11

обоснование необходимости

применения -продолжение

void main()

void main()

 

{

{

 

A obj1(1); D obj2('W');

A obj1(1); D

 

obj1.print();

obj2('W');

Результат

A *ptr;

выполнения

obj2.print();

ptr = &obj1;

программы:

 

ptr->print();

 

}

ptr = &obj2;

Как так

 

ptr->print();

// Старый вариант

}

получилось?

 

// Новый вариант

 

Когда указатель на базовый класс указывает на экземпляр базового

класса – вызывается функция print этого класса (1-я строка

результата). В случае, когда он указывает на экземпляр производного класса, по идее должна вызываться функция из производного класса. Но этого не происходит! Указатель на базовый класс может быть использован только для доступа к компонентам, которые описаны в базовом классе. Для доступа к компонентам из производных классов необходимо использовать указатель на

производный класс. Эта задача решается путем использования

12

Виртуальные функции – определение

Виртуальная функция задается точно также как и обычная, только в начале определения такой функции ставится

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

Виртуальная функция объявляется внутри базового класса.

Виртуальная функция не может быть статической.

Если виртуальная функция переопределяется в производных классах, то она автоматически в них становится виртуальной и в этом случае нет необходимости в

использовании ключевого слова virtual.

Деструкторы могут быть виртуальными, а конструкторы – нет.

Виртуальная функция может быть вызвана как обычная.

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

13

 

применение

 

Если бы в рассмотренном примере функция print была объявлена виртуальной:

#include <iostream> using namespace std;

class A {

int n;

public:

A(int n){this->n = n;}

virtual void print(){cout << " Base n=" << n << endl; }

};

class D : public A { char c;

public:

D(char c): A(1) { this->c = c; }

virtual void print(){cout << " Derived c=" << c << endl; }

};

void main()

{

A obj1(1); D obj2('W');

obj1.print();

obj2.print();

char s; std::cin>>s;

}

Результат

выполнения

программы:

14

Виртуальные функции и полиморфизм

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

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

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

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

Если бы в рассмотренном примере функция print в классе D описана как:

int print(int a){cout << " Derived c=" << c << endl; return a;}

то результат работы программы будет

Base n =1

15

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

#include <iostream>

void main()

using namespace std;

class B {

{

D d;

public:

B b, *pb = &d;

virtual void f(int) { cout << " fB(int)\n"; }

b.f(6);

virtual void f(double) { cout << "

d.f(6);

fB(dbl)\n"; }

b.f(6.2);

};

d.f(6.2);

class D : public B {

pb->f(6);

pb->f(6.2);

public:

}

virtual void f(int) { cout << " fD(int)\n"; }

 

};

Результат

 

 

выполнения

 

программы:

16

Виртуальные функции – пример 2

#include <iostream> using namespace std;

class A { public:

virtual void print() { cout << " Base A\n”; }

};

class D : public A { public:

void print() { cout << " Derived B \n”; }

};

class D1 : public A { public:

void fun() { cout << " Nothing\n”; }

};

void main()

{

A obj1; D obj2; D1 obj3; A *ptr;

ptr = &obj1; ptr->print(); ptr = &obj2; ptr->print();

ptr = &obj3; ptr->print(); ptr- >fun();

char s; std::cin>>s;

}

Где ошибка?

17

Виртуальные функции – пример 2’

#include <iostream> using namespace std;

class A { public:

virtual void print(){cout << " Base A\n”; }

};

class D : public A { public:

void print(){cout << " DerivedB \n”; }

};

void main()

{

A obj1; D obj2; D1 obj3; A *ptr; D1 *p; ptr = &obj1; ptr->print();

ptr = &obj2; ptr->print();

p = &obj3; p->print(); p->fun();

char s; std::cin>>s;

}

Результат

выполнения

программы:

class D1 : public A { public:

void fun(){cout << " Nothing\n”; }

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

18

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

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

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

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

Для их определения используют следующую запись:

virtual возвращаемый_тип имя_функции (аргументы) =0;

Данная запись при компиляции воспринимается как отсутствие определения виртуальной функции для базового класса.

Вызов такой функции будет восприниматься как ошибка. Если в базовом классе определен прототип чисто виртуальной

функции, то она должна быть обязательно определена для всех производных классов.

Если в базовом классе определена хотя бы одна чисто

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

базовым классом. Для такого класса нельзя создать экземпляр.

19

Раннее и позднее связывание

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

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

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

Так, например, компилятор заранее может не предугадать вызов виртуальной функции для конкретных экземпляров производных классов.

Адрес виртуальной функции вычисляется только при работе

20

Виртуальные деструкторы

Виртуальные деструкторы необходимы в случаях использования указателей на базовые классы при выделении динамической памяти под объекты производных классов

class A { public:

A(char* t)

{cout << " Base A(" <<t << endl; } virtual void print()=0;

virtual ~A() {cout <<" Base destructor\n"; }

};

class Dn : public A { int n;

public:

Dn(int n, char *id="number") : A(id) {this->n = n;}

void print(){cout <<" n=“<< n << endl; }

~Dn () { cout << "Dn destruction\n";

}

};

class Ds : public A { char *str;

public:

Ds(char *str, char *id="string") : A(id) {this->str = str;}

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

~Ds () { cout << "Ds destruction\n"; } };

void f()
{
A *ptr1, *ptr2; ptr1 = new Dn(5);
ptr2 = new Ds("hello"); ptr1->print(); ptr2->print();
delete ptr1; delete ptr2;
}
void main() { f();
char s; std::cin>>s;
}

21

Виртуальные деструкторы -продолжение

Результат

выполнения

программы:

В случае не виртуальных деструкторов при удалении объектов вызывался бы деструктор только базового класса!

Если деструктор базового класса объявлен как виртуальный, все

деструкторы производ-ных классов Как и для виртуальных функций, если в производном классе не

описан деструктор, то при удалении объекта будет вызван деструктор базового класса. Если деструктор описан, то сперва произойдет вызов деструктора производного, а затем базового

класса.

22

Ключевое слово explicit

Ключевое слово explicit запрещает автоматическое создание конвертирующего конструктора.

class A {

public: A(int);

}

// ...

A a = 5; // Автоматическое преобразование int в объект A

Можно запретить создание автоматического конвертирующего

конструктора: class A {

public: explicit A(int);

}

// ...

A a = 5; // (1) Нельзя – преобразование запрещено!

A a = A(5); // (2) Будет работать копирующий конструктор,

//который тоже создается автоматически

(1)– неявный вызов (implicit) конструктора, (2) – явный вызов (explicit)

23

Ключевое слово explicit - пример

#include <iostream>

class cls

{

public:

explicit cls( int ) { std::cout << "int\n";

}

cls( double ) { std::cout <<

"double\n";

}

};

void main()

{

cls ii( 7 ); cls dd = 7;

}

Запись вида «cls ii( 7 );» является явным

вызовом конструктора, а «cls dd = 7;» — неявным. Если бы конструктор с int был в private

секции, то была бы выдана ошибка. Но ключевое слово explicit скывает конструктор так

незаметно, что даже предупреждения компилятор не выдаст. Если скомпилировать и запустить пример, то окажется, что во втором

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

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

Если написать explicit для обоих конструкторов, то ошибки можно будет избежать

— компилятор подскажет где ошибка. Поэтому explicit следует писать для всех конструкторов с одним параметром, если специально не предполагается другое поведение.

24

Тесты …

Вопрос: Что выведет следующая программа:

#include <iostream>

int main() {

Варианты ответа:

int i = 256;

 

unsigned char c;

возникнет ошибка компиляции на строчке 1,

for (c = 0; c < i; c+

// т.к. пропущено тело

+); // 1

цикла

std::cout << (char) c; //

зависит от компилятора

2

символ с кодом 256

// 3

возникнет ошибка компиляции, т.к. в

}

строчке 3

 

// должно быть 'return

 

0;'

 

256

 

ничего

25

Тесты …

Вопрос: Что выведет следующая программа:

#include <iostream>

int main() {

Варианты ответа:

int i = 256;

 

 

unsigned char c;

возникнет ошибка компиляции на строчке 1,

for (c = 0; c < i; c+

 

// т.к. пропущено тело

+); // 1

цикла

std::cout << (char) c; //

зависит от компилятора

2

символ с кодом 256

// 3

возникнет ошибка компиляции, т.к. в

}

строчке 3

 

 

// должно быть 'return

 

0;'

 

 

256

на строчке // 1 программа войдет в бесконечный цикл, т.к. значение переменной c никогда не достигнет 256 (оно будет циклически проходить значения от 0 до 255). Поэтому на экран ничего не будет выведено.