Недавно мне задали вопрос: как бы я реализовал механизм виртуальных функций на языке C?
Поначалу я понятия не имел, как это можно сделать: ведь C не является языком объектно-ориентированного программирования, и здесь нет такого понятия, как наследование. Но поскольку у меня уже было немного опыта с C, и я знал, как работают виртуальные функции, я подумал, что должен быть способ сымитировать поведение виртуальных функций, используя структуры (struct).
Краткое пояснение для тех, кто не знает, что такое виртуальные функции:
Виртуальная функция — это функция, которая может быть переопределена классом-наследником, для того чтобы тот имел свою, отличающуюся, реализацию. В языке C++ используется такой механизм, как таблица виртуальных функций
(кратко vtable) для того, чтобы поддерживать связывание на этапе выполнения программы. Виртуальная таблица — статический массив, который хранит для каждой виртуальной функции указатель на ближайшую в иерархии наследования реализацию этой функции. Ближайшая в иерархии реализация определяется во время выполнения посредством извлечения адреса функции из таблицы методов объекта.
Давайте теперь посмотрим на простой пример использования виртуальных функций в C++
class ClassA { public: ClassA() {data = 10;} virtual void set() { std::cout << "ClassA is increasing" << std::endl; data++; } int get() { set(); return data; } protected: int data; }; class ClassB : public ClassA { public: void set() { std::cout << "ClassB is decreasing" << std::endl; data--; } };
В приведенном примере у нас есть класс ClassA
, имеющий два метода: get()
и set()
. Метод get()
помечен как виртуальная функция; в классе ClassB
его реализация меняется. Целое число data
помечено ключевым словом protected
, чтобы класс-наследник ClassB
имел доступ к нему.
int main() { ClassA classA; ClassB classB; std::cout << "ClassA value: " << classA.get() << std::endl; std::cout << "ClassB value: " << classB.get() << std::endl; return 0; }
Вывод:
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 = {ClassA_constructor, ClassA_set, ClassA_get }; /* Реализации методов */ void ClassA_constructor(ClassA *this) { this->vtable = &ClassA_vtable; this->data = 10; } void ClassA_set(ClassA *this) { printf("ClassA is increasingn"); 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 = {ClassB_constructor, ClassB_set, ClassB_get}; void ClassB_constructor(ClassB *this) { /* Требуется явное приведение типов */ ClassA_constructor((ClassA*)this); /* Для таблицы виртуальных функций также требуется явное приведение типов */ this->inherited_class.vtable = (ClassA_functiontable*)&ClassB_vtable; } void ClassB_set(ClassB *this) { printf("ClassB decreasingn"); 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: %dn", classA.vtable->get(&classA)); /* Обращаемся к get() через класс-предок */ printf("ClassB value: %dn", classB.inherited_class.vtable->get((struct ClassA*)&classB)); }
Вывод:
ClassA is increasing ClassA value: 11 ClassB decreasing ClassB value: 9
Конечно же, этот трюк не выглядит настолько же естественно, как в C++ или в другом объектно-ориентированном языке программирования, и мне никогда не приходилось реализовывать нечто подобное, когда я писал программы на C, но тем не менее это может помочь лучше понять внутреннее устройство виртуальных функций.
от переводчика: подробная статья про внутреннюю реализацию виртуальных функций в C++
Автор: kharvd