Я немного изменю ваш пример, чтобы он показал более интересные аспекты объектной ориентации.
Предположим, у нас есть следующее:
#include <iostream>
struct Animal
{
int age;
Animal(int a) : age {a} {}
virtual int setAge(int);
virtual void sayHello() const;
};
int
Animal::setAge(int a)
{
int prev = this->age;
this->age = a;
return prev;
}
void
Animal::sayHello() const
{
std::cout << "Hello, I'm an " << this->age << " year old animal.\n";
}
struct Tiger : Animal
{
int stripes;
Tiger(int a, int s) : Animal {a}, stripes {s} {}
virtual void sayHello() const override;
virtual void doTigerishThing();
};
void
Tiger::sayHello() const
{
std::cout << "Hello, I'm a " << this->age << " year old tiger with "
<< this->stripes << " stripes.\n";
}
void
Tiger::doTigerishThing()
{
this->stripes += 1;
}
int
main()
{
Tiger * tp = new Tiger {7, 42};
Animal * ap = tp;
tp->sayHello(); // call overridden function via derived pointer
tp->doTigerishThing(); // call child function via derived pointer
tp->setAge(8); // call parent function via derived pointer
ap->sayHello(); // call overridden function via base pointer
}
Я игнорирую хороший совет, согласно которому классы с virtual
функциями-членами должны иметь virtual
деструктор для целей этого примера. Я все равно собираюсь протечь объект.
Давайте посмотрим, как мы можем перевести этот пример на старый добрый C, где нет функций-членов, не говоря уже о virtual
. Весь следующий код написан на C, а не на C ++.
struct animal
прост:
struct animal
{
const void * vptr;
int age;
};
В дополнение к члену age
мы добавили vptr
, который будет указателем на vtable. Я использую для этого указатель void
, потому что нам все равно придется делать уродливые преобразования, а использование void *
немного уменьшает уродство.
Затем мы можем реализовать функции-члены.
static int
animal_set_age(void * p, int a)
{
struct animal * this = (struct animal *) p;
int prev = this->age;
this->age = a;
return prev;
}
Обратите внимание на дополнительный 0-й аргумент: указатель this
, который неявно передается в C ++. Опять же, я использую указатель void *
, поскольку это упростит задачу в дальнейшем. Обратите внимание, что внутри любой функции-члена мы всегда знаем тип указателя this
статически, поэтому приведение не является проблемой. (И на машинном уровне он в любом случае ничего не делает.)
Член sayHello
определяется аналогично, за исключением того, что указатель this
на этот раз квалифицирован const
.
static void
animal_say_hello(const void * p)
{
const struct animal * this = (const struct animal *) p;
printf("Hello, I'm an %d year old animal.\n", this->age);
}
Время для животных vtable. Сначала мы должны дать ему простой тип.
struct animal_vtable_type
{
int (*setAge)(void *, int);
void (*sayHello)(const void *);
};
Затем мы создаем единственный экземпляр vtable и настраиваем его с правильными функциями-членами. Если бы Animal
имел чистый virtual
член, соответствующая запись имела бы значение NULL
и лучше не разыменовывалась.
static const struct animal_vtable_type animal_vtable = {
.setAge = animal_set_age,
.sayHello = animal_say_hello,
};
Обратите внимание, что animal_set_age
и animal_say_hello
были объявлены static
. Это возможно, потому что они никогда не будут упоминаться по именам, а только через vtable (а vtable только через vptr
, так что это также может быть static
).
Теперь мы можем реализовать конструктор для Animal
…
void
animal_ctor(void * p, int age)
{
struct animal * this = (struct animal *) p;
this->vptr = &animal_vtable;
this->age = age;
}
… И соответствующий operator new
:
void *
animal_new(int age)
{
void * p = malloc(sizeof(struct animal));
if (p != NULL)
animal_ctor(p, age);
return p;
}
Единственное, что интересно, это строка, в которой в конструкторе задано vptr
.
Перейдем к тиграм.
Tiger
наследуется от Animal
, поэтому получает struct tiger
подобъект. Я делаю это, помещая struct animal
в качестве первого члена. Важно, чтобы это был первый член, потому что это означает, что первый член этого объекта - vptr
- имеет тот же адрес, что и наш объект. Нам это понадобится позже, когда мы проведем сложный кастинг.
struct tiger
{
struct animal base;
int stripes;
};
Мы также могли бы просто скопировать члены struct animal
лексически в начале определения struct tiger
, но это может быть труднее поддерживать. Компилятор не заботится о таких стилистических проблемах.
Мы уже знаем, как реализовать функции-члены для тигров.
void
tiger_say_hello(const void * p)
{
const struct tiger * this = (const struct tiger *) p;
printf("Hello, I'm an %d year old tiger with %d stripes.\n",
this->base.age, this->stripes);
}
void
tiger_do_tigerish_thing(void * p)
{
struct tiger * this = (struct tiger *) p;
this->stripes += 1;
}
Обратите внимание, что на этот раз мы преобразуем указатель this
в struct tiger
. Если вызывается функция тигра, указатель this
должен указывать на тигра, даже если мы вызываемся через базовый указатель.
Рядом с vtable:
struct tiger_vtable_type
{
int (*setAge)(void *, int);
void (*sayHello)(const void *);
void (*doTigerishThing)(void *);
};
Обратите внимание, что первые два члена точно такие же, как для animal_vtable_type
. Это важный и, по сути, прямой ответ на ваш вопрос. Возможно, это было бы более ясно, если бы я поместил struct animal_vtable_type
в качестве первого члена. Я хочу подчеркнуть, что макет объекта был бы точно таким же, за исключением того, что в этом случае мы не могли разыграть наши неприятные трюки с приведением типов. Опять же, это аспекты языка C, которых нет на машинном уровне, поэтому компилятора это не беспокоит.
Создайте экземпляр vtable:
static const struct tiger_vtable_type tiger_vtable = {
.setAge = animal_set_age,
.sayHello = tiger_say_hello,
.doTigerishThing = tiger_do_tigerish_thing,
};
И реализуем конструктор:
void
tiger_ctor(void * p, int age, int stripes)
{
struct tiger * this = (struct tiger *) p;
animal_ctor(this, age);
this->base.vptr = &tiger_vtable;
this->stripes = stripes;
}
Первым делом конструктор тигра вызывает конструктор животного. Помните, как конструктор животных устанавливает vptr
в &animal_vtable
? Это причина того, почему вызов virtual
функций-членов из конструктора базового класса часто удивляет людей. Только после запуска конструктора базового класса мы повторно назначаем vptr
производному типу, а затем выполняем нашу собственную инициализацию.
operator new
- это просто шаблон.
void *
tiger_new(int age, int stripes)
{
void * p = malloc(sizeof(struct tiger));
if (p != NULL)
tiger_ctor(p, age, stripes);
return p;
}
Были сделаны. Но как вызвать виртуальную функцию-член? Для этого я определю вспомогательный макрос.
#define INVOKE_VIRTUAL_ARGS(STYPE, THIS, FUNC, ...) \
(*((const struct STYPE ## _vtable_type * *) (THIS)))->FUNC( THIS, __VA_ARGS__ )
Это уродливо. Он принимает статический тип STYPE
, this
указатель THIS
и имя функции-члена FUNC
, а также любые дополнительные аргументы, передаваемые в функцию.
Затем он создает имя типа vtable из статического типа. (##
- это оператор вставки токена препроцессора. Например, если STYPE
равно animal
, тогда STYPE ## _vtable_type
будет расширяться до animal_vtable_type
.)
Затем указатель THIS
приводится к указателю на указатель только что производного типа vtable. Это работает, потому что мы позаботились о том, чтобы vptr
был первым элементом в каждом объекте, чтобы у него был один и тот же адрес. Это очень важно.
Как только это будет сделано, мы можем разыменовать указатель (чтобы получить фактический vptr
), а затем запросить его член FUNC
и, наконец, вызвать его. (__VA_ARGS__
расширяется до дополнительных аргументов макроса с переменным числом аргументов.) Обратите внимание, что мы также передаем указатель THIS
как 0-й аргумент функции-члену.
Фактическая правда в том, что мне пришлось снова определить почти идентичный макрос для функций, которые не принимают аргументов, потому что препроцессор не позволяет пакету аргументов вариативного макроса быть пустым. Так и должно быть.
#define INVOKE_VIRTUAL(STYPE, THIS, FUNC) \
(*((const struct STYPE ## _vtable_type * *) (THIS)))->FUNC( THIS )
И это работает:
#include <stdio.h>
#include <stdlib.h>
/* Insert all the code from above here... */
int
main()
{
struct tiger * tp = tiger_new(7, 42);
struct animal * ap = (struct animal *) tp;
INVOKE_VIRTUAL(tiger, tp, sayHello);
INVOKE_VIRTUAL(tiger, tp, doTigerishThing);
INVOKE_VIRTUAL_ARGS(tiger, tp, setAge, 8);
INVOKE_VIRTUAL(animal, ap, sayHello);
return 0;
}
Вам может быть интересно, что происходит в
INVOKE_VIRTUAL_ARGS(tiger, tp, setAge, 8);
вызов. Что мы делаем, так это вызываем непереопределенный setAge
член Animal
на Tiger
объекте, на который имеется ссылка через указатель struct tiger
. Этот указатель сначала неявно приводится к указателю void
и как таковой передается как указатель this
на animal_set_age
. Затем эта функция преобразует его в указатель struct animal
. Это правильно? Это потому, что мы осторожно поместили struct animal
в качестве самого первого члена в struct tiger
, чтобы адрес объекта struct tiger
был таким же, как адрес для подобъекта struct animal
. Это тот же трюк (только на один уровень меньше), который мы использовали с vptr
.
person
5gon12eder
schedule
08.10.2015