Виртуальные функции
Виртуальные функции — специальный вид функций-членов класса. Виртуальная функция отличается об обычной функции тем, что для обычной функции связывание вызова функции с ее определением осуществляется на этапе компиляции. Для виртуальных функций это происходит во время выполнения программы.
Для объявления виртуальной функции используется ключевое слово virtual . Функция-член класса может быть объявлена как виртуальная, если
- класс, содержащий виртуальную функцию, базовый в иерархии порождения;
- реализация функции зависит от класса и будет различной в каждом порожденном классе.
Виртуальная функция — это функция, которая определяется в базовом классе, а любой порожденный класс может ее переопределить. Виртуальная функция вызывается только через указатель или ссылку на базовый класс.
Определение того, какой экземпляр виртуальной функции вызывается по выражению вызова функции, зависит от класса объекта, адресуемого указателем или ссылкой, и осуществляется во время выполнения программы. Этот механизм называется динамическим (поздним) связыванием или разрешением типов во время выполнения .
Указатель на базовый класс может указывать либо на объект базового класса, либо на объект порожденного класса. Выбор функции-члена зависит от того, на объект какого класса при выполнении программы указывает указатель, но не от типа указателя. При отсутствии члена порожденного класса по умолчанию используется виртуальная функция базового класса.
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
#include
using namespace std;
class X
protected :
int i;
public :
void seti( int c) < i = c; >
virtual void print() < cout
>;
class Y : public X // наследование
public :
void print() < cout // переопределение базовой функции
>;
int main()
X x;
X *px = &x; // Указатель на базовый класс
Y y;
x.seti(10);
y.seti(15);
px->print(); // класс X: 10
px = &y;
px->print(); // класс Y: 15
cin.get();
return 0;
>
Результат выполнения
В каждом случае выполняется различная версия функции print() . Выбор динамически зависит от объекта, на который ссылается указатель.
Если в строке 9 (см. код выше) убрать ключевое слово virtual , то результат выполнения будет уже другим, т.к. связывание функций будет происходить на этапе компиляции:
В терминологии ООП «объект посылает сообщение print и выбирает свою собственную версию соответствующего метода». Виртуальной может быть только нестатическая функция-член класса. Для порожденного класса функция автоматически становится виртуальной, поэтому ключевое слово virtual можно опустить.
Пример : выбор виртуальной функции
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
#include
using namespace std;
class figure
protected :
double x, y;
public :
figure( double a = 0, double b = 0) < x = a; y = b; >
virtual double area() < return (0); >// по умолчанию
>;
class rectangle : public figure
public :
rectangle( double a = 0, double b = 0) : figure(a, b) <>;
double area() < return (x*y); >
>;
class circle : public figure
public :
circle( double a = 0) : figure(a, 0) <>;
double area() < return (3.1415*x*x); >
>;
int main()
figure *f[2];
rectangle rect(3, 4);
circle cir(2);
double total = 0;
f[0] = ▭
f[1] = ○
total = f[1]->area();
cout total += f[0]->area();
cout cin.get();
return 0;
>
Результат выполнения
Чистая виртуальная функция
Базовый класс иерархии типа обычно содержит ряд виртуальных функций, которые обеспечивают динамическую типизацию. Часто в самом базовом классе сами виртуальные функции фиктивны и имеют пустое тело. Определенное значение им придается лишь в порожденных классах. Такие функции называются чистыми виртуальными функциями .
Чистая виртуальная функция — это метод класса, тело которого не определено.
В базовом классе такая функция записывается следующим образом:
virtual ПрототипФункции = 0;
virtual void func() = 0;
Чистая виртуальные функции используются для того, чтобы отложить решение задачи о реализации функции на более поздний срок. В терминологии ООП это называется отсроченным методом . Класс, имеющий по крайней мере одну чистую виртуальную функцию, называется абстрактным базовым классом . Для иерархии типа полезно иметь абстрактный базовый класс. Он содержит общие свойства иерархии типа, но каждый порожденный класс реализует эти свойства по-своему.
Для рассмотренного выше примера (класс Фигура) функцию вычисления площади целесообразно задать чистой виртуальной функцией, которую переопределяет каждый наследуемый класс.
Строка 9 при этом будет иметь вид:
virtual double area() = 0;
Комментариев к записи: 4
Спасибо очень, чётко и просто объяснили в отличие от других сайтов, где авторы статей, словно школьники, которых поймали, когда те курили за гаражами, уходящие от ответа на вопрос: «Зачем?».
Нормальная статья. Вирутуальная функция — это просто указатель на функцию, которую каждый класс потомок может изменить на свою. Это если бы мы писали на С
Fruit Ninja Online
Виртуальные методы — один из важнейших приёмов реализации полиморфизма . Они позволяют создавать общий код, который может работать как с объектами базового класса, так и с объектами любого его класса-наследника. При этом базовый класс определяет способ работы с объектами и любые его наследники могут предоставлять конкретную реализацию этого способа.
Сюда же можно было бы про виртуальный деструктор написать, я думаю. Статья годная, первый пример не очень удачный. С абстрактными классами можно было бы связать второй пример. У Вас там как раз описывается некая «фигура», т.е. тоже абстрактное понятие, для которого нельзя описать метод вычисления площади или чего-то там. Это был бы удачный пример, т.к. это:
virtual double area() < return (0);>// по умолчанию
не совсем верно. Формально, фигура — это множество точек и оно может не иметь площади, возвращать ноль — не правильно. Описать чисто виртуальный метод — правильно. Термин «отсроченный метод» какой-то странный. Он встречается в классических книгах?
Виртуальные функции
Полиморфизм времени исполнения обеспечивается за счет использования производных классов и виртуальных функций. Виртуальная функция — это функция, объявленная с ключевым словом virtual в базовом классе и переопределенная в одном или в нескольких производных классах. Виртуальные функции являются особыми функциями, потому что при вызове объекта производного класса с помощью указателя или ссылки на него С++ определяет во время исполнения программы, какую функцию вызвать, основываясь на типе объекта. Для разных объектов вызываются разные версии одной и той же виртуальной функции. Класс, содержащий одну или более виртуальных функций, называется полиморфным классом (polymorphic class).
Виртуальная функция объявляется в базовом классе с использованием ключевого слова virtual. Когда же она переопределяется в производном классе, повторять ключевое слово virtual нет необходимости, хотя и в случае его повторного использования ошибки не возникнет.
В качестве первого примера виртуальной функции рассмотрим следующую короткую программу:
// небольшой пример использования виртуальных функций
#include
class Base public:
virtual void who() < // определение виртуальной функции
cout >
>;
class first_d: public Base public:
void who() < // определение who() применительно к first_d
cout >
>;
class seconded: public Base public:
void who() < // определение who() применительно к second_d
cout >
>;
int main()
Base base_obj;
Base *p;
first_d first_obj;
second_d second_obj;
p = &base_obj;
p->who(); // доступ к who класса Base
p = &first_obj;
p->who(); // доступ к who класса first_d
p = &second_ob;
p->who(); // доступ к who класса second_d
return 0;
>
Программа выдаст следующий результат:
Base
First derivation
Second derivation
Проанализируем подробно эту программу, чтобы понять, как она работает.
Как можно видеть, в объекте Base функция who() объявлена как виртуальная. Это означает, что эта функция может быть переопределена в производных классах. В каждом из классов first_d и second_d функция who() переопределена. В функции main() определены три переменные. Первой является объект base_obj, имеющий тип Base. После этого объявлен указатель р на класс Base, затем объекты first_obj и second_obj, относящиеся к двум производным классам. Далее указателю р присвоен адрес объекта base_objи вызвана функция who(). Поскольку эта функция объявлена как виртуальная, то С++ определяет на этапе исполнения, какую из версий функции who() употребить, в зависимости от того, на какой объект указывает указатель р. В данном случае им является объект типа Base, поэтому исполняется версия функции who(), объявленная в классе Base. Затем указателю р присвоен адрес объекта first_obj. (Как известно, указатель на базовый класс может быть использован для любого производного класса.) После того, как функция who() была вызвана, С++ снова анализирует тип объекта, на который указывает р, для того, чтобы определить версию функции who(), которую необходимо вызвать. Поскольку р указывает на объект типа first_d, то используется соответствующая версия функции who(). Аналогично, когда указателю р присвоен адрес объекта second_obj, то используется версия функции who(), объявленная в классе second_d.
Наиболее распространенным способом вызова виртуальной функции служит использование параметра функции. Например, рассмотрим следующую модификацию предыдущей программы:
/* Здесь ссылка на базовый класс используется для доступа к виртуальной функции */
#include
class Base public:
virtual void who() < // определение виртуальной функции
cout >
>;
class first_d: public Base public:
void who () < // определение who() применительно к first_d
cout >
>;
class second_d: public Base public:
void who() < // определение who() применительно к second_d
cout >
>;
// использование в качестве параметра ссылки на базовый класс
void show_who (Base &r) r.who();
>
int main()
Base base_obj;
first_d first_obj;
second_d second_obj;
show_who (base_ob j) ; // доступ к who класса Base
show_who(first_obj); // доступ к who класса first_d
show_who(second_obj); // доступ к who класса second_d
return 0;
>
Эта программа выводит на экран те же самые данные, что и предыдущая версия. В данном примере функция show_who() имеет параметр типа ссылки на класс Base. В функции main() вызов виртуальной функции осуществляется с использованием объектов типа Base, first_d и second_d. Вызываемая версия функции who() в функции show_who() определяется типом объекта, на который ссылается параметр при вызове функции.
Ключевым моментом в использовании виртуальной функции для обеспечения полиморфизма времени исполнения служит то, что используется указатель именно на базовый класс. Полиморфизм времени исполнения достигается только при вызове виртуальной функции с использованием указателя или ссылки на базовый класс. Однако ничто не мешает вызывать виртуальные функции, как и любые другие «нормальные» функции, однако достичь полиморфизма времени исполнения на этом пути не удается.
На первый взгляд переопределение виртуальной функции в производном классе выглядит как специальная форма перегрузки функции. Но это не так, и термин перегрузка функции не применим к переопределению виртуальной функции, поскольку между ними имеются существенные различия. Во-первых, функция должна соответствовать прототипу. Как известно, при перегрузке обычной функции число и тип параметров должны быть различными. Однако при переопределении виртуальной функции интерфейс функции должен в точности соответствовать прототипу. Если же такого соответствия нет, то такая функция просто рассматривается как перегруженная и она утрачивает свои виртуальные свойства. Кроме того, если отличается только тип возвращаемого значения, то выдается сообщение об ошибке. (Функции, отличающиеся только типом возвращаемого значения, порождают неопределенность.) Другим ограничением является то, что виртуальная функция должна быть членом, а не другом класса, для которого она определена. Тем не менее виртуальная функция может быть другом другого класса. Хотя деструктор может быть виртуальным, но конструктор виртуальным быть не может.
В силу различий между перегрузкой обычных функций и переопределением виртуальных функций будем использовать для последних термин переопределение (overriding).
Если функция была объявлена как виртуальная, то она и остается таковой вне зависимости от количества уровней в иерархии классов, через которые она прошла. Например, если класс second_d получен из класса first_d, а не из класса Base, то функция who() останется виртуальной и будет вызываться корректная ее версия, как показано в следующем примере:
Если в производном классе виртуальная функция не переопределяется, то тогда используется ее версия из базового класса. Например, запустим следующую версию предыдущей программы:
#include
class Base public:
virtual void who() cout >
>;
class first_d: public Base public:
void who() cout >
>;
class second_d: public Base // who() не определяется
>;
int main()
Base base_obj;
Base *p;
first_d first_obj; ,
second_d second_obj;
p = &base_obj;
p->who(); // доступ к who класса Base
p = &first obj;
p->who(); // доступ к who класса first_d
p = &sepond_ob;
p->who(); /* доступ к who() класса Base, поскольку second_d не переопределяет */
return 0;
>
Эта программа выдаст следующий результат:
Base
First derivation
Base
Надо иметь в виду, что характеристики наследования носят иерархический характер. Чтобы проиллюстрировать это, предположим, что в предыдущем примере класс second_d порожден от класса first_d вместо класса Base. Когда функцию who() вызывают, используя указатель на объект типа second_d (в котором функция who() не определялась), то будет вызвана версия функции who(), объявленная в классе first_d, поскольку этот класс — ближайший к классу second_d. В общем случае, когда класс не переопределяет виртуальную функцию, С++ использует первое из определений, которое он находит, идя от потомков к предкам.
Вызов виртуальных функций в конструкторах и деструкторах (C++)
В разных языках программирования поведение виртуальных функций отличается, когда речь заходит о конструкторах и деструкторах. Неправильное использование виртуальных функций – это классическая ошибка при разработке на языке С++, которую мы разберём в этой статье.
Теория
Предполагаю, что читатель уже знаком с виртуальными функциями в языке C++, поэтому сразу перейду к сути. Когда в конструкторе вызывается виртуальная функция, она работает только в пределах базовых или создаваемого в данный момент классов. Конструкторы в классах-наследниках ещё не вызывались, и поэтому реализованные в них виртуальные функции не будут вызваны.
Для начала поясню это рисунком.
- От класса A наследуется класс B;
- От класса B наследуется класс C;
- Функции foo и bar являются виртуальными;
- У функции foo нет реализации в классе B.
Создадим объект класса C и рассмотрим, что произойдёт, если мы вызовем эти две функции в конструкторе класса B.
- Функция foo. Класс C ещё не создан, а в классе B нет функции foo. Поэтому будет вызвана реализация функции из класса A.
- Функция bar. Класс C ещё не создан. Поэтому вызывается функция, относящаяся к текущему классу B.
Теперь продемонстрирую то же самое кодом.
#include class A < public: A() < std::cout ; virtual void foo() < std::cout ; virtual void bar() < std::cout ; >; class B : public A < public: B() < std::cout ; void bar() < std::cout ; >; class C : public B < public: C() < std::cout ; void foo() < std::cout ; void bar() < std::cout ; >; int main()
Если скомпилировать и запустить этот код, то он распечатает:
A() B() A::foo() B::bar() C()
При вызове виртуальных методов в деструкторах всё работает точно так же.
Казалось бы, в чём проблема? Всё это описано в книжках по программированию на языке С++.
Проблема в том, что про это легко забыть! И считать, что функции foo и bar будут вызваны из крайнего наследника, т.е. из класса C.
Вопрос «Почему код работает неожиданным образом?» вновь и вновь поднимается на форумах. Пример: Calling virtual functions inside constructors.
Думаю, теперь понятно, почему в подобном коде легко допустить ошибку. Особенно легко запутаться, если довелось программировать на других языках, где поведение отличается. Рассмотрим следующую программу на языке C#:
class Program < class Base < public Base() < Test(); >protected virtual void Test() < Console.WriteLine("From base"); >> class Derived : Base < protected override void Test() < Console.WriteLine("From derived"); >> static void Main(string[] args) < var obj = new Derived(); >>
Если её запустить, то будет распечатано:
From derived
Соответствующая визуальная схема:
Вызывается функция в наследнике из конструктора базового класса!
При вызове виртуального метода из конструктора учитывается тип времени выполнения создаваемого экземпляра. Исходя из этого типа и происходит виртуальный вызов. Несмотря на то, что вызов метода происходит в конструкторе базового типа, фактический тип создаваемого экземпляра – Derived, что и определяет выбор метода. Подробнее про виртуальные методы можно почитать в спецификации.
Стоит отметить, что такое поведение тоже может быть чревато ошибками. Например, проблемы могут возникнуть, если виртуальный метод работает с членами производного типа, которые ещё не были проинициализированы в его конструкторе.
class Base < public Base() < Test(); >protected virtual void Test() < >> class Derived : Base < public String MyStr < get; set; >public Derived(String myStr) < MyStr = myStr; >protected override void Test() => Console.WriteLine($"Length of : "); >
При попытке создания экземпляра типа Derived возникнет исключение типа NullReferenceException, даже если в качестве аргумента в конструктор передаётся значение, отличное от null: new Derived(«Hello there»).
При исполнении тела конструктора типа Base будет вызвана реализация метода Test из типа Derived. Этот метод обращается к свойству MyStr, которое в текущий момент проинициализировано значением по умолчанию (null), а не параметром, переданным в конструктор (myStr).
С теорией разобрались. Теперь расскажу, почему я вообще решил написать эту статью.
Как появилась статья
Всё началось с вопроса «Scan-Build for clang-13 not showing errors» на сайте StackOverflow. Хотя вернее будет сказать, что всё началось с обсуждения статьи «О том, как мы с сочувствием смотрим на вопрос на StackOverflow, но молчим».
Можете не переходить по ссылкам. Я сейчас кратко перескажу суть истории.
Человек спросил, как с помощью статического анализа искать ошибки двух видов. Первая ошибка касается переменных типа bool, и сейчас нам не интересна. Вторая часть вопроса как раз касалась поиска вызовов виртуальных функций в конструкторе и деструкторе.
Если удалить всё, не относящееся к теме, то задача состоит в выявлении вызовов виртуальных функций в этом коде:
class M < public: virtual int GetAge()< return 0; >>; class P : public M < public: virtual int GetAge() < return 1; >P() < GetAge(); >// maybe warn ~P() < GetAge(); >// maybe warn >;
И вдруг выяснилось, что не все понимают, чем опасен такой код и почему статические анализаторы кода предупреждают о вызове виртуальных методов в конструкторах/деструкторах.
К публикации на сайте habr появились комментарии (RU) следующего вида:
Сокращенный комментарий N1. Так что компилятор прав, ошибки нет. Ошибка только в логике программиста, его пример кода всегда будет возвращать единицу в первом случае. И он мог бы даже использовать inline для того, чтобы ускорить работу и кода конструктора, и деструктора. Но компилятору это все равно не имеет значение, либо результат функции нигде не используется, функция не задействует никакие внешние аргументы — компилятор просто выкинет пример в качестве оптимизации. И это логичный правильный поступок. Как итог, ошибки просто нет.
Сокращенный комментарий N2. Про виртуальные функции вообще вашего юмора не понял. [цитата из книги про виртуальные функции]. Автор подчеркивает, что ключевое слово virtual используется только один раз. Далее в книге разъясняется, что оно наследуется. А теперь студенты ответьте мне на вопрос: «Где вы увидели проблему вызова виртуальной функции в конструкторе и деструкторе класса? Ответ дать по отдельности для каждого случая». Подразумевая, что вы оба, как неприлежные студенты, не разбираетесь в вопросе, когда вызываются конструктор и деструктор класса. И в добавок совершенно упустили тему «В каком порядке определяются объекты родительских классов при определение предка, и в каком порядке они уничтожаются».
Возможно, прочитав эти комментарии вы недоумеваете, как всё это относится к рассмотренной ранее теме. Правильно, что недоумеваете. Ответ: никак не относится.
Тот, кто оставлял комментарии, просто не догадывается, от какой проблемы хочет защититься человек, задавший вопрос на StackOverflow.
Да, стоит признать, что вопрос можно было бы сформулировать лучше. Собственно, как таковой проблемы в приведённом коде действительно нет. Пока нет. Она появится в дальнейшем, когда у классов появятся новые наследники, реализующие функцию GetAge, которые что-то делают. Если бы в примере присутствовал ещё один класс, наследующий P, то вопрос стал бы более полным.
Однако любой, кто хорошо знает язык C++, сразу понимает, какая проблема обсуждается и почему человек хочет искать вызовы функций.
Запрет на вызов виртуальных функций в конструкторах/деструкторах нашёл своё отражение и в стандартах кодирования. Например в SEI CERT C++ Coding Standard есть правило: OOP50-CPP. Do not invoke virtual functions from constructors or destructors. Это диагностическое правило реализуют многие анализаторы кода, такие как Parasoft C/C++test, Polyspace Bug Finder, PRQA QA-C++, SonarQube C/C++ Plugin. В их число входит и разрабатываемый нашей командой PVS-Studio (диагностика V1053).
А если ошибки нет?
Мы не рассмотрели ситуацию, что никакой ошибки нет. Другими словами, всё работает ровно так, как задумывалось. В этом случае можно явно указать, какие функции мы планируем вызывать:
Такой код будет однозначно правильно понят вашими коллегами. Статические анализаторы в свою очередь тоже всё поймут и промолчат.
Заключение
Цените статический анализ кода. Он поможет выявить потенциальные проблемы в коде, причём такие, о которых вы и ваши коллеги могут даже не догадываться. Несколько примеров:
- V718. The ‘Foo’ function should not be called from ‘DllMain’ function.
- V1032. Pointer is cast to a more strictly aligned pointer type.
- V1036. Potentially unsafe double-checked locking.
Работа виртуальных функций, конечно, не такое тайное знание, как примеры по ссылкам :). Однако, как показывают комментарии и вопросы на StackOverflow, эта тема заслуживает внимания и контроля. Было бы всё очевидно – не было бы этой статьи. Хорошо, что анализаторы кода способны подстраховать программиста в его работе.
Спасибо за внимание, и приходите попробовать анализатор PVS-Studio.
Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Andrey Karpov. Virtual function calls in constructors and destructors (C++).
- си++
- cpp
- программирование
- статический анализ кода
- виртуальные функции
- конструктор
Виртуальные функции в C
Недавно мне задали вопрос: как бы я реализовал механизм виртуальных функций на языке C?
Поначалу я понятия не имел, как это можно сделать: ведь C не является языком объектно-ориентированного программирования, и здесь нет такого понятия, как наследование. Но поскольку у меня уже было немного опыта с C, и я знал, как работают виртуальные функции, я подумал, что должен быть способ сымитировать поведение виртуальных функций, используя структуры (struct).
Краткое пояснение для тех, кто не знает, что такое виртуальные функции:
Виртуальная функция — это функция, которая может быть переопределена классом-наследником, для того чтобы тот имел свою, отличающуюся, реализацию. В языке C++ используется такой механизм, как таблица виртуальных функций
(кратко vtable) для того, чтобы поддерживать связывание на этапе выполнения программы. Виртуальная таблица — статический массив, который хранит для каждой виртуальной функции указатель на ближайшую в иерархии наследования реализацию этой функции. Ближайшая в иерархии реализация определяется во время выполнения посредством извлечения адреса функции из таблицы методов объекта.
Давайте теперь посмотрим на простой пример использования виртуальных функций в C++
class ClassA < public: ClassA() virtual void set() < std::cout int get() < set(); return data; >protected: int data; >; class ClassB : public ClassA < public: void set() < std::cout >;
В приведенном примере у нас есть класс ClassA , имеющий два метода: get() и set() . Метод get() помечен как виртуальная функция; в классе ClassB его реализация меняется. Целое число data помечено ключевым словом protected , чтобы класс-наследник ClassB имел доступ к нему.
int main()
ClassA is increasing ClassA value: 11 ClassB is decreasing ClassB value: 9
Теперь давайте подумаем, как реализовать концепцию виртуальных функций на C. Зная, что виртуальные функции представлены в виде указателей и хранятся в vtable, а vtable — статический массив, мы должны создать структуру, имитирующую сам класс ClassA, таблицу виртуальных функций для ClassA, а также реализацию методов ClassA.
/* Опережающее объявление структуры */ struct ClassA; /* Таблица функций, хранящая указатели на функции. */ typedef struct < void (*ClassA)(struct ClassA*); /* "конструктор" */ void (*set)(struct ClassA*); /* функция set */ int (*get)(struct ClassA*); /* функция get */ >ClassA_functiontable; typedef struct ClassA < int data; ClassA_functiontable *vtable; /* Таблица виртуальных функций ClassA */ >ClassA; /* Прототипы методов ClassA */ void ClassA_constructor(ClassA *this); void ClassA_set(ClassA *this); int ClassA_get(ClassA *this); /* Инициализация таблицы виртуальных функций ClassA */ ClassA_functiontable ClassA_vtable = ; /* Реализации методов */ void ClassA_constructor(ClassA *this) < this->vtable = &ClassA_vtable; this->data = 10; > void ClassA_set(ClassA *this) < printf("ClassA is increasing\n"); this->data++; > int ClassA_get(ClassA *this) < this->vtable->set(this); return this->data; >
В C не существует указателя this , который указывал бы на вызывающий объект. Я назвал параметр та́к, для того чтобы сымитировать использование указателя this в C++ (кроме того это похоже на то, как на самом деле работает вызов метода объекта в C++).
Как мы видим из кода, приведенного выше, реализация ClassA_get() вызывает функцию set() через указатель из vtable. Теперь посмотрим на реализацию класса-наследника:
/* Опережающее объявление структуры */ struct ClassB; /* Так же, как и в предыдущем примере, храним указатели на методы класса */ typedef struct < void (*ClassB)(struct ClassB*); void (*set)(struct ClassB*); void (*get)(struct ClassA*); >ClassB_functiontable; typedef struct ClassB < ClassA inherited_class; >ClassB; void ClassB_constructor(ClassB *this); void ClassB_set(ClassB *this); int ClassB_get(ClassB *this); ClassB_functiontable ClassB_vtable = ; void ClassB_constructor(ClassB *this) < /* Требуется явное приведение типов */ ClassA_constructor((ClassA*)this); /* Для таблицы виртуальных функций также требуется явное приведение типов */ this->inherited_class.vtable = (ClassA_functiontable*)&ClassB_vtable; > void ClassB_set(ClassB *this) < printf("ClassB decreasing\n"); this->inherited_class.data--; > int ClassB_get(ClassB *this) < this->inherited_class.vtable->set((ClassA*)this); return this->inherited_class.data; >
Как видно из кода, мы так же вызываем функцию set() из реализации get() ClassB, используя vtable, указывающую на нужную функцию set() , а также обращаемся к тому же целому числу data через «унаследованный» класс ClassA.
Вот так выглядит функция main() :
int main() < ClassA classA; ClassB classB; ClassA_constructor(&classA); ClassB_constructor(&classB); printf("ClassA value: %d\n", classA.vtable->get(&classA)); /* Обращаемся к get() через класс-предок */ printf("ClassB value: %d\n", classB.inherited_class.vtable->get((struct ClassA*)&classB)); >
ClassA is increasing ClassA value: 11 ClassB decreasing ClassB value: 9
Конечно же, этот трюк не выглядит настолько же естественно, как в C++ или в другом объектно-ориентированном языке программирования, и мне никогда не приходилось реализовывать нечто подобное, когда я писал программы на C, но тем не менее это может помочь лучше понять внутреннее устройство виртуальных функций.