OOC:en:2.3 Selectors, Dynamic Linkage, and Polymorphisms
来自 ChinaUnix Wiki
Who does the messaging? The constructor is called by new() for a new memory area which is mostly uninitialized:
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;
}
The existence of the struct Class pointer at the beginning of an object is extremely important. This is why we initialize this pointer already in new(): 注意: 原文这里有一张图片。 The type description class at the right is initialized at compile time. The object is created at run time and the dashed pointers are then inserted. In the assignment
* (const struct Class **) p = class;
p points to the beginning of the new memory area for the object. We force a conversion of p which treats the beginning of the object as a pointer to a struct Class and set the argument class as the value of this pointer.
Next, if a constructor is part of the type description, we call it and return its result as the result of new(), i.e., as the new object. Section 2.6 illustrates that a clever constructor can, therefore, decide on its own memory management.
Note that only explicitly visible functions like new() can have a variable parame- ter list. The list is accessed with a va_list variable ap which is initialized using the macro va_start() from stdarg.h. new() can only pass the entire list to the construc- tor; therefore, .ctor is declared with a va_list parameter and not with its own vari- able parameter list. Since we might later want to share the original parameters among several functions, we pass the address of ap to the constructor — when it returns, ap will point to the first argument not consumed by the constructor.
delete() assumes that each object, i.e., each non-null pointer, points to a type description. This is used to call the destructor if any exists. Here, self plays the role of p in the previous picture. We force the conversion using a local variable cp and very carefully thread our way from self to its description:
void delete (void * self)
{ const struct Class ** cp = self;
if (self && * cp && (* cp) —> dtor)
self = (* cp) —> dtor(self);
free(self);
}
The destructor, too, gets a chance to substitute its own pointer to be passed to free() by delete(). If the constructor decides to cheat, the destructor thus has a chance to correct things, see section 2.6. If an object does not want to be deleted, its destructor would return a null pointer.
All other methods stored in the type description are called in a similar fashion. In each case we have a single receiving object self and we need to route the method call through its descriptor:
int differ (const void * self, const void * b)
{ const struct Class * const * cp = self;
assert(self && * cp && (* cp) —> differ);
return (* cp) —> differ(self, b);
}
The critical part is, of course, the assumption that we can find a type description pointer * self directly underneath the arbitrary pointer self. For the moment at least, we guard against null pointers. We could place a ‘‘magic number’’ at the beginning of each type description, or even compare * self to the addresses or an address range of all known type descriptions, but we will see in chapter 8 that we can do much more serious checking.
In any case, differ() illustrates why this technique of calling functions is called dynamic linkage or late binding: while we can call differ() for arbitrary objects as long as they start with an appropriate type description pointer, the function that actually does the work is determined as late as possible — only during execution of the actual call, not before.
We will call differ() a selector function. It is an example of a polymorphic func- tion, i.e., a function that can accept arguments of different types and act differently on them based on their types. Once we implement more classes which all contain .differ in their type descriptors, differ() is a generic function which can be applied to any object in these classes.
We can view selectors as methods which themselves are not dynamically linked but still behave like polymorphic functions because they let dynamically linked functions do their real work.
Polymorphic functions are actually built into many programming languages, e.g., the procedure write() in Pascal handles different argument types differently, and the operator + in C has different effects if it is called for integers, pointers, or float- ing point values. This phenomenon is called overloading: argument types and the operator name together determine what the operator does; the same operator name can be used with different argument types to produce different effects.
There is no clear distinction here: because of dynamic linkage, differ() behaves like an overloaded function, and the C compiler can make + act like a polymorphic function — at least for the built-in data types. However, the C compiler can create different return types for different uses of the operator + but the function differ() must always have the same return type independent of the types of its arguments.
Methods can be polymorphic without having dynamic linkage. As an example, consider a function sizeOf() which returns the size of any object:
size_t sizeOf (const void * self)
{ const struct Class * const * cp = self;
assert(self && * cp);
return (* cp) —> size;
}
All objects carry their descriptor and we can retrieve the size from there. Notice the difference:
void * s = new(String, "text"); assert(sizeof s != sizeOf(s));
sizeof is a C operator which is evaluated at compile time and returns the number of bytes its argument requires. sizeOf() is our polymorphic function which at run time returns the number of bytes of the object, to which the argument points.
