2.3 Селекторы, динамическая компоновка и полиморфизм

Предыдущая  Содержание  Следующая V*D*V

Кто выполняет обмен сообщениями? new() вызывает конструктор для новой области памяти, которая в основном неинициализирована:

 

void * new (const void * _class, ...) {

    const struct Class * class = _class;

    void * p = calloc(1, class -> size);

 

    assert(p);

    * (const struct Class **) p = class;

 

    if (class -> ctor) {

        va_list ap;

 

        va_start(ap, _class);

        p = class -> ctor(p, & ap);

        va_end(ap);

    }

    return p;

}

 

Наличие указателя struct Class в начале объекта является чрезвычайно важным. Именно поэтому мы инициализируем этот указатель уже в new():

 

OOC_Selectors_Dynamic_Linkage_and_Polymorphisms

 

Описание типа class справа инициализируется во время компиляции. Объект создаётся во время выполнения и затем добавляются показанные пунктирными линиями указатели. При присвоении

 

* (const struct Class **) p = class;

 

p указывает на начало новой области памяти для данного объекта. Принудительное преобразование p трактует начало объекта как указатель на struct Class и устанавливает в качестве значения этого указателя аргумент class.

Далее, если конструктор является частью описания типа, мы вызываем его и возвращаем результат его работы как результат работы new(), то есть как новый объект. В разделе 2.6 показано, что умный конструктор может, тем не менее, принимать решение о своём собственном управлении памятью.

Обратите внимание, что только явно видимые функции, подобные new(), могут получать список с изменяемым числом параметров. Доступ к списку обеспечивается переменной ар типа va_list, которая инициализируется с помощью макроса va_start() из stdarg.h. new() может только передать весь этот список в конструктор; таким образом, .ctor объявляется с параметром va_list, а не со своим собственным списком параметров переменной длины. Так как мы могли бы позже захотеть поделиться исходными параметрами между несколькими функциями, передаём адрес ap конструктору — когда он вернётся, ap будет указывать на первый аргумент не потреблённый конструктором.

delete() предполагает, что каждый объект, то есть каждый ненулевой указатель, указывает на описание типа. Это используется для вызова деструктора, если таковой существует. Здесь self играет роль p в предыдущей картинке. Выполняем принудительное преобразование с использованием локальной переменной cp и очень осторожно идём от self к его описанию:

 

void delete (void * self) {

    const struct Class ** cp = self;

 

    if (self && * cp && (* cp) -> dtor)

        self = (* cp) -> dtor(self);

    free(self);

}

 

Деструктор тоже получает возможность заменить свой собственный указатель, который будет передан free() от delete(). Если конструктор решает смухлевать, деструктор имеет таким образом шанс исправить ситуацию, смотрите раздел 2.6. Если бы объект не пожелал быть удалённым, его деструктор вернул бы нулевой указатель.

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

 

int differ (const void * self, const void * b) {

    const struct Class * const * cp = self;

 

    assert(self && * cp && (* cp) -> differ);

    return (* cp) -> differ(self, b);

}

 

Критически важной частью является, конечно же, предположение о том, что можно найти указатель описания типа * self непосредственно под произвольным указателем self. На данный момент мы предохраняемся по крайней мере от нулевых указателей. Мы могли бы поместить в начале каждого описания типа ''магическое число", или даже сравнить * self с адресами или диапазоном адресов всех известных описаний типов, но в главе 8 мы увидим, что можно выполнить гораздо более серьёзную проверку.

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

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

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

Фактически, полиморфные функции  встроены во многие языки программирования, например, процедура write() в Pascal по-разному обрабатывает разные типы аргументов, а оператор + в C производит разный эффект при вызове для целых чисел, указателей и значений с плавающей запятой. Это явление называется перегрузкой: типы аргументов и имя оператора совместно определяют, что делает оператор; одно и тоже название оператора может использоваться с аргументами различных типов для создания различных эффектов.

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

Методы могут быть полиморфными и без динамической компоновки. В качестве примера рассмотрим функцию sizeOf(), которая возвращает размер любого объекта:

 

size_t sizeOf (const void * self) {

    const struct Class * const * cp = self;

 

    assert(self && * cp);

    return (* cp) -> size;

}

 

Все объекты содержат свой дескриптор и оттуда можно получить их размер. Обратите внимание на отличие:

 

void * s = new(String, "text");

assert(sizeof s != sizeOf(s));

 

sizeof является оператором C, который вычисляется во время компиляции и возвращает количество байтов, требуемых её аргументу. sizeOf() же наша полиморфная функция, которая возвращает число байтов объекта, на который указывает её аргумент, во время выполнения.

 

Предыдущая  Содержание  Следующая