Таблица виртуальных методов

Способ определения

Одни языки программирования (например, C++, C#) требуют явно указывать, что данный метод является виртуальным. В других языках (например, Java, Python) все методы являются виртуальными по умолчанию (но только те методы, для которых это возможно; например в Java методы с доступом private не могут быть переопределены в связи с правилами видимости).

Базовый класс может и не предоставлять реализации виртуального метода, а только декларировать его существование. Такие методы без реализации называются «чистыми виртуальными» (перевод англ.  pure virtual) или абстрактными. Класс, содержащий хотя бы один такой метод, тоже будет абстрактным. Объект такого класса создать нельзя (в некоторых языках допускается, но вызов абстрактного метода приведёт к ошибке). Наследники абстрактного класса должны предоставить реализацию для всех его абстрактных методов, иначе они, в свою очередь, будут абстрактными классами. Абстрактный класс, который содержит только абстрактные методы, называется интерфейсом.

Example 2: C++ virtual Function Demonstration

Output

Animal: Animal
Animal: Dog
Animal: Cat

Here, we have used the virtual function and an pointer ani in order to avoid repeating the function in every class.

In , we have created 3 pointers to dynamically create objects of , and classes.

We then call the function using these pointers:

  1. When is called, the pointer points to an object. So, the virtual function in class is executed inside of .
  2. When is called, the pointer points to a object. So, the virtual function is overridden and the function of is executed inside of .
  3. When is called, the pointer points to a object. So, the virtual function is overridden and the function of is executed inside of .

Пример виртуальной функции на C++


Диаграмма класса Animal

Пример на C++, иллюстрирующий отличие виртуальных функций от невиртуальных:

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

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

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

class Animal {
public
    void /*невиртуальный*/ move() { 
        std::cout << "This animal moves in some way" << std::endl; 
    }
    virtual void eat() {
        std::cout << "Animal eat something!" << std::endl; 
    }
    virtual ~Animal(){} // деструктор
};

class Wolf  public Animal {
public
    void move() { 
        std::cout << "Wolf walks" << std::endl; 
    }
    void eat(void) { // метод eat переопределён и тоже является виртуальным
        std::cout << "Wolf eats meat!" << std::endl; 
    }
};

int main() {
    Animal* zoo[] = {new Wolf(), new Animal()};
    for(Animal* azoo) {
        a->move();
        a->eat();
        delete a; // Так как деструктор виртуальный, для каждого 
                  // объекта вызовется деструктор его класса
    }
    return ;
}

Вывод:

This animal moves in some way
Wolf eats meat!
This animal moves in some way
Animal eat something!

Вызов

Вызов происходит при разыменовании vpointer из : просмотр записи о в vtable, а затем разыменование этого указателя вызывает код.

В случае одиночного наследования (или в случае языка с поддержкой только одиночного наследования), если vpointer всегда является первым элементом в (как это происходит у многих компиляторов), то это решается следующим псевдо-C++ кодом:

*((*d)[])(d)

В более общем случае, как упоминалось выше, вызов , и на будет сложнее

*((d->/*указатель ТВМ D (для B1)*/)[])(d)    // d->f1();
*((d->/*указатель ТВМ D (для B2)*/)[])(d+8)  // d->f2();
*((/*адрес ТВМ B2 */)[])(d+8)               // d->B2::f2();

Для сравнения, вызов гораздо проще:

*B1::f0(d)

Use of C++ Virtual Functions

Suppose we have a base class and derived classes and .

Suppose each class has a data member named type. Suppose these variables are initialized through their respective constructors.

Now, let us suppose that our program requires us to create two functions for each class:

  1. to return the value of type
  2. to print the value of type

We could create both these functions in each class separately and override them, which will be long and tedious.

Or we could make virtual in the class, then create a single, separate function that accepts a pointer of type as its argument. We can then use this single function to override the virtual function.

This will make the code shorter, cleaner, and less repetitive.

Как заполняются виртуальные таблицы?

Из примера, приведенного выше, у нас есть только две виртуальные функции, поэтому каждая виртуальная таблица будет иметь две записи (одна для function1() и одна для function2()). Помните, что при заполнении виртуальных таблиц выбираются наиболее дочерние методы, доступ к которым имеют объекты.

Виртуальная таблица для объектов класса Parent проста. Объект класса Parent имеет доступ только к членам класса Parent, он не имеет доступ к членам классов C1 и C2. Следовательно, запись function1 будет указывать на Parent::function1(), а запись function2 будет указывать на Parent::function2().

Виртуальная таблица для C1 уже немного сложнее. Объект класса C1 имеет доступ как к членам C1, так и к членам Parent. Однако C1 имеет переопределение function1(), что делает C1::function1() более дочерним методом, нежели Parent::function1(). Следовательно, запись function1 будет указывать на C1::function1(). C1 не переопределяет function2(), поэтому запись function2 остается указывать на Parent::function2().

В виртуальной таблице для C2 запись function1 будет указывать на Parent::function1(), а запись function2 будет указывать на C2::function2().

Смотрим:

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

Рассмотрим, что произойдет при создании объекта класса C1:

int main()
{
C1 c1;
}

1
2
3
4

intmain()

{

C1 c1;

}

Поскольку является объектом класса C1, то он имеет свой , который указывает на виртуальную таблицу класса C1.

Теперь создадим указатель класса Parent на объект :

int main()
{
C1 c1;
Parent *cPtr = &c1;
}

1
2
3
4
5

intmain()

{

C1 c1;

Parent*cPtr=&c1;

}

Поскольку является указателем класса Parent, то он указывает только на часть Parent объекта . Однако, тоже находится в части Parent, поэтому имеет доступ к этому указателю. Наконец, будет указывать на виртуальную таблицу C1, поскольку  указывает на объект класса C1! Даже если является указателем класса Parent, он всё равно имеет доступ к виртуальной таблице C1.

Поэтому, что произойдет, если мы попытаемся вызвать ?

int main()
{
C1 c1;
Parent *cPtr = &c1;
cPtr->function1();
}

1
2
3
4
5
6

intmain()

{

C1 c1;

Parent*cPtr=&c1;

cPtr->function1();

}

Во-первых, компилятор распознает, что function1() является виртуальной функцией. Во-вторых, он будет использовать для перехода к виртуальной таблице C1. В-третьих, он будет искать, какую версию function1() вызывать в виртуальной таблице C1. Он найдет C1::function1(). Следовательно, будет вызывать C1::function1()!

Теперь вы можете спросить: «А если бы указывал на объект класса Parent вместо объекта класса C1? Вызывал бы ли он по-прежнему C1::function1()?». Ответ: «Нет, не вызывал бы!».

int main()
{
Parent p;
Parent *pPtr = &p;
pPtr->function1();
}

1
2
3
4
5
6

intmain()

{

Parentp;

Parent*pPtr=&p;

pPtr->function1();

}

В этом случае, при создании объекта , указывает на виртуальную таблицу класса Parent вместо C1. Следовательно, также будет указывать на виртуальную таблицу класса Parent. Запись function1() в виртуальной таблице класса Parent будет указывать на Parent::function1(). Таким образом, будет вызывать Parent::function1(), который является наиболее дочерним методом, доступ к которому имеет объект .

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

Вызов виртуальной функции происходит медленнее, чем вызов не виртуальной функции из-за следующего:

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

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

   И только тогда мы сможем выполнить вызов функции.

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

Решение

Вот как я понял не только то, что функции есть, но зачем они нужны:

Допустим, у вас есть эти два класса:

В вашей основной функции:

Пока все хорошо, правда? Животные едят дженерики, кошки едят крыс, все без ,

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

Теперь наша основная функция:

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

Решение состоит в том, чтобы сделать от Класс виртуальной функции:

Главный:

Готово.

2376

Эффективность

Виртуальный вызов требует как минимум дополнительно индексированного разыменования, а иногда дополнительной «адресной привязки» (fixup), схожей с невиртуальным вызовом, который является простым переходом к скомпилированному указателю. Поэтому вызов виртуальных функций по сути медленнее, чем вызов невиртуальных. Эксперимент, проведённый в 1996 году, показал, что примерно 6-13% времени выполнения тратится просто на поиск соответствующей функции, в то время как общий рост времени выполнения может достичь 50%. Стоимость использования виртуальных функций на современных архитектурах процессоров может быть не столь высока из-за наличия значительно больших кэшей и лучшего предсказания переходов.

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

Для избежания подобных потерь компиляторы обычно избегают использования vtable всегда, когда вызов может быть выполнен во время компиляции.

Таким образом, вышеприведеный вызов может и не требовать просмотра vtable, так как компилятор может сообщить о том, что может иметь в этой точке только , а не переопределяет . Или компилятор (как вариант, оптимизатор) может обнаружить отсутствие подклассов в программе, переопределяющей . Вызов или вероятно не потребует просмотра vtable благодаря реализации, определенной явным образом (хотя все еще требуется привязка по указателю ‘this’).

Другие решения

Просто используйте шаблоны до конца:

Теперь назовите это:

2

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

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

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

Я признаю, что это раздражает, и было бы неплохо сказать «Я хочу, чтобы этот шаблон был виртуальным, но только со следующими типами».

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

1

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

Оказывается, мы можем определить чистые виртуальные функции:

#include <iostream>
#include <string>

class Animal // это абстрактный родительский класс
{
protected:
std::string m_name;

public:
Animal(std::string name)
: m_name(name)
{
}

std::string getName() { return m_name; }
virtual const char* speak() = 0; // окончание «= 0» означает, что эта функция является чистой виртуальной функцией
};

const char* Animal::speak() // несмотря на то, что вот здесь её определение
{
return «buzz»;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

#include <iostream>
#include <string>
 

classAnimal// это абстрактный родительский класс

{

protected

std::stringm_name;

public

Animal(std::stringname)

m_name(name)

{

}

std::stringgetName(){returnm_name;}

virtualconstchar*speak()=;// окончание «= 0» означает, что эта функция является чистой виртуальной функцией

};

constchar*Animal::speak()// несмотря на то, что вот здесь её определение

{

return»buzz»;

}

В этом случае speak() по-прежнему считается чистой виртуальной функцией (хотя позже мы её определили), а Animal по-прежнему считается абстрактным родительским классом (и, следовательно, объекты этого класса не могут быть созданы). Любой класс, который наследует класс Animal, должен переопределить метод speak() или он также будет считаться абстрактным классом.

При определении чистой виртуальной функции, её тело (определение) должно быть записано отдельно (не встроено).

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

#include
#include

class Animal // это абстрактный родительский класс
{
protected:
std::string m_name;

public:
Animal(std::string name)
: m_name(name)
{
}

std::string getName() { return m_name; }
virtual const char* speak() = 0; // обратите внимание, speak() является чистой виртуальной функцией
};

const char* Animal::speak()
{
return «buzz»; // реализация по умолчанию
}

class Dragonfly: public Animal
{

public:
Dragonfly(std::string name)
: Animal(name)
{
}

virtual const char* speak() // этот класс уже не является абстрактным, так как мы переопределили функцию speak()
{
return Animal::speak(); // используется реализация по умолчанию класса Animal
}
};

int main()
{
Dragonfly dfly(«Barbara»);
std::cout

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

#include
#include

classAnimal// это абстрактный родительский класс

{

protected

std::stringm_name;

public

Animal(std::stringname)

m_name(name)

{

}

std::stringgetName(){returnm_name;}

virtualconstchar*speak()=;// обратите внимание, speak() является чистой виртуальной функцией

};

constchar*Animal::speak()

{

return»buzz»;// реализация по умолчанию

}

classDragonflypublicAnimal

{

public

Dragonfly(std::stringname)

Animal(name)

{

}

virtualconstchar*speak()// этот класс уже не является абстрактным, так как мы переопределили функцию speak()

{

returnAnimal::speak();// используется реализация по умолчанию класса Animal

}

};

intmain()

{

Dragonfly dfly(«Barbara»);

std::cout

Результат выполнения программы:

Хотя это используется редко.

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

Тем не менее, мы сталкивались с проблемой, когда родительский указатель или ссылка вызывали только родительские методы, а не дочерние. Например:

#include <iostream>

class Parent
{
public:
const char* getName() { return «Parent»; }
};

class Child: public Parent
{
public:
const char* getName() { return «Child»; }
};

int main()
{
Child child;
Parent &rParent = child;
std::cout << «rParent is a » << rParent.getName() << ‘\n’;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

#include <iostream>
 

classParent

{

public

constchar*getName(){return»Parent»;}

};

classChildpublicParent

{

public

constchar*getName(){return»Child»;}

};

intmain()

{

Childchild;

Parent&rParent =child;

std::cout<<«rParent is a «<<rParent.getName()<<‘\n’;

}

Результат:

Поскольку является ссылкой класса Parent, то вызывается Parent::getName(), хотя фактически мы ссылаемся на часть Parent объекта .

На этом уроке мы рассмотрим, как можно решить эту проблему с помощью виртуальных функций.

Виртуальная функция в языке С++ — это особый тип функции, которая, при её вызове, вызывает «наиболее» дочерний метод, который существует между родительским и дочерними классами. Это свойство еще известно, как полиморфизм. Дочерний метод вызывается тогда, когда совпадает сигнатура (имя, типы параметров и является ли метод константным) и тип возврата дочернего метода с сигнатурой и типом возврата метода родительского класса. Такие методы называются переопределениями (или «переопределенными методами»).

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

#include <iostream>

class Parent
{
public:
virtual const char* getName() { return «Parent»; } // добавили ключевое слово virtual
};

class Child: public Parent
{
public:
virtual const char* getName() { return «Child»; }
};

int main()
{
Child child;
Parent &rParent = child;
std::cout << «rParent is a » << rParent.getName() << ‘\n’;

return 0;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

#include <iostream>
 

classParent

{

public

virtualconstchar*getName(){return»Parent»;}// добавили ключевое слово virtual

};

classChildpublicParent

{

public

virtualconstchar*getName(){return»Child»;}

};

intmain()

{

Childchild;

Parent&rParent =child;

std::cout<<«rParent is a «<<rParent.getName()<<‘\n’;

return;

}

Результат:

Поскольку является ссылкой на родительскую часть объекта , то, обычно, при обработке вызывался бы Parent::getName(). Тем не менее, поскольку Parent::getName() является виртуальной функцией, то компилятор понимает, что нужно посмотреть, есть ли переопределения этого метода в дочерних классах. И компилятор находит Child::getName()!

Рассмотрим пример посложнее:

#include <iostream>

class A
{
public:
virtual const char* getName() { return «A»; }
};

class B: public A
{
public:
virtual const char* getName() { return «B»; }
};

class C: public B
{
public:
virtual const char* getName() { return «C»; }
};

class D: public C
{
public:
virtual const char* getName() { return «D»; }
};

int main()
{
C c;
A &rParent = c;
std::cout << «rParent is a » << rParent.getName() << ‘\n’;

return 0;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

#include <iostream>
 

classA

{

public

virtualconstchar*getName(){return»A»;}

};

classBpublicA

{

public

virtualconstchar*getName(){return»B»;}

};

classCpublicB

{

public

virtualconstchar*getName(){return»C»;}

};

classDpublicC

{

public

virtualconstchar*getName(){return»D»;}

};

intmain()

{

Cc;

A&rParent =c;

std::cout<<«rParent is a «<<rParent.getName()<<‘\n’;

return;

}

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

Рассмотрим всё по порядку:

   Сначала создается объект класса C.

    — это ссылка класса A, которой мы указываем ссылаться на часть A объекта .

   Затем вызывается метод .

   Вызов приводит к вызову A::getName(). Однако, поскольку A::getName() является виртуальной функцией, то компилятор ищет «наиболее» дочерний метод между A и C. В этом случае — это C::getName().

Обратите внимание, компилятор не будет вызывать D::getName(), поскольку наш исходный объект был класса C, а не класса D, поэтому рассматриваются методы только между классами A и C. Результат выполнения программы:

Результат выполнения программы:

Пример виртуального метода на C#

Пример виртуального метода на C#. В примере используется ключевое слово , предоставляющее доступ к методу родительского (базового) класса A.

class Program
{
    static void Main(string[] args)
    {
        A myObj = new B();
        Console.ReadKey();
    }        
}

//Базовый класс A
public class A
{
    public virtual string a()
    {
        return "огонь";
    }
}

//Произвольный класс B наследующий класс A
class B  A
{
    public override string a()
    {
        return "вода";
    }

    public B()
    {
        //Выводим результат возвращаемый переопределенным методом
        Console.Out.WriteLine(a()); //вода
        //Выводим результат возвращаемый методом родительского класса
        Console.Out.WriteLine(base.a());    //огонь
    }
}

Реализация

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

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

Обычно компилятор создает отдельную vtable для каждого класса. После создания объекта указатель на эту vtable, называемый виртуальный табличный указатель или vpointer (также иногда называется vptr или vfptr), добавляется как скрытый член данного объекта (а зачастую как первый член). Компилятор также генерирует «скрытый» код в конструкторе каждого класса для инициализации vpointer’ов его объектов адресами соответствующей vtable.

Реализация

Техника вызова виртуальных методов называется ещё «динамическим связыванием». Имеется в виду, что имя метода, использованное в программе, связывается с адресом входа конкретного метода динамически (во время исполнения программы), а не статически (во время компиляции), так как в момент компиляции, в общем случае, невозможно определить, какая из существующих реализаций метода будет вызвана.

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

Способ определения

Одни языки программирования (например, C++, C#) требуют явно указывать, что данный метод является виртуальным. В других языках (например, Java, Python) все методы являются виртуальными по умолчанию (но только те методы, для которых это возможно; например в Java методы с доступом private не могут быть переопределены в связи с правилами видимости).

Базовый класс может и не предоставлять реализации виртуального метода, а только декларировать его существование. Такие методы без реализации называются «чистыми виртуальными» (перевод англ.  pure virtual) или абстрактными. Класс, содержащий хотя бы один такой метод, тоже будет абстрактным. Объект такого класса создать нельзя (в некоторых языках допускается, но вызов абстрактного метода приведёт к ошибке). Наследники абстрактного класса должны предоставить реализацию для всех его абстрактных методов, иначе они, в свою очередь, будут абстрактными классами. Абстрактный класс, который содержит только абстрактные методы, называется интерфейсом.

Пример виртуальной функции в Delphi

полиморфизм языка Object Pascal, использующемся в Delphi . Рассмотрим пример:

Объявим два класса. Предка (Ancestor):

 TAncestor = class
 private
 protected
 public
   {Виртуальная процедура.} 
   procedure VirtualProcedure; virtual; 
   procedure StaticProcedure;
 end;

и его потомка (Descendant):

 TDescendant = class(TAncestor)
 private
 protected
 public
    {Перекрытие виртуальной процедуры.}
   procedure VirtualProcedure; override;
   procedure StaticProcedure;
 end;

Как видно в классе предке объявлена виртуальная функция — . Чтобы воспользоваться достоинствами полиморфизма, её нужно перекрыть в потомке.

Реализация выглядит следующим образом:

 { TAncestor }
   
 procedure TAncestor.StaticProcedure;
 begin
   ShowMessage('Ancestor static procedure.');
 end;
   
 procedure TAncestor.VirtualProcedure;
 begin
   ShowMessage('Ancestor virtual procedure.');
 end;
 { TDescendant }
   
 procedure TDescendant.StaticProcedure;
 begin
   ShowMessage('Descendant static procedure.');
 end;
   
 procedure TDescendant.VirtualProcedure;
 begin
   ShowMessage('Descendant override procedure.');
 end;

Посмотрим как это работает:

 procedure TForm2.BitBtn1Click(Sender: TObject);
 var
   MyObject1: TAncestor;
   MyObject2: TAncestor;
 begin
   MyObject1 := TAncestor.Create;
   MyObject2 := TDescendant.Create;
   try
     MyObject1.StaticProcedure;
     MyObject1.VirtualProcedure;
     MyObject2.StaticProcedure;
     MyObject2.VirtualProcedure;
   finally
     MyObject1.Free;
     MyObject2.Free;
   end;
 end;

Заметьте, что в разделе мы объявили два объекта и типа . А при создании создали как , а как . Вот что мы увидим при нажатии на кнопку :

  1. Ancestor static procedure.
  2. Ancestor virtual procedure.
  3. Ancestor static procedure.
  4. Descendant override procedure.

Для все понятно, просто вызвались указанные процедуры. А вот для это не так.

Вызов привел к появлению «Ancestor static procedure.». Ведь мы объявили , поэтому и была вызвана процедура класса .

А вот вызов привел к вызову реализованной в потомке(). Это произошло потому, что был создан не как , а как :
. И виртуальный метод был перекрыт.

В Delphi полиморфизм реализован с помощью так называемой виртуальной таблицы методов (или VMT).

Достаточно часто виртуальные методы забывают перекрыть с помощью ключевого слова . Это приводит к закрытию метода. В этом случае замещения методов в VMT не произойдет и требуемая функциональность не будет получена.

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

Вызов метода предка из перекрытого метода

Бывает необходимо вызвать метод предка в перекрытом методе.

Объявим два класса. Предка(Ancestor):

 TAncestor = class
 private
 protected
 public
   {Виртуальная процедура.} 
   procedure VirtualProcedure; virtual; 
 end;

и его потомка (Descendant):

 TDescendant = class(TAncestor)
 private
 protected
 public
    {Перекрытие виртуальной процедуры.}
   procedure VirtualProcedure; override;
 end;

Обращение к методу предка реализуется с помощью ключевого слова «inherited»

 procedure TDescendant.VirtualProcedure;
 begin
     inherited;
 end;

Стоит помнить, что в Delphi деструктор должен быть обязательно перекрытым — «override» — и содержать вызов деструктора предка

TDescendant = class(TAncestor)
 private
 protected
 public
    destructor Destroy; override;
 end;
 destructor TDescendant. Destroy;
 begin
     inherited;
 end;

В языке C++ не нужно вызывать конструктор и деструктор предка, деструктор должен быть виртуальным. Деструкторы предков вызовутся автоматически.
Чтобы вызвать метод предка, нужно явно вызвать метод:

class Ancestor
{
public
  virtual void  function1 () { printf("Ancestor::function1"); }
};

class Descendant  public Ancestor
{
public
  virtual void  function1 () {
     printf("Descendant::function1");
     Ancestor::function1(); // здесь будет напечатано "Ancestor::function1"
  }
};

Для вызова конструктора предка нужно указать конструктор:

class Descendant  public Ancestor
{
public
  Descendant() Ancestor(){}
};
Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Adblock
detector