Chapter 4 of "Object-Oriented Programming With ANSI-C" (Inheritance - Code Reuse and Improvement)

 

Chapter 4 Inheritance—Code Reuse and Improvement

 

4.1 A super class - point

       We'll start this chapter with a basic paint program. Here's a quick test of one of the classes we'd love to have:

#include "Point.h"

#include "new.h"

 

int main (int argc, char ** argv)

{    

    void * p;

 

       while (* ++ argv)

       {    

        switch (** argv) {

                  case 'p':

                         p = new(Point, 1, 2);

                         break;

                  default:

                         continue;

              }

              draw(p);

              move(p, 10, 20);

              draw(p);

              delete(p);

       }

       return 0;

}

For each command argument starting with the character p , we get a new drawing point, move the point somewhere, redraw, and delete. Standardized C does not contain standard functions for graphical output: however, if we insist on producing a picture, we can publish text, for which Kernighan's picture [Ker82] understands:

$ points p

"." at 1,2

"." at 11,22

Coordinates are irrelevant for testing - paraphrased from business and object-oriented parlance: "A point is a message."

What can we do with this point? new() will generate a point, and the constructor expects initial coordinates to be passed as further arguments to new() . Normally, delete() will reclaim our point and call the destructor by convention.

draw() arranges for the points to be displayed. Since we want to work with other graphics objects - so there will be a switch in the test program - for draw() we will provide a dynamic connection.

move() changes the coordinates of a point by passing it a series of parameters. If we implement every graphics object that is associated with the point it refers to, we will be able to move it by simply applying the move() method of this point. So for move() we should be able to do without dynamic linking.

 

4.2 Implementation of super class - point

       In Point.h, abstract data types are included as follows:

extern const void * Point;                    /* new(Point, x, y); */

 

void move (void * point, int dx, int dy);

We were able to reuse the new file from Chapter 2, although we removed many methods and added a draw() method to the new.h file:

void * new (const void * class, ...);

void delete (void * item);

void draw (const void * self);

The type description struct Class in new.r should be associated with the method declared in new.h:

struct Class {

       size_t size;

       void * (* ctor) (void * self, va_list * app);

       void * (* dtor) (void * self);

       void (* draw) (const void * self);

};

The selector draw() is implemented in new.c. It replaces selectors such as differ() introduced in Section 2.3, and is coded in the same style:

void draw (const void * self)

{    

    const struct Class * const * cp = self;

 

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

       (* cp) -> draw(self);

}

       After the preparatory work is completed, we will turn to the real work to write Point.c, the realization of the point. Here, object orientation helps us identify exactly what we need to do: we have to make decisions about expressions and implement constructors, destructors, the dynamically linked method draw() and the statically linked method move(), these are the basic functions. If we stick to two-dimensional, Cartesian coordinates, we choose the following explicit representation:

struct Point {

       const void * class;

       int x, y;                        /* coordinates */

};

The constructor must initialize coordinates .x and .y - now an absolute routine looks like this:

static void * Point_ctor (void * _self, va_list * app)

{    

    struct Point * self = _self;

 

       self -> x = va_arg(* app, int);

       self -> y = va_arg(* app, int);

       return self;

}

Now it turns out that we don't need a destructor, because there are no resources to reclaim before delete(). In the Point_draw() function, we print the current coordinates in a way that the image can recognize:

static void Point_draw (const void * _self)

{    

    const struct Point * self = _self;

 

       printf("\".\" at %d,%d\n", self -> x, self -> y);

}

This takes care of all dynamic linkage methods, and we can define type descriptors where a null pointer represents a non-existent destructor:

static const struct Class _Point = {

       sizeof(struct Point), Point_ctor, 0, Point_draw

};

 

const void * Point = & _Point;

move() is not dynamically linked, so we omit static so that its scope extends beyond Point.c and we don't prefix it with the class name Point :

void move (void * _self, int dx, int dy)

{    

    struct Point * self = _self;

 

       self -> x += dx, self -> y += dy;

}

       Combined with the dynamic linkage in new.c, this results in the implementation of the point in Point.c.

 

4.3 Inheritance - Rings

       A circle is just a large point: besides it needs a radius for the center coordinate. The drawing is a bit different, but moving only requires us to change the center coordinates.

       This is where we can normally prepare our text editors and select source code for reuse. We make a copy of the point implementation and change the ring to differ from the point. Struct Circle takes other additional components:

int rad;                                                                

This part is initialized in the constructor

self->rad=va_arg(*app,int);                                                

And use in Circle_draw() :

printf("circle at %d,%d rad %d\n",

self —> x, self —> y, self —> rad);

       We're a little confused in move(). The necessary actions are the same for a point and a ring: for the coordinate part we need to add the transfer parameter. However, in one case move() works with struct Point and in the other case it works with struct Circle . If move() is dynamically linked, we need to provide two different functions to do the same thing. However, there is a better way, consider layers represented by points and rings:

                                           struct Point          struct Circle

 

The picture shows that each ring starts with a point. If we assign a struct Circle by appending to the end of the struct Point , we can pass a circle to the move() function, because the initialization part of the expression looks like just a point, and the move() method expects to receive a point, and the point is only something that the move() method can change. Here's a reasonable way to ensure that the initialization part of the ring always looks like a point:

struct Circle { const struct Point _; int rad; };                                  

We start the derived struct with a copy of the base struct we want to extend. Information hiding requires that we never directly access the base structure; therefore, we use almost invisible underscores for its name and declare it const to avoid careless assignments.

       That's what simple inheritance is all about: a subclass inherits from a superclass (or base class) simply by extending the struct representing the superclass.

Since the representation of a subclass object (a ring) behaves just like the representation of a superclass object (a point). A ring can always masquerade as a point - at the initialization address of a ring's representation there is indeed a representation of a point.

       Passing a ring to move() is completely deterministic: the subclass inherits the methods of the superclass, since those methods operate only on the subclass's representation, which is identical to the superclass's representation, on which the methods were originally written. Passing a circle like passing a point means converting struct Circle* to struct Point* . We'll think of such an operation as a throw-up from the subclass to the superclass —in standardized C, it could be done using an explicit conversion operator or through an intermediate void* value.

       This is generally undesirable, however, to pass a point to a function designed for circles eg, Circle_draw(): Conversion from struct Point* to struct Circle* is only permissible if a point is originally a circle. We call such a cast from superclass to subclass cast down - this also requires an explicit cast or void* value, and it can only be used for pointers, which can be used for objects, pointers, and objects do the cast at the beginning of the subclass.

       The situation is different for dynamically linked methods such as draw(). Let's look at the previous picture again, this time fully explicit with the type descriptor as follows:

 

4.4 Connection and inheritance

move() is not dynamically linked and does not use dynamic link methods to do its work. While we can pass a pointer and ring to move() , it is indeed not a function of a peptide: move() does not treat different objects differently, it always adds parameters to coordinates, ignoring others that are attached to coordinates.

When we flip up from a ring to a point, we don't change the state of the ring, in other words, even if we treat the ring's struct Circle representation as a point's struct Point , we don't change its content. As a result, treating a circle as a point as a type descriptor still has Circle because the point doesn't change in its .class section. draw() is a selector function, that is, it will use whatever parameter is passed as itself, to process the type descriptor indicated by .class , and call the drawing method stored here.

       A subclass inherits its superclass's statically linked methods - these methods operate on the parts of the subclass object that are already present on the superclass object. A subclass can choose to support its own methods instead of its superclass's dynamically linked methods. If inherited, ie, without overriding, dynamically linked methods of a superclass act like statically linked methods and modify subclass objects of the superclass's part. If overridden, the subclass's own version of the dynamic link method has access to all representations of the subclass object, ie, for a circle, draw() will call the Circle_draw() method, which takes the radius into account when drawing the circle.

 

4.5 Static and dynamic linking

       A subclass inherits its superclass's statically linked methods and optionally inherits or overrides dynamically linked methods. Consider the following declarations for move() and draw() :

void move(void* point, int dx,int dy);

void draw(const void* self);

We cannot discover connections from these two declarations, although the implementation for move() works directly, whereas draw() is just a selector function that tracks dynamic connections at runtime. The difference is that we declare a static linkage method like move() in Point.h as part of the abstract data type interface, and we declare a dynamic linkage method like draw() in new.h with the memory management interface, because so far we have decided to implement the data selector in new.c.

       Static linking is more efficient because the C compiler can call subroutines using direct addresses, but a function such as move() cannot be overridden for subclasses. Dynamic linkage is more convenient in the expansion of indirect calls - we have made a decision on the overhead of calling selector functions such as draw() , checking parameters, positioning, and calling the correct method. We drop the check and reduce the overhead by using macro* like this:

#define draw(self)  ((*(struct Class**)self)->draw(self));                         

But macros can cause problems if their arguments have negative effects and there is no clear technique for macros to manipulate variable argument lists. Furthermore, the macro requires the declaration of the struct Class which has been available so far for the implementation of the class and not for the entire program.

       Unfortunately when we design the superclass, we also need to decide a lot of things. But the function calling method will not change, it will take a lot of text editing, and more likely in many classes, to convert a function definition from static to dynamic linking, and vice versa. Starting in Chapter 7 we'll use a simple preprocessing to simplify coding, even though concatenating transformations is extremely error-prone.

       With this suspicion, it might be better to decide that dynamic linking is better than static linking even though it is less efficient. Generic functions can provide a useful conceptual abstraction and they tend to reduce the number of function names we need to remember over the course of a project. If, after implementing all required classes, we found that the dynamically linked method was never overridden, it would be less trouble to replace its selector by its single implementation and even waste its place in struct Class than to extend the type description and correct all initializations.

      

4.6 Visibility and access functions

       We can now try to implement Circle_draw() . Information hiding based on rules like "need to know" requires us to use 3 files for each class. Circle.h contains the abstract data type interface; for a subclass it includes the superclass's interface file so that declarations like this make the inherited methods available:

#include "Point.h"

extern const void* Circle;  /*new(Circle,x,y,rad)*/

The interface file Circle.h is included by the application code and implements the class; it avoids errors caused by multiple inclusions.

       A circle's representation is declared in a second header file, Circle.r. For subclasses it contains the representation file of the superclass so that we can derive the representation of the subclass by extending the superclass:

#include "Point.r"

struct Circle{const struct Point _;int rad;};

Subclasses need a representation of the superclass to implement inheritance: struct Circle contains a const struct Point . The point is definitely not read-only - move() will change its coordinates - but the const qualifier prevents accidental overwriting of its components. Indicates that the file Circle.r is only included by the implementation of the class; still protected from multiple calls.

       Finally, the implementation of a circle for classes, for object management, is defined in the original file Circle.c containing the interface and presentation files:

#include "Circle.h"

#include "Circle.r"

#include "new.h"

#include "new.r"

      

static void Circle_draw(const void * _self)

{

       const struct Circle* self=_self;

       printf("circle at %d rad %d\n",self->_.x,self->_.y,self->rad);

}

In Circle_draw() , for the circle we read the dot part through the subclass part using the "visible name" _ . This is not a good idea from an information hiding point of view. While reading coordinate values ​​should not cause significant problems, we must never ensure that in other cases a subclass implementation does not directly cheat and modify parts of its superclass, thus playing havoc with its invariants.

       Efficiency requires that a subclass have direct access to the components of its superclass. The principles of information hiding and maintainability require that a superclass hide its representation of itself from its subclasses as well as possible. If we choose later, we should be able to provide access functions for those subclasses that are allowed to see all the components of the superclass, and provide correction functions for these components, that is, for subclasses to make changes.

       Methods for static linking when accessing and modifying functions. If we declare them in the presentation file for the superclass, the superclass is only included in the implementation of the subclass, we can use macros, and if the macro uses each parameter only for this there is no problem with side effects. As an example, in Point.r we define the following accessor macros:*

#define x(p)    (((const struct Point*)(p))->x)

#define y(p)    (((const struct Point*)(p))->y)

These macros can be applied to a pointer to any object starting with struct Point , that is, to objects of any subclass from our point. The technique is to throw our point up to the superclass and reference the part we are interested in. const Assignment of the result during throwing. if const is ignored

#define x(p)    (((struct Point*)(p))->x)                                         

A macro call to x(p) yields an l-value that can be the target of an assignment. A good modifier function is preferably a macro definition

#define set_x(p,v) (((struct Point*)(p))->x=(v))                                

This definition produces an assignment.

       For accessing and modifying functions outside the subclass implementation we only use static linking methods. We cannot resort to macros, since the internal representation of the superclass referenced by macros is invisible. Information hiding for inclusion in applications is implemented without providing the presentation file Point.r.

       The macro definition reveals, however, that once a class representation is available, information hiding can be easily defeated. Here's a better way to hide the struct Point . In the implementation of the superclass we use the normal definition:

struct Point{

       const void* class;

       int x,y;

};

For subclass implementations we provide the following opaque-looking version:

struct Point{

       const char _[sizeof(struct {const void* class; int x,y;})];

};

This struct has the same size as before, but we can neither read nor write its components because they are hidden in an anonymous internal structure. The point is that both declarations must contain the same component declarations and this is difficult to maintain without a handler.

 

4.7 Implementation of Subclasses - Ring

       Now that we're ready for some full implementations, we can choose our favorite techniques from the previous sections. Object-oriented regulations require that we need a constructor, and possibly a destructor , Circle_draw() , and the type description Circle is bound together. In order to practice our approach, we include Circle.h and add the following lines to the program in Section 4.1 for testing:

case 'c':

       p=new(Circle,1,2,3);

       break;

Now we can observe the behavior of the following test program:

$ circles p c

"." at 1,2

"." at 11,12

circle at 1,2 rad 3

circle at 11,22 rad 3

The ring constructor accepts 3 parameters: the first parameter is the coordinates of the point of the ring and the next is the radius. Initializing the point part is the job of the point's constructor. It handles the arguments that are part of the new() argument list. The ring's constructor takes the reserved parameter list from where it initialized the radius.

A subclass's constructor should first allow the superclass to do partial initialization that brings explicit memory into the superclass object. Once the superclass constructor is constructed, the subclass constructor completes the initialization and takes the superclass object into the subclass object.

For rings, that means we need to call Point_ctor() . Like all other dynamic links, this function is declared static and thus hidden inside Point.c. However, we can still get this function through the type descriptor Point available in Circle.c.

static void * Circle_ctor (void * _self, va_list * app)

{

       struct Circle * self =

       ((const struct Class *) Point) —> ctor(_self, app);

      

       self —> rad = va_arg(* app, int);

       return self;

}

It should be clear here why we pass the address of the argument app list pointer to each constructor instead of the value of va_list itself: new() calls the subclass's constructor, which calls the superclass's constructor, etc. The super constructor is the first one that will actually do something, and will pick up the leftmost argument list passed into new() . The reserved parameters are available to the next subclass, and so on until finally, the rightmost parameter is used by the final subclass, that is, by the constructor called directly by new() .

       The constructors are best organized in strict reverse order: delete() calls the subclass's destructor. It should first destroy its own resources and then call the immediate superclass's destructor, which can directly destroy the next resource set and so on. Construction happens first on the parent class before the subclass. Destruction is the opposite, the child class must precede the parent class, that is, the ring part must precede the point part. Here, however, nothing needs to be done.

       We have previously got Circle_draw() working, we used the visible part, and the encoded representation file Point.r is as follows:

struct Point {

       const void * class;

       int x, y; /* coordinates */

};

#define x(p) (((const struct Point *)(p)) -> x)

#define y(p) (((const struct Point *)(p)) -> y)

Now we can use access macros for Circle_draw() :

static void Circle_draw (const void * _self)

{

      const struct Circle * self = _self;

       printf("circle at %d,%d rad %d\n",x(self), y(self), self —> rad);

}

       move() has static linkage and is inherited from point's implementation. We conclude that the implementation of the circle is defined only globally visible as part of Circle.c:

static const struct Class _Circle = {

       sizeof(struct Circle), Circle_ctor, 0, Circle_draw

};

const void * Circle = & _Circle;                                             

However, between interface, expression, and implementation files it seems that we have a viable strategy for allocating program text implementation classes, the point and ring example does not yet reveal a problem: if a dynamically linked method such as Point_draw() is not overridden in a subclass, the type descriptor of the subclass needs to point to the function implemented in the superclass . The function name, however, is defined as static here , so the selector cannot be circumvented. We will see a clean solution to this problem in Chapter 6. As a temporary trade-off, we can avoid the use of static in this case , just declare the header of the function in the implementation file of the subclass, and use the function name to initialize the type description for the subclass.

 

4.8 Summary

       Objects of the superclass and subclasses are similar, but not identical in representation. Subclasses normally have more methods that are more fully stated - they are specific to the superclass object's version.

       We use a copy of the superclass object's representation as the start of the subclass object representation, ie, the subclass object is represented by adding its components to the end of the superclass object.

       A subclass inherits the methods of the superclass: since the beginning of a subclass object looks like the superclass object, we can throw up and see a pointer to the subclass object as a pointer to the superclass object to which we can pass superclass methods. To avoid explicit conversion, we declare all method parameters using void* as a generic pointer.

       Inheritance can be seen as a fundamental form of polymorphism mechanism: a superclass method accepts objects of different types, its own class and all subclasses named. However, because objects pretend to be superclass objects, methods only work on the superclass part of each object, and it will, therefore, not work differently on objects from different classes.

       Dynamic linking methods can be inherited from a superclass or overridden in a subclass - for subclasses it is determined by whatever function pointer is put into the type descriptor. Therefore, if a dynamically linked method is called on an object, we can always access methods belonging to the object's real class even if the pointer is thrown to some superclass. If the dynamic link method is inherited, it can only work on the superclass part of the subclass object, because it does not know the existence of the subclass. If a method is overridden, the subclass version has access to the entire object, and it can even call all methods of its associated superclass through the explicit use of the superclass's type descriptor.

       Note in particular that, for superclass representations, the constructor first calls back to the superclass's constructor up to the ultimate ancestor so that each subclass's constructor handles only its own extensions to the class. Each superclass destructor should first delete the resources of its subclasses and then call the superclass's destructor and so on up to the final ancestor. Constructors are called from ancestor to final subclass, and destructors are called in the reverse order.

       Our strategy is a little buggy: in general we shouldn't call a dynamically linked method from a constructor, because the object might not be fully initialized. new() inserts the final type descriptor into an object before the constructor is called , as there is no necessary accessor method in the same class as a constructor. The safe technique is to call the method by the internal name for the constructor in the same class, that is, for the points, we call Points_draw() instead of draw() .

       To encourage information hiding, we use a three-file implementation of the class. The interface file contains the abstract data type description, the representation file contains the structure of the object, and the implementation file contains the code for the method and initialization type description. An interface file contains the superclass interface file and is implemented and included by any application. A presentation file contains the superclass's presentation file and is included only by implementations.

       Sections of superclasses should not be referenced directly in subclasses. Instead, for each part we can both provide static link access and possible modification methods, as well as add appropriate macros to the superclass's presentation file. Function symbols make it easier to use a text editor or debugger to track down possible information leaks or violations of invariants.

 

4.9 Yes or yes? - collection of inherited pairs

       As struct Circle our representation of the ring contains the representation of the point:

struct Circle { const struct Point _; int rad; };

However, we naturally never go to the direct visitors section. Instead, when we want to inherit we drop Point from Circle and handle initialization of struct Point here.

       Here's another way to represent a ring: it can contain a point as a set. We can deal with objects only through pointers; thus the representation of such a ring looks like this:

struct Circle2 { struct Point * point; int rad; };                   

This ring is nothing like a point, that is, it cannot inherit from Point and reuse its methods. However, it can apply point methods to parts of points; it just cannot apply point methods to itself.

       If a language has an explicit notation for inheritance, the difference will be more obvious, similar notation in C++ would have the following notation:

struct Circle:Point{int rad;};  //inheritance

struct Circle2{ struct Point point;int rad}; //aggregate

In C++ as a pointer we don't have to access the object.

       Inheritance, that is, subclassing from a superclass, and collections, that is, making one part of an object a part of another object, provide very similar functionality. These applications are usually referred to as is-it-or-has-it? The test determines that: if an object of a new class is just like objects of some other class, we should use inheritance to implement the new class; if a new class has an object of another class as part of its state, we should build collections.

       As far as our point is concerned, a ring is just one big point, which is why we use inheritance to make a ring. A square is an ambiguous example: we can describe it by a reference point and the lengths of the sides, or we can use the diagonals of the endpoints or even three corners. Just a somewhat fancy point with a reference point being a square; other representations lead to collections. In our arithmetic expressions, we've used inheritance from monocular to binocular operation nodes, but this already violates the test sufficiently.

 

4.10 Multiple Inheritance

       Because we use trivial standardized C language. We can't hide the fact that inheritance means including a struct at the beginning of another struct. Using upcasting is the key to reusing superclass methods on objects of subclasses. An upthrow from a roundabout to a point is done by throwing the starting address of a structure; the value of the pointer does not change.

       If we include two or more structs in other structs, and if we're willing to do some address manipulation during throw-up, we can call the result multiple inheritance: an object can behave as if it belongs to several classes. The advantage seems to be that we don't have to carefully design the inheritance relationship - we can quickly put classes together and inherit whatever we want. The downside is, obviously, that we have to have address handling before we can reuse methods.

       Things can actually confuse us very quickly. Consider a text, a square, each with an inherited reference point. We were able to throw them all together on a button - the only question that existed was hoping that the button should inherit one or two reference points. ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――

       We have a great advantage in using the standardized C language: it will make obvious the fact that inheritance - multiple or otherwise - always comes with inclusion. Contains, however, can also be implemented as a set. It's not at all clear that multiple inheritance is going to do more for the programmer than complicate the language definition and add implementation bloat. We're going to keep things simple with weapons just doing simple inheritance. Chapter 14 will first show the use of multiple inheritance, and the integration of libraries can be achieved by collection and message conversion.

 

Guess you like

Origin blog.csdn.net/besidemyself/article/details/6587858