Welcome to Duncan White's Practical Software Development (PSD) Pages.
I'm Duncan White, an experienced and professional programmer, and have been programming for well over 30 years, mainly in C and Perl, although I know many other languages. In that time, despite my best intentions:-), I just can't help learning a thing or two about the practical matters of designing, programming, testing, debugging, running projects etc. Back in 2007, I thought I'd start writing an occasional series of articles, book reviews, more general thoughts etc, all focussing on software development without all the guff.
![]()
Simlating OOP in ANSI C, part 3
- In the first part of this article I discussed how to simulate Object Oriented Programming (OOP) in plain old ANSI C, starting with a discussion of OO terminology, an OO pseudo-code, and presenting a 3-class example system (classes Vector2, Vehicle and Cars).
- In the second part I showed how to translate a single class (Vector2, aka a 2-D point) into C, leaving inheritance for later.
- Now, in the third and final part, I'll show how to translate single inheritance into C, translate the rest of the classes, and finally summarise the technique.
Refresher on Inheritance
Thinking back on our Vehicle and Car example from the first part of this article, inheritance will require 4 things to work together:
It turns out that the first two (dynamic dispatch and the ability to redefine methods in a subclass) are different parts of the same thing, and better yet, we've already implemented this - our class structure contains a table of function pointers, each pointing at a method function, each class constructor can wire up those method slots to different method function implementations, and our OMn method call macros perform the runtime table lookup. In a moment we'll see a small example to prove that.
- The ability to redefine an existing method in a subclass, ensuring that our redefined method will be invoked whenever we have a subclass object.
- Dynamic dispatch: Methods must be virtual, i.e. every method call must be looked up, at run time, in a table associated with the object.
- The ability to add completely new methods and attributes in a subclass.
- Implement subclass compatibility, enabling safe assignment of a Car into a Vehicle variable, with all Vehicle methods working perfectly - but invoking the Car equivalents (using dynamic dispatch).
However, the last two (adding new attributes and methods and implementing subclass compatibilty) still remain to be solved, working out how to implement them this is the main goal of this part of the article.
Dynamic Dispatch and Method Redefinition without Inheritance
Before getting into full inheritance, we can demonstrate the power of dynamic dispatch (and method redefinition) by a very simple new test program that modifies an object's method table on the fly, let's call it overrideprint.c:
- First we declare a new "replacement print" method function in the test program:
// override print method for some Vector2 objects.. static void myprint( Vector2 this ) { printf( "hello from myprint" ); }- Then, in main, we create two Vector2 variables as usual:
int main( void ) { Vector2 a = new_Vector2( 10, 0 ); Vector2 b = new_Vector2( 0, 10 );- Next, we modify b's print() method slot, by assigning a different value to the corresponding field in the structure:
b->print = &myprint;Of course, this is not strict OOP, you can't normally override a single method without using inheritance; but here we're deliberately breaking OO conventions to make a point.- Finally, we print out what a and b are (using isa), and print both objects out via standard method calls:
printf( "create a and b, then override b->print\n" ); printf( "a isa %s. b isa %s\n", a->isa, b->isa ); printf( "a=" ); OM0(a,print); printf( ", b=" ); OM0(b,print); printf( "\n" );- Then dispose our objects and return:
OM0(b,dispose); OM0(a,dispose); return 0; }- You can download and compile the 02.oo-override tarball containing overrideprint.c (and all the vector2 materials unchanged from the previous tarball). Unpack it, enter the
02.oo-vector2
directory, typemake
andoverrideprint
will be ready for you to run. Run it via./overrideprint
and you'll see it print:create a and b, then override b->print a isa Vector2. b isa Vector2 a=[10,0], b=hello from myprint- This shows that both a and b are still Vector2s - we haven't created a full blown subclass (after all, we don't yet know how to do that) - but they print out very differently. What's going on?
- Each print() method call uses dynamic dispatch to pick "this object's" specific print function, so "a" prints normally using the Vector2 print() method function, whereas b prints using our myprint() function, which prints "hello from myprint".
- Dynamic dispatch is clearly the first small step on the road to inheritance.
The Simplest form of Inheritance: Method Redefinition
- The first, and simplest to implement, form of inheritance is where the only changes are that one or more existing methods are redefined (but no methods or attributes are added, we'll cover that later).
- As an example, let's take our on-the-fly method overriding example (overrideprint.c) above, and turn it into a proper subclass. However, rather than print "hello from myprint" which is useless (and confusing when you have several objects of that class), let's make a verbose vector2 or vv subclass, with a redefined print() method that prints the magnitude as well as x and y:
class VV subclassof Vector2 { void method print(); } implementation class VV; void method print() { printf( "[%.3f,%.3f,magnitude %g]", x, y, magnitude() ); }We could even make the print() method display what class the object is via isa, but that would surely be better placed in the parent (ancestral base) class.- Personal aside: I dislike subclassing so much, that almost every example I come up with (certainly this one) makes me immediately think - why not build this functionality into the parent class (perhaps two print methods, or a verbose/terse mode attribute) and avoid subclassing entirely. Perhaps I'm just bad at examples - it's certainly hard to justify a VV as a separate kind of entity from a Vector2. Still, it's an example to help us on the way to understanding how to implement inheritance in C.
- When we consider implementing our VV class, bear in mind that as well as the explicit redefinition of print() we'll need to redefine the isa attribute as "VV" not "Vector2". Thus, every single act of subclassing - even a null subclass where nothing is changed - involves altering the value of the isa attribute.
- Considering vv.h, our definition class translation, because we're only redefining methods, we can simply reuse struct Vector. All we will need is a new pointer type VV, a simple type alias of Vector2 (the pointer-to-struct-Vector2 type, remember):
typedef Vector2 VV; // shape unchanged: alias typeIf we prefer, we could remind ourselves what Vector2 was:typedef struct Vector2 *VV; // shape unchanged: equivalent type(Both of the above forms are equivalent, so we'll pick the first.)- We will also need constructor, allocator and initializer, so vv.h needs to include these prototypes:
extern VV new_VV( double x, double y ); // constructor extern VV alloc_VV( void ); // allocator extern void init_VV( Vector2 v, double x, double y ); // initialiser- Amazingly, apart from some comments, that's all we need to put into vv.h
/* vv.h: header for C equivalent of a "verbose vector2", a VV class VV subclassof Vector2 { void method print(); // redefined } */ typedef Vector2 VV; // shape unchanged: alias type extern VV new_VV( double x, double y ); // constructor extern VV alloc_VV( void ); // allocator extern void init_VV( Vector2 v, double x, double y ); // initialiserOne detail to note: clearly vv.h only makes sense in a context where Vector2 has already been defined (i.e. where vector2.h has already been included). So any piece of C code wanting to use a VV must:#include "vector2.h" #include "vv.h"However, many C programmers love to embed #include's into header files, so you might consider adding:#include "vector2.h"into vv.h, as in:/* vv.h: header for C equivalent of a "verbose vector2", a VV */ #include "vector2.h" // add this? typedef Vector2 VV; // shape unchanged: alias type extern VV new_VV( double x, double y ); // constructor extern VV alloc_VV( void ); // allocator extern void init_VV( Vector2 v, double x, double y ); // initialiserThis is superficially appealing, as a user now only has to write:#include "vv.h"to embody the intention I want to use class VV here - I don't know or care that VV is a subclass of Vector2..However, one has to be incredibly careful to avoid including the same header file multiple times, which generates pages of variable/function redefined errors. There is a way round this, the old include guard trick - see https://en.wikipedia.org/wiki/Include_guard for more details. In short, an include guard wraps the contents of each header file (eg vector2.h) inside:
#ifndef VECTOR2_INCLUDED #define VECTOR2_INCLUDED [contents of vector2.h] #endifSimilarly, vv.h would become:/* vv.h: header for C equivalent of a "verbose vector2", a VV ... */ #ifndef VV_INCLUDED #define VV_INCLUDED #include "vector2.h" typedef Vector2 VV; // shape unchanged: alias type extern VV new_VV( double x, double y ); // constructor extern VV alloc_VV( void ); // allocator extern void init_VV( Vector2 v, double x, double y ); // initialiser #endifUse that trick if you like, it's robust and widely used. Personally I prefer to know explicitly in each C module using module X that X uses definitions from modules A and B:#include "A.h" #include "B.h" #include "X.h"Aside: This has other advantages, such as explicitly preventing include loops (avoiding mutual dependency situations), and also making it much easier to write programs to automatically generate Makefile compilation rules - using the rule: if module.c includes A.h, B.h and C.h then the Makefile rule is:module.o: module.c A.h B.h C.h- Now, returning from to-include-or-not-to-include in header files, we've finished vv.h. Next, let's consider vv.c - it's very straightforward. We start by including what we need:
#include "vector2.h" #include "vv.h"- Then we define private (static) functions which implement the redefined method(s):
static void print( VV this ) { printf( "[%.3f,%.3f,magnitude %g]", this->x, this->y, OM0(this,magnitude) ); }Note that inside these methods, we continue to access parental attributes via this->x (etc). When we want to call other methods in our object (as in the call to magnitude() above), note that we're using our OM0 helper macro to invoke methods via dynamic dispatch, just as we've seen in the test programs.The alert reader will remember that, back in part 2 (when discussing Vector2), we talked about intra-class method calls, and used plain function calls - passing "this" explicitly. Only one Vector2 method happened to call other methods:
static void normalize( Vector2 this ) { double m = magnitude(this); // m = magnitude() if( m != 0.0 && m != 1.0 ) { scale( this, 1.0/m ); // scale(1.0/m) } }We said that this didn't work when inheritance got involved, and was a bad idea anyway. In fact, even back in Vector2, it would have been better to use proper (dynamic dispatch, aka virtual) method calls:static void normalize( Vector2 this ) { double m = OM0(this,magnitude); // m = magnitude() if( m != 0.0 && m != 1.0 ) { OM1(this, scale, 1.0/m); // scale(1.0/m) } }I'll leave you all to think of an inheritance and method redefinition scenario where the two implementations of normalize() behave differently. Drop me an email if you want a solution to that:-)
- Having built our (redefined) method functions, we implement our allocator, initializer and constructor and wire everything up:
VV alloc_VV( void ) { VV v = alloc_Vector2(); // allocator chaining return v; } void init_VV( Vector2 v, double x, double y ) { init_Vector2( v, x, y ); // initializer chaining v->print = &print; // now override methods v->isa = "VV"; // and attributes } VV new_VV( double x, double y ) { VV v = alloc_VV(); init_VV( v, x, y ); return v; }- Let's unpick that a little: we must allocate exactly one memory chunk, so alloc_VV() calls alloc_Vector2() to create our object:
VV v = alloc_Vector2(); // allocator chaining return v;You may feel worried here, because we are implicitly typecasting from alloc_Vector2()'s return type (Vector2) to a VV. However, as they are type aliases of each other, this is perfectly safe. If you prefer to make the typecast explicit, write:VV v = (VV)alloc_Vector2(); // allocator chaining- Our init_VV() initializer avoids Repeating Ourselves by using init_Vector2() to do the basic initializations (initializer chaining):
init_Vector2( v, x, y ); // initializer chainingThen it overrides a method and an attribute by writing new values into some fields:v->print = &print; // now override methods v->isa = "VV"; // and attributes- The new_VV() constructor simply glues the allocator and the initializer together:
VV v = alloc_VV(); init_VV( v, x, y ); return v;- As they're so short, let's show the complete final versions of vv.h and vv.c:
/* vv.h: header for C equivalent of a "verbose vector2", a VV: class VV subclassof Vector2 { void method print(); // redefined } */ typedef Vector2 VV; // shape unchanged: alias type extern VV new_VV( double x, double y ); // constructor extern VV alloc_VV( void ); // allocator extern void init_VV( Vector2 v, double x, double y ); // initialiser
/* vv.c: C equivalent of VV subclass: implementation class VV; void method print() { printf( "[%.3f,%.3f,magnitude %g]", x, y, magnitude() ); } */ #include <stdio.h> #include <stdlib.h> #include "vector2.h" #include "vv.h" static void print( VV this ) { printf( "[%.3f,%.3f,magnitude %g]", this->x, this->y, OM0(this,magnitude) ); } VV alloc_VV( void ) { VV v = alloc_Vector2(); // allocator chaining return v; } void init_VV( Vector2 v, double x, double y ) { init_Vector2( v, x, y ); // initializer chaining v->print = &print; // now override methods v->isa = "VV"; // and attributes } VV new_VV( double x, double y ) { VV v = alloc_VV(); init_VV( v, x, y ); return v; }- I've added a couple of new test programs (testvv and simpletestvv) to exercise Vector2s and VVs and their interactions, and verified they all work. Read simpletestvv.c yourself to understand it in detail, but in summary it creates 3 objects (and then plays with them):
Vector2 origin = new_Vector2( 0, 0 ); // Vector2 in Vector2 Vector2 a = new_VV( 10, 0 ); // VV in Vector2 VV b = new_VV( 0, 10 ); // VV in VV- Note that "a" is a VV object stored in a Vector2 variable - but it remembers that it's a VV. The assignment shows subclass compatibility - a crucial part of OO subtype polymorphism.
- When the test program later prints out "a":
OM0( a, print );according to dynamic dispatch we want "a" to print as a VV, because the object stored in a is-a VV. This works in our C translation because the OM0() macro looks up the print value from the structure, this is &VV's print, and invokes it.You can download and compile the 03.oo-vv tarball containing vv.h, vv.c, testvv.c and simpletestvv.c, an updated Makefile (and all the vector2 materials unchanged from part 2). Unpack it, enter the 03.oo-vv-vector2-subclass directory, type
make
, run./simpletestvv
and you'll see it print:origin isa Vector2 stored in a Vector2, value [0,0] a isa VV stored in a Vector2, value [10,0,magnitude 10] b isa VV stored in a VV, value [0,10,magnitude 10] magnitude of [0,0] is 0 distance from [0,0] to [10,0,magnitude 10] is 10 magnitude of [10,0,magnitude 10] is 10 distance from [10,0,magnitude 10] to itself is 0 distance from [10,0,magnitude 10] to [0,10,magnitude 10] is 14.1421 normalizing a: [10,0,magnitude 10] gives [1,0,magnitude 1] with magnitude 1 scaling a: [1,0,magnitude 1] by 5 gives [5,0,magnitude 5] with magnitude 5 adding b: [0,10,magnitude 10] to a: [5,0,magnitude 5] gives [5,10,magnitude 11.1803] with magnitude 11.1803From Vectors to Vehicles
- Before we consider more general inheritance, I think we've come to a natural end (or perhaps well beyond the end:-)) of our Vector2 and VV mini-example, and should switch to our main Vehicle and Car example (described in part 1). Let's refresh our memory of the Vehicle class (in our OO pseudo-code as usual):
class Vehicle { string name; // each vehicle has a name for clarity Vector2 pos; // where this vehicle is at present Vector2 dir; // what direction this vehicle is going double speed default 0; // speed of this vehicle in metres per second int people_cur default 1; // how many people the vehicle is carrying int people_max default 4; // how many people the vehicle can carry void method move(double T); // move in the current direction for T seconds void method print(); // display this vehicle }- With the implementation part being:
implementation class Vehicle; void method move(double T) // move in the current direction for T seconds { dir.normalize unless dir.magnitude == 1; Vector2 delta = new Vector2( dir.x, dir.y ); delta.scale( T * speed ); pos.add( delta ); } void method print() // display this vehicle { printf( "Vehicle( " ); printf( "%s: pos:%OBJ, dir:%OBJ", name, pos, dir ); printf( ", speed:%.3f, people: %d (max:%d)", speed, people_cur, people_max ); printf( " )" ); }- Let's have a go at implementing the Vehicle class. As this is a base class, we only need the part 2 techniques we used to translate class Vector2. Our header file vehicle.h is a straight translation of the class declaration using those techniques:
- typedef a struct CLASS * type called CLASS.
- typedef a pointer-to-function type for each METHOD, called CLASS_METHOD, taking "this" and any method parameters, and returning the method return type
- define the struct CLASS with every attribute and method field
- finally, define constructor, allocator and initializer prototypes.
typedef struct Vehicle *Vehicle; // all objects are pointers // foreach method, declare a type definition to make life easier // note that every method, and hence method signature, takes "this" first. typedef void (*Vehicle_move )( Vehicle this, double t ); typedef void (*Vehicle_print )( Vehicle this ); typedef void (*Vehicle_dispose )( Vehicle this ); struct Vehicle { char * isa; // attributes char * name; Vector2 pos; Vector2 dir; double speed; int people_cur; int people_max; Vehicle_move move; // methods Vehicle_print print; Vehicle_dispose dispose; // deconstructor }; extern Vehicle new_Vehicle( char * name, Vector2 pos, Vector2 dir, double speed, int people_cur, int people_max ); extern Vehicle alloc_Vehicle( void ); extern void init_Vehicle( Vehicle v, char * name, Vector2 pos, Vector2 dir, double speed, int people_cur, int people_max );The only thing to note here is that we can't directly implement the key/value "named parameters" new_Vehicle took in our examples, so we've written a standard list of parameters in declaration order. This is fine, we are translating our OO pseudo-code into C after all. Let's decide that NULL pointers or -1 numeric values in the parameter list mean: use the default value for that field if one was defined.- Similarly, when we implement vehicle.c we:
- include standard libraries that we need, plus vehicle.h and anything it requires (vector2.h)
- define a static method function for each method, using OMn method macros for any method calls, whether on "this" (intra-class) or other objects (extra-class)
- define an allocator malloc()ing the struct Vehicle pointer, an initializer setting all the attributes (using the default values if NULL or -1 values are passed in), and setting all the methods, and a constructor calling both:
#include <stdio.h> #include <stdlib.h> #include <assert.h> #include "vector2.h" #include "vehicle.h" // methods static void move( Vehicle this, double t ) { if( OM0(this->dir,magnitude) != 1 ) { OM0( this->dir, normalize); } Vector2 delta = new_Vector2( this->dir->x, this->dir->y ); OM1( delta, scale, t * this->speed ); OM1( this->pos, add, delta ); OM0( delta, dispose ); // essential not to leak memory! } static void print( Vehicle this ) { printf( "vehicle( " ); printf( "%s: pos:", this->name ); OM0( this->pos, print ); printf( ", dir:" ); OM0( this->dir, print ); printf( ", speed:%.3f, people: %d (max:%d)", this->speed, this->people_cur, this->people_max ); printf( " )" ); } static void dispose( Vehicle this ) { // should this dispose pos and dir? not clear. yes for now. if( this->pos != NULL ) OM0( this->pos, dispose ); if( this->dir != NULL ) OM0( this->dir, dispose ); free( this ); } Vehicle alloc_Vehicle( void ) { Vehicle v = (Vehicle) malloc( sizeof(struct Vehicle) ); assert( v != NULL ); return v; } void init_Vehicle( Vehicle v, char * name, Vector2 pos, Vector2 dir, double speed, int people_cur, int people_max ) { v->isa = "Vehicle"; v->name = name; v->pos = pos == NULL ? new_Vector2(-1,-1) : pos; v->dir = dir == NULL ? new_Vector2(-1,-1) : dir; v->speed = speed==-1 ? 0 : speed; v->people_max = people_max==-1 ? 4 : people_max; v->people_cur = people_cur==-1 ? 1 : people_cur; v->move = &move; v->print = &print; v->dispose = &dispose; } Vehicle new_Vehicle( char * name, Vector2 pos, Vector2 dir, double speed, int people_cur, int people_max ) { Vehicle v = alloc_Vehicle(); init_Vehicle( v, name, pos, dir, speed, people_cur, people_max ); return v; }Note that inside the move() method we create a temporary Vector2 object (delta), we must not forget to dispose of it!Note that the dispose() method must free the Vehicle object block, but we have an open question over whether it should free contained objects passed into the constructor (dir and pos). We'll discuss the options further below.
- Now that we have our C vehicle implementation, we'll need a test program. Let's move a single Vehicle around. Here's simpletestvehicle.c:
#include <stdio.h> #include <stdlib.h> #include "vector2.h" #include "vehicle.h" Vector2 startingpoint; // // d = movefor( v, dx, dy, dirname, speed, t ); // move in current direction for t seconds, printing out // useful after message; return distance from starting point // static double movefor( Vehicle v, double dx, double dy, char *dirname, double speed, double t ) { v->dir->x = dx; v->dir->y = dy; v->speed = speed; // let v move for t seconds in direction (dx,dy) at the given speed OM1( v, move, t ); // where is the vehicle now? printf( "\nafter %g seconds moving %s at %g m/s, %s now: ", t, dirname, speed, v->name ); OM0(v,print); printf( "\n" ); // how far has it travelled? // d = v.pos.distance_to(startingpoint) double d = OM1(v->pos,distance_to,startingpoint); printf( "%s is now %g metres away from startingpoint\n", v->name, d ); return d; } int main( void ) { startingpoint = new_Vector2( 0, 0 ); Vector2 pos = new_Vector2( 0, 0 ); Vector2 dir = new_Vector2( 0, 0 ); Vehicle v1 = new_Vehicle( "v1", // name pos, // pos: origin dir, // dir: standstill 3, // speed 1, // people_cur (default) 6 // people_max ); printf( "%s initially: ", v1->name ); OM0( v1, print ); printf( "\n" ); // try moving when you're stationary double d = movefor( v1, 0, 0, "stationary", 3, 2 ); // now make v1 move north for 3 seconds d = movefor( v1, 0, 1, "north", 3, 2 ); // now make v1 move east at 2m/s for 5 seconds d = movefor( v1, 1, 0, "east", 2, 5 ); // move back to starting point at 1m/s // - compute unit vector back to starting point Vector2 dirback = new_Vector2( -v1->pos->x, -v1->pos->y ); OM0( dirback, normalize ); // - move in that direction at 1m/s for d seconds movefor( v1, dirback->x, dirback->y, "back", 1, d ); OM0( dirback, dispose ); // free dirback OM0( v1, dispose ); // disposes dir and pos OM0( startingpoint, dispose ); // but startingpoint needs freeing exit(0); return 0; }- You can download and compile the 04.oo-vehicle tarball containing vehicle.h, vehicle.c, the above simpletestvv.c, an updated Makefile (and all the vector2 and vv materials unchanged from part 2). As usual, unpack it, enter the 04-oo-vehicle directory, type
make
andsimpletestvehicle
will be ready for you to run. Run it via./simpletestvehicle
and you'll see it print:v1 initially: vehicle( v1: pos:[0.000,0.000], dir:[0.000,0.000], speed:3, people: 1 (max:6) ) after 2.000 seconds moving stationary at 3 m/s: vehicle( v1: pos:[0.000,0.000], dir:[0.000,0.000], speed:3, people: 1 (max:6) ) v1 is now 0.000 metres away from startingpoint after 2.000 seconds moving north at 3 m/s: vehicle( v1: pos:[0.000,6.000], dir:[0.000,1.000], speed:3, people: 1 (max:6) ) v1 is now 6.000 metres away from startingpoint after 5.000 seconds moving east at 2 m/s: vehicle( v1: pos:[10.000,6.000], dir:[1.000,0.000], speed:2, people: 1 (max:6) ) v1 is now 11.662 metres away from startingpoint after 11.662 seconds moving back at 1 m/s: vehicle( v1: pos:[-0.000,-0.000], dir:[-0.857,-0.514], speed:1, people: 1 (max:6) ) v1 is now 0.000 metres away from startingpointNote that the final position of the Vehicle is -0.000, -0.000 - this is a symptom of floating point inexactitude!- We said above that we would finish discussing how the constructor and dispose() method must handle allocation and freeing memory, let's do that now. Remember that in C our goal should always be to free() every dynamically allocated block exactly once. There are 3 ways of implementing this here:
Which option should we choose? I've implemented the first option, but with hindsight I don't like it much. The third option simplifies the constructor call slightly but deprives us of the ability to pass (say) a VV as pos or dir instead of a Vector2:
- (As shown above) create new_Vector2 objects for dir and pos and pass them into every new_Vehicle() constructor call, and then dispose() both dir and pos in Vehicle's dispose() method. In this case, we must be careful to create separate Vector2 objects for each parameter, so that each one is disposed (free()d) exactly once. In the above example, there are three new_Vector2(0,0) objects. You must resist the temptation to create one (startingpoint) and reuse it for the dir and pos parameters - this would mean that changing any one of the Vector2's would change the other two, which would completely break the program. At the end, that Vector2 object would then be free()d three times!
- Create and pass Vector2 dir and pos objects into every new_Vehicle() call as before, but explicitly make the dispose() responsibility the caller's.
- A third way that sidesteps the problem would be to pass two x,y pairs into the new_Vehicle() constructor instead of the two Vector2s. Then the constructor would build the dir and pos Vector2s internally, so the responsibility of dispose()ing of them would unambigiously fall to the Vehicle2 dispose() method.
Vehicle v1 = new_Vehicle( "v1", // name 0, 0, // pos: origin (default) 0, 0, // dir: standstill (default) 3, // speed 6, // people_max 1 // people_cur (default) );I think I prefer the second option as it's the cleanest - the function that creates the objects should be responsible for freeing them.Feel free to explore all three options, modify vehicle.c and the test program as appropriate. It's especially instructive to try the experiment - using my code as written - of passing startingpoint into the constructor as both dir and pos, see how it breaks the program's behaviour and then see what happens when it triple frees the single Vector2 object at the end. Users of recent gcc compilers should find this generates an error message and dies, many other C compiler users will see a segmentation fault. If the program still runs to completion without any error, despite triple free()ing a block, that's quite worrying and tells you to use a memory checking technique like valgrind.
More Complex Inheritance: New attributes and/or methods
- Ok, now we've got base classes Vector2s and Vehicles under our belt, and have seen pure-method-reimplementation inheritance at work in Verbose Vectors, let's consider how to implement more general inheritance, i.e. where we add attributes and/or methods. We haven't yet translated our Car class into C, that is complex enough to show off all the principles of general inheritance. Refreshing our memories the pseudo-code for Car was:
class Car subclassof Vehicle { int wheels default 4; // only cars have wheels(?) double fuel_cur default 0; // in litres double fuel_max; // in litres void method addfuel( double fuel ); // add some fuel to the fuel tank. } implementation class Car; void method addfuel( double fuel ) // add fuel litres to the fuel tank. { assert fuel >= 0; assert fuel_cur >= 0 && fuel_cur <= fuel_max; fuel_cur += fuel; fuel_cur = fuel_max if fuel_cur > fuel_max; } void method print() // redefine how to display this car { printf( "Car( " ); printf( "%s: pos:%OBJ, dir:%OBJ", name, pos, dir ); printf( ", speed:%.3f, people: %d (max:%d)", speed, people_cur, people_max ); printf( ", wheels:%d, fuel:%.3f (max %.3f)", wheels, fuel_cur, fuel_max ); printf( " )" ); }- Now, clearly, when we consider translating this into C, these extra attributes and methods aren't going to fit into struct Vehicle. So we're going to need to define a
struct Car type, a newCar pointer, and a new set of function pointer typedefs. Our newstruct Car type must contain all the Vehicle fields and the extra Car-specific fields we'll need. Our first attempt might effectively copy all the Vehicle fields in and thus give a broken car.h reading something like:typedef struct Car *Car; // foreach new method, declare a type definition to make life easier // note that every method, and hence method signature, takes "this" first. typedef void (*Car_addfuel )( Car this, double t ); struct Car { char * isa; // copy Vehicle attributes in char * name; Vector2 pos; Vector2 dir; double speed; int people_max; int people_cur; int wheels; // new Car attributes double fuel_cur; double fuel_max; Vehicle_move move; // copy Vehicle methods in Vehicle_print print; Vehicle_dispose dispose; Car_addfuel addfuel; // new Car methods }; extern Car new_Car( char *name, Vector2 pos, Vector2 dir, double speed, int people_cur, int people_max, int wheels, double fuel_cur, double fuel_max ); extern Car alloc_Car( void ); extern void init_Car( Car v, char * name, Vector2 pos, Vector2 dir, double speed, int people_cur, int people_max, int wheels, double fuel_cur, double fuel_max );- While looking superficially reasonable, there are two major problems with this:
- All Vehicle's method fields that we've copied in are pointers to functions taking a Vehicle as their first parameter. But we'll want to invoke them with a Car as their first parameter instead.
- How can we achieve subclass compatibility between a Vehicle and a Car, the structures are laid out in memory completely incompatibly.
- To solve the first problem, we'll need to define our own set of Car-specific method pointer-to-function types (for all inherited and new methods), and use our Car_METHOD types in the struct:
typedef struct Car *Car; // foreach method (inherited or new), declare a method type definition // note that every method, and hence method signature, takes "this" first. typedef void (*Car_move )( Car this, double t ); typedef void (*Car_print )( Car this ); typedef void (*Car_dispose )( Car this ); typedef void (*Car_addfuel )( Car this, double t ); struct Car { char * isa; // copy Vehicle attributes in char * name; ... int wheels; // new Car attributes ... Car_move move; // copy Vehicle methods in Car_print print; Car_dispose dispose; Car_addfuel addfuel; // new Car methods }; extern Car new_Car( char *name, Vector2 pos, Vector2 dir, double speed, int people_cur, int people_max, int wheels, double fuel_cur, double fuel_max ); extern Car alloc_Car( void ); extern void init_Car( Car v, char * name, Vector2 pos, Vector2 dir, double speed, int people_cur, int people_max, int wheels, double fuel_cur, double fuel_max );- To solve the second problem (achieving subclass compatibility) we must simply
reorder the fields in struct Car so that all the Vehicle fields come first, in exactly their original order, followed by the new Car fields, giving our proper car.h:/* car.h: header for C equivalent of: class Car subclassof Vehicle { int wheels default 4; // only cars have wheels(?) double fuel_max; // in litres double fuel_cur default 0; // in litres void method addfuel( double L ); // add L litres to the fuel tank. } */ typedef struct Car *Car; // all objects are pointers // foreach inherited or new method, declare a method type definition // note that every method, and hence method signature, takes "this" first. // many of these are inherited from Vehicle, but called Car_METHOD and // taking "Car this" not "Vehicle this" typedef void (*Car_move )( Car this, double t ); typedef void (*Car_print )( Car this ); typedef void (*Car_dispose )( Car this ); typedef void (*Car_addfuel )( Car this, double t ); struct Car { // FIRST, Vehicle fields in same order, with Car_METHOD typenames char * isa; // attributes char * name; Vector2 pos; Vector2 dir; double speed; int people_cur; int people_max; Car_move move; // methods Car_print print; Car_dispose dispose; // deconstructor // SECOND, new Car fields int wheels; // only cars have wheels(?) double fuel_cur; // in litres double fuel_max; // in litres Car_addfuel addfuel; // add some fuel to the fuel tank. }; extern Car new_Car( char *name, Vector2 pos, Vector2 dir, double speed, int people_cur, int people_max, int wheels, double fuel_cur, double fuel_max ); extern void init_Car( Car c, char * name, Vector2 pos, Vector2 dir, double speed, int people_cur, int people_max, int wheels, double fuel_cur, double fuel_max ); extern Car alloc_Car( void );- Why does changing the field order implement subclass compatibility? Because of the way that all C compilers lay out structures in memory! This rests on 4 observations:
- First, all pointers are the same size (sizeof(void *)). That size varies between architectures and compilers, but a single compiler running on a single architecture will pick a single size for all pointers. Usually this corresponds directly to the width of the address bus, and these days will typically either be 32 bits (4 bytes) or 64 bits (8 bytes).
- Objects are pointers, hence are all the same size. Thus passing a Vehicle or a Car into methods will at least not corrupt the pointer by widening or narrowing it.
- All method fields are also pointers (to functions). No matter what the function signature, i.e. combination of parameters and return type), at runtime they are also pointers too. Thus all method fields will be the same size.
- All C compilers must lay fields in a structure out in linear order, using increasing offsets from the first field to the last. They will not sneakily reorder fields "to help" fit the fields into less memory. As a C compiler lays the fields out, it will enforce architecture-specific padding constraints to respect the underlying computer architecture's alignment rules. For example, 8-byte (64-bit) quantities like pointers, doubles and longs (on many architectures) may need to be aligned to an 8-byte boundary (i.e. their addresses must always be divisible by 8). Similarly, 4-byte (32-bit) quantities (eg. ints) may need 4-byte alignment. Eric Raymond has recently written a guide called The Lost Art Of Structure Packing which describes how C compilers (portably) lay out fields in structures, how they are not allowed to reorder fields, all about padding etc. (Thanks to the brilliantly named Latency McLaughlin of LinkedIn for this URL).
- Together, these observations guarantee that any two structures with a common prefix of fields (i.e. where the first N fields have the same sizes in both structures) will have those first N fields laid out in memory identically, by any C compiler/architecture combination. Different C compilers will lay the structures out differently - in particular, with different amounts of padding for alignment - but the same C compiler running with the same compilation options will lay the prefix fields out identically.
- But how does this help with subclass compatibility? Look at this memory layout diagram:
![]()
struct Car and struct Vehicle have a common prefix of fields (all fields from struct Vehicle, in fact), and these overlap perfectly, including all the padding between sequences of fields of the same size. Note that many of the padding values may happen to be zero on a particular architecture, but non-zero on another architecture. In fact, although I haven't checked this, I would expect only p4 to be non-zero on 64-bit linux with gcc, where sizeof(void *) and sizeof(double) are 8, and sizeof(int) = 4.
- Now, whether or you're fully convinced that overlaying a larger structure onto a smaller one is safe, portable, and correctly implements subclass compatibility (if you're not, don't worry, in a little while we'll walk through exactly what happens as we initialize a Car object and call methods on it) let's press on and start implementing car.c:
- First, we include what we need:
#include <stdio.h> #include <stdlib.h> #include <assert.h> #include "vector2.h" #include "vehicle.h" #include "car.h"- Second, we define the methods we are redefining, or adding:
static void addfuel( Car this, double fuel ) // add fuel litres to the fuel tank. { assert( fuel >= 0 ); double max = this->fuel_max; assert( this->fuel_cur >= 0 && this->fuel_cur <= max ); this->fuel_cur += fuel; if( this->fuel_cur > max ) this->fuel_cur = max; } static void print( Car this ) // redefine how to display this car { printf( "Car( " ); printf( "%s: pos:", this->name ); OM0( this->pos, print ); printf( ", dir:" ); OM0( this->dir, print ); printf( ", speed:%g, people: %d (max:%d)", this->speed, this->people_cur, this->people_max ); printf( ", wheels:%d, fuel:%g (max %g)", this->wheels, this->fuel_cur, this->fuel_max ); printf( " )" ); }- Third, we define our allocator:
Car alloc_Car( void ) { Car c = (Car) malloc( sizeof(struct Car) ); assert( c != NULL ); return c; }Unlike our VV allocator, which reused the Vector2 allocator, we must malloc() a block big enough for a struct Car!- Fourth, we define our initializer:
void init_Car( Car c, char * name, Vector2 pos, Vector2 dir, double speed, int people_cur, int people_max, int wheels, double fuel_cur, double fuel_max ) { // set "vehicly" bits up init_Vehicle( (Vehicle)c, name, pos, dir, speed, people_cur, people_max ); // now set "car"ry bits up (overriding the print method) c->isa = "Car"; c->wheels = wheels==-1 ? 4 : wheels; c->fuel_cur = fuel_cur==-1 ? 0 : fuel_cur; c->fuel_max = fuel_max; c->addfuel = &addfuel; c->print = &print; }Here we use initializer chaining to avoid Repeating Ourselves, and then override or add our new Car-specific extra fields. We'll walk through how this works at run-time in a little while, complete with more diagrams.- Finally, our constructor glues it all together:
Car new_Car( char * name, Vector2 pos, Vector2 dir, double speed, int people_cur, int people_max, int wheels, double fuel_cur, double fuel_max ) { Car c = alloc_Car(); init_Car( c, name, pos, dir, speed, people_cur, people_max, wheels, fuel_cur, fuel_max ); return c; }- Note that we don't need to redefine any other methods, not even our dispose() method. Our inherited (Vehicle) dispose() method simply calls free(v) which works as well for a struct Car * as for a struct Vehicle *.
- Now, we create a test program, simpletestcar.c, which is a clone of simpletestvehicle.c that creates a Car not a Vehicle, and also adds some fuel to it's tank. You can download and compile the 05.oo-car tarball containing car.c, car.h, simpletestcar.c (and all the vehicle support materials unchanged from the previous tarball). As usual, unpack it, enter the 05-oo-car directory, type
make
andsimpletestcar
will be ready for you to run. Run it via./simpletestcar
and you'll see it print:c1 initially: Car( c1: pos:[0.000,0.000], dir:[0.000,0.000], speed:3, people: 1 (max:6), wheels:4, fuel:100 (max 200) ) after 2.000 seconds moving stationary at 3 m/s: Car( c1: pos:[0.000,0.000], dir:[0.000,0.000], speed:3, people: 1 (max:6), wheels:4, fuel:100 (max 200) ) c1 is now 0.000 metres away from startingpoint after 2.000 seconds moving north at 3 m/s: Car( c1: pos:[0.000,6.000], dir:[0.000,1.000], speed:3, people: 1 (max:6), wheels:4, fuel:100 (max 200) ) c1 is now 6.000 metres away from startingpoint after 5.000 seconds moving east at 2 m/s: Car( c1: pos:[10.000,6.000], dir:[1.000,0.000], speed:2, people: 1 (max:6), wheels:4, fuel:100 (max 200) ) c1 is now 11.662 metres away from startingpoint c1 after refuelling: Car( c1: pos:[10.000,6.000], dir:[1.000,0.000], speed:2, people: 1 (max:6), wheels:4, fuel:150 (max 200) ) after 11.662 seconds moving back at 1 m/s: Car( c1: pos:[-0.000,-0.000], dir:[-0.857,-0.514], speed:1, people: 1 (max:6), wheels:4, fuel:150 (max 200) ) c1 is now 0.000 metres away from startingpointHow did that work: step by step
- In case you're having difficulty seeing exactly how this works, let's single step through the first part of the simpletestcar program, especially the creation of a new_Car() and how method calls work. If you're familiar with a debugger such as gdb, you might well want to follow along by single-stepping through the program.
- simplestestcar's main() function starts by creating 3 new_Vector2()s, and then creating a new_Car() using two of them.
startingpoint = new_Vector2( 0, 0 ); Vector2 pos = new_Vector2( 0, 0 ); Vector2 dir = new_Vector2( 0, 0 ); Car c1 = new_Car( "c1", // name pos, // pos: origin dir, // dir: standstill 3, // speed 1, // people_cur 6, // people_max -1, // default wheels 100, // fuel_cur 200 // fuel_max );Let's assume that we understand how the new_Vector2(0,0) calls work, and follow the new_Car() call.- new_Car() reads:
Car new_Car( char * name, Vector2 pos, Vector2 dir, double speed, int people_cur, int people_max, int wheels, double fuel_cur, double fuel_max ) { Car c = alloc_Car(); init_Car( c, name, pos, dir, speed, people_cur, people_max, wheels, fuel_cur, fuel_max ); return c; }- So, new_Car() first calls alloc_Car(), which malloc()s a chunk of size sizeof(struct Car), tests it's not null, and returns it:
Car alloc_Car( void ) { Car c = (Car) malloc( sizeof(struct Car) ); assert( c != NULL ); return c; }All fields in that struct Car will contain unpredictable uninitialized values. If you're following this discussion with gdb, feel free to step through alloc_Car() until it returns, and then print out c (print *c).- Next, new_Car() calls init_Car() to initialize our new object. Substituting the parameters, our call is:
init_Car( c, name="c1", pos=new_Vector2(0,0), dir=new_Vector2(0,0), speed=3, people_cur=1, people_max=6, wheels=-1, fuel_cur=100, fuel_max=200 );- init_Car() is as follows:
void init_Car( Car c, char * name, Vector2 pos, Vector2 dir, double speed, int people_cur, int people_max, int wheels, double fuel_cur, double fuel_max ) { // set "vehicly" bits up init_Vehicle( (Vehicle)c, name, pos, dir, speed, people_cur, people_max ); // now set "car"ry bits up (overriding the print method) c->isa = "Car"; c->wheels = wheels==-1 ? 4 : wheels; c->fuel_cur = fuel_cur==-1 ? 0 : fuel_cur; c->fuel_max = fuel_max; c->addfuel = &addfuel; c->print = &print; }- The first thing init_Car() does is to call:
init_Vehicle( (Vehicle)c, name="c1", pos=new_Vector2(0,0), dir=new_Vector2(0,0), speed=3, people_cur=1, people_max=6 );- Before we look at init_Vehicle(), lets get very clear about what the (Vehicle) typecast does (and doesn't) do.
Vehicle v = (Vehicle)c; // c is a CarBoth Vehicle and Car are pointers, all pointers are the same size - pure memory addresses - so at run-time the typecast is a no-op. It generates no extra code, as widening an int to a long, or narrowing a double to a float would do. So what does the typecast do? It tells the compiler that the unaltered memory address is a pointer to a struct Vehicle instead of a struct Car. Thus inside init_Vehicle(), the compiler will start using struct Vehicle offsets and padding - but as these offsets are identical to the prefix offsets in struct Car, initialization will take place correctly.- The function init_Vehicle() (in vehicle.c) reads:
void init_Vehicle( Vehicle v, char * name, Vector2 pos, Vector2 dir, double speed, int people_cur, int people_max ) { v->isa = "Vehicle"; v->name = name; v->pos = pos == NULL ? new_Vector2(-1,-1) : pos; v->dir = dir == NULL ? new_Vector2(-1,-1) : dir; v->speed = speed==-1 ? 0 : speed; v->people_cur = people_cur==-1 ? 1 : people_cur; v->people_max = people_max==-1 ? 4 : people_max; v->move = &move; v->print = &print; v->dispose = &dispose; }- We therefore perform the following assignments:
v->isa = "Vehicle"; v->name = "c1"; v->pos = new_Vector2(0,0); v->dir = new_Vector2(0,0); v->speed = 3; v->people_cur = 1; v->people_max = 6; v->move = &move; // vehicle move() function v->print = &print; // vehicle print() function v->dispose = &dispose; // vehicle dispose() functionAs v is overlaid on top of c, these assignments actually set the following fields in c. Having done these assignments, init_Vehicle() returns to init_Car().- Before we continue with init_Car(), let's see a memory layout of our partially-constructed Car:
![]()
This clearly shows that, at this stage, it's only been initialized as a Vehicle and in fact it "isa" Vehicle too.
- init_Car() continues with the rest of it's body:
void init_Car( Car c, char * name, Vector2 pos, Vector2 dir, double speed, int people_cur, int people_max, int wheels, double fuel_cur, double fuel_max ) { ... // now set "car"ry bits up (overriding the print method) c->isa = "Car"; c->wheels = wheels==-1 ? 4 : wheels; c->fuel_cur = fuel_cur==-1 ? 0 : fuel_cur; c->fuel_max = fuel_max; c->addfuel = &addfuel; c->print = &print; }- We therefore perform the following assignments:
c->isa = "Car"; c->wheels = 4; c->fuel_cur = 100; c->fuel_max = 200; c->addfuel = &addfuel; c->print = &print;- Now, our Car object is fully initialized, note that the isa field now correctly says "Car", and the print field has been redirected to Car's print method:
![]()
- init_Car() finally returns our newly created Car object to main(), where it is stored in Car c1. main() continues:
Car c1 = new_Car( "c1", .... ); printf( "%s initially: ", c1->name ); OM0( c1, print ); printf( "\n" );This shows us accessing an attribute (name) directly through the c1 pointer, and printing the object as usual via a OM0() method invocation.- Recall that OM0() expands the print call to:
(*(c1->print))(c1)which fetches c1->print (from the diagram, this is &car's print function), and dereferences and calls it with c1 as it's only argument. Naturally, car's print function invoked with c1, i.e. print(c1), then displays car c1 in all it's glory, formatting and displaying all it's attributes appropriately.
- The only thing we haven't shown here is how an inherited Vehicle method (such as move()) works with a Car, eg. how the following works:
OM1( c1, move, 5 ); // move for 5 secondsThere's one final subtlety here, Vehicle's move() method will be called - but with a Car as it's "this" argument. This happens completely safely and portably - but let's just convince ourselves how it works:
First, recall that when we defined struct Car we defined a Car_move type taking a Car as it's first parameter (and a double as it's second):
typedef void (*Car_move)( Car this, double t );Then we used the Car_move type to define struct Car's move field as:
struct Car { ... Car_move move; ... }From the diagram, the value contained in the c1->move field is &vehicle's move() function, however we've cheated slightly and told the compiler that the pointer points to a function that "takes a Car" (by the Car_move type declaration).
Hence, we can dereference the function pointer and call it with Car c1 as a parameter, without a typecast - as we told the compiler "it takes a Car" and we just gave it a Car. But is it safe and portable to give Vehicle's move(Vehicle this..) method function a Car as it's argument? Yes, this is just like when we initialized the Vehicle parts of our new Car via init_Vehicle( (Vehicle)c...), the only difference being that there we needed an explicit typecast, here we don't.
- Feel free to continue tracing through how the test program (and all it's method calls) work, but we'll leave it there.
A slight modification to simpletestcar:
- As a final flourish, let's create and pass a VV (a verbose-vector, remember) into new_Car() instead of a simple Vector2, so that we can use even more inheritance and check even more subclass compatibility. In simpletestcar.c add:
#include "vv.h"Then edit the Makefile to ensure that simpletestcar.o depends on vv.h, and that we must link vv.o into the executable:simpletestcar: simpletestcar.o car.o vehicle.o vector2.o vv.o simpletestcar.o: vector2.h vehicle.h car.h vv.hThen change the constructor call to:
Vector2 pos = new_VV( 0, 0 ); Vector2 dir = new_Vector2( 0, 0 ); Car c1 = new_Car( "c1", // name pos, // pos: origin dir, // dir: standstill 3, // speed 1, // people_cur 6, // people_max -1, // default wheels 100, // fuel_cur 200 // fuel_max );- Now, after recompiling simpletestcar via make, the output becomes:
c1 initially: Car( c1: pos:[0.000,0.000,magnitude 0], dir:[0.000,0.000], speed:3, people: 1 (max:6), wheels:4, fuel:100 (max 200) ) after 2.000 seconds moving stationary at 3 m/s: Car( c1: pos:[0.000,0.000,magnitude 0], dir:[0.000,0.000], speed:3, people: 1 (max:6), wheels:4, fuel:100 (max 200) ) c1 is now 0.000 metres away from startingpoint after 2.000 seconds moving north at 3 m/s: Car( c1: pos:[0.000,6.000,magnitude 6], dir:[0.000,1.000], speed:3, people: 1 (max:6), wheels:4, fuel:100 (max 200) ) c1 is now 6.000 metres away from startingpoint after 5.000 seconds moving east at 2 m/s: Car( c1: pos:[10.000,6.000,magnitude 11.6619], dir:[1.000,0.000], speed:2, people: 1 (max:6), wheels:4, fuel:100 (max 200) ) c1 is now 11.662 metres away from startingpoint c1 after refuelling: Car( c1: pos:[10.000,6.000,magnitude 11.6619], dir:[1.000,0.000], speed:2, people: 1 (max:6), wheels:4, fuel:150 (max 200) ) after 11.662 seconds moving back at 1 m/s: Car( c1: pos:[-0.000,-0.000,magnitude 1.98603e-15], dir:[-0.857,-0.514], speed:1, people: 1 (max:6), wheels:4, fuel:150 (max 200) ) c1 is now 0.000 metres away from startingpointSure enough, every time we call thec1->pos->print()
method, it now prints via the VV print() method - and displays the magnitude of pos as well as it's x and y values. Note the final magnitude is not quite 0.0 - further evidence of floating point inexactitude.- Aside: back in the first part, we discussed the possibility of method chaining, where we refactored Car and Vehicle's print() methods into a wrapper and a separate innerprint() method, and then Car's innerprint() method invoked Vehicle's innerprint() method. We wrote OO pseudocode:
implementation class Car; void method innerprint() // display the key/value innards of a car { this.Vehicle.innerprint(); // method chain: call Vehicle's innerprint() printf( ", wheels:%d, fuel:%.3f (max %.3f)", wheels, fuel_cur, fuel_max ); }To implement method chaining in C, we have to gain access to the Vehicle's innerprint() method function, eg by making it non-static, renaming it Vehicle_innerprint(), adding a prototype for it into vehicle.h, and then calling Vehicle_innerprint((Vehicle)this) above. Feel free to implement this if you like, it's straightforward.- We could go on to increase the size of our OO system, so far we've only had test programs with a single Car or Vehicle (although we've had 3 Vector2's at a time). We could easily build, generate or reuse a linked list module that can store a list of Vehicles (either building a
list(void *)
or alist(Vehicle)
). In the latter case, note that subclass compatibility would allow a singlelist(Vehicle)
to contain a mix of Vehicles and Cars (and any future subclasses of Cars or Vehicles).Then we could simulate many independent vehicles moving around the plane. An obvious extension would be to write a collision detection method to see if any two vehicles come dangerously close together at any time. An interesting challenge would be to replace the list of Vehicles with some plane-based structure that would make it more efficient to check whether any 2 vehicles collide - without needing to take one Vehicle as the datum and see if it has collided with every other Vehicle.
But we must stop somewhere. This article was not about building a traffic simulation system, but about explaining a general mechanism for implementing OO programming in C. We've done that!
Summary.
In this 3-part article, I've shown how to translate single inheritance Object Oriented Programming with virtual methods into ANSI C, using the following mixture of techniques:
- An ANSI C module (.h and .c pair) for each class.
- Each object is a pointer to an object structure, malloc()d and initialized by a new_Class(args) constructor.
- The object structure contains all the object's attributes and methods, encapsulating them into a nice little package of smart data.
- A method field in an object structure is a pointer to a function that must always take an object pointer (me, myself, this object) as it's first argument.
- Every method call is dynamically dispatched to the correct (virtual) method by looking the method field up in the object structure, dereferencing and calling it - remembering to pass the object pointer as it's first argument. eg.
(*(c->move)((c,5)
. This can be written asc->move(c,5)
in recent ANSI C.- Polymorphism (inheritance and subclass compatibility) are implemented in ANSI C by using compatible memory layouts for object structures, and a small amount of safe typecasting where necessary.
I can't emphasise enough that this is a safe and portable technique, not depending on any particular C compiler to work. Eric Raymond has recently written a guide called The Lost Art Of Structure Packing which describes how C compilers (portably) lay out fields in structures, how they are not allowed to reorder fields, all about padding etc. (Thanks to Latency McLaughlin of LinkedIn for this URL).
If you're still not convinced that every ANSI C compiler must abide by the structural layout rules I described, then I challenge you, gentle reader, to find me an ANSI C compiler anywhere in the world where structures are not laid out in that fashion. Please email me if you find one! Just in case I'm wrong - there might even be a free beer in it for the first person to find one:-)
If this is useful to you, I'd be delighted if you'd drop me a quick email to let me know:-) -- Duncan White, 10th Aug 2014.
d.white@imperial.ac.uk Back to the first part Back to the second part Back to PSD Top Written: July-August 2014