Статичне і динамічне зв’язування: поліморфізм, віртуальні функції, заміщення (overriding) функцій, віртуальні деструктори

 

Успадкування надає можливість не тільки довизначити метод, залишений не визначеним

у батьківському класі, але й замістити його іншим ( заміщення — overridding ).

Причому рішення про те, яку саме функцію буде викликано приймається на етапі

виконання програми залежно від конкретного типу створеного об'єкту. Це явище,

при якому об'єкти одного типу (підкласи) заміщують об'єкти іншого типу (суперкласу)

дістало назву поліморфізму.

 

Наступний приклад показує просте застосування поліморфізму

 

 

Заміщувані функції, як і абстрактні, позначаються як віртуальні .

Це свого роду невеликий пережиток мови С++, оскільки в пізніших мовах необхідність

виділення віртуальних функцій відпала — замістити можна будь-яку функцію.

 

class Shape

{

public:

virtual void whatAmI();

};

void Shape::whatAmI()

{

cout <<"I don't know what kind of shape I am!\n";

}

class Rectangle : public Shape

{

public:

void whatAmI();

};

void Rectangle::whatAmI()

{

cout << "I'm a rectangle!\n";

}

class Triangle : public Shape

{

public:

void whatAmI();

};

void Triangle::whatAmI()

{

cout << "I'm a triangle!\n";

}

 

Зверніть увагу, в усіх трьох випадках виклику методу whatAmI використовується

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

значення, на яке указник встановлено. Ясно, що використання імен замість указників,

зробило б поліморфізм неможливим.

 

int main()

{

Shape *s1, *s2, *s3;

 

s1 = new Rectangle;

s2 = new Triangle;

s3 = new Shape;

 

s1->whatAmI();

s2->whatAmI(); (*)

s3->whatAmI();

 

return 0;

}

 

Особливу роль відіграють віртуальні деструктори . Вони призначені

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

нам приклад. Якби деструктор не був віртуальним, то після створенні прямокутника

викликався б лише деструктор фігури, а тому пам'ять, виділена під прямокутник

залишилася б не звільненою.

 

const int sizeOfShape=100;

const int sizeOfRec=60;

class Shape

{

public:

double *description;

Shape()

{

cout<<"Shape constructor was called"<<endl;

description = new double[sizeOfShape];

}

virtual void whatAmI();

virtual ~Shape()

{

delete [] description;

cout<<"Shape destructor was called"<<endl;

}

};

void Shape::whatAmI()

{

cout << "I don't know what kind of shape I am!\n";

}

class Rectangle : public Shape

{

public:

double *description;

Rectangle()

{

cout<<"Rectangle constructor was called"<<endl;

description = new double[sizeOfRec];

}

virtual void whatAmI();

~Rectangle()

{

delete [] description;

cout<<"Rectangle destructor was called"<<endl;

}

};

void Rectangle::whatAmI()

{

cout << "I'm a rectangle!\n";

}

 

int main()

{

Shape *s1, *s2;

 

s1 = new Shape;

s1->whatAmI();

delete s1;

 

cout<<endl;

 

s2 = new Rectangle;

s2->whatAmI();

delete s2;

 

return 0;

}

 

Останній приклад показує спосіб, яким можна звичайний масив замінити масивом

з контролем за виходом індексу за межі масиву. Операція індексування оголошується

віртуальною

 

class IntArray

{

public:

. . . . . . .

 

IntArray& operator= (const IntArray&);

virtual int& opetator[] (int index);

 

int size() const;

. . . . . . .

}

class IntArrayRC: public IntArray

{

public:

IntArrayRC (int sz=defaultArraySize):IntArray(sz){};

IntArrayRC (int* array, int sz):

IntArray(array, sz){};

IntArrayRC (const IntArray&):IntArray(r){};

 

int& operator[] (int index);

private:

void checkRange(int index);

}

int& IntArrayRC ::operator[] (int index)

{

checkRange(index);

return ia[index];

}

#include

void IntArrayRC:: checkRange(int index)

{

assert (index>=0 && index <_size);

}

 

Ще ближче до повного рішення:

 

template

class ArrayRC: public Array

{

public:

ArrayRC (int sz=defaultArraySize):

Array(sz){};

ArrayRC (int* array, int sz):

Array(array, sz){};

ArrayRC (const IntArray&):Array(r){};

elemType& operator[] (int index);

private:

void checkRange(int index);

}

 

Якщо кожен з базових класів випадково чи зумисне міститиме елементи з одним і

тим же іменем, то похідний клас успадкує обидва з них. Для використання таких

елементів може знадобитись уточнення, наприклад, Predator::name .

 

 

class Predator

{

private:

string favoritePrey;

protected:

string name;

public:

Predator(string aName, string aPrey );

~Predator();

setName(const string& aName);

};

class Pet

{

private:

string favoriteToy;

protected:

string name;

public:

Pet(string aName, string aToy );

~Pet();

setName(const string& aName);

};

class Cat : public Predator, public Pet

{

private:

short catID;

static short lastCatID;

public:

Cat(string aName, string aPrey, string aToy );

~Cat();

static void ShowLastID();

};

 

Не дуже добре:

 

Cat myCat(“Murka”, “Mouse” “Mouse”);

//Як Вас тепер називати?

Pet::MyCat.setName(”NotMurka”);

Predator::MyCat.setName(”StillMurka”);

 

Було б зручно мати об'єкт на зразок “іменованого звіра”, а вже від нього виводити

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