ObjectOrientedC
2000-01-01
This is just an example how to get object oriented C. There are many more ways to do it, some much more advanced. This is the one used throughout Land.About the used terms, just always assume what makes sense. Everything listed in the same line below means the same to me. Especially note terms in bold who appear more than once:
* base, super class, parent * sub class, derived object, child * instance, class instance, object * object, class, type * method, function * property, variable
Assume we have an object called Animal, with two methods "eat" and "go".
void animal_eat(Animal *self, char const *what, int howmuch); int animal_go(Animal *self, int direction, int howfar);
And, we also have a derived object called Bird.
This is how we could use it:
Bird *bird = bird_new("Sparrow"); animal_eat(bird, "Apple", 1); animal_go(bird, 0, 1); bird_del(bird);
Let's define all the structs, and also add four properties to Animal, "name", "hungry", "x", and "y", and an extra property "z" to Bird.
typedef struct AnimalInterface AnimalInterface; typedef struct Animal Animal;
struct AnimalInterface { void (*eat)(Animal *self, char const *what, int howmuch); bool (*go)(Animal *self, int direction, int howfar); };
struct Animal { AnimalInterface *vt; char *name; int hungry; int x, y; };
struct Bird { Animal super; int z; };
As you can see, this is defined using two C structs, one for the interface and one for the object itself. The interface is simply implemented as a vtable pointed to by the object.
Next, we define some housekeeping stuff.
AnimalInterface *animal_interface; void animal_init(void) { animal_interface = calloc(1, sizeof *animal_interface); animal_interface->eat = NULL; animal_interface->go = NULL; }
This is an initialization function for the module. It defines the interface for out Animal class. We set both the eat and go methods to NULL, making this an abstract class. If we wanted we could also set it to two functions which can be overridden by subclasses.
void animal_create(Animal *self, char const *name) { memset(self, sizeof *self, 0); self->vt = animal_interface; self->name = name; }
Animal *animal_new(char const *name) { Animal *self = malloc(sizeof *self); animal_initialize(self, name); return self; }
void animal_del(Animal *self) { free(self); }
We defined a constructor, and a destructor. The constructor can have parameters and we have a version animal_create which will not allocate but just initialize and a version animal_new which will do both. The animal_create version will be useful for derived classes who want to call the parent constructor.
So now let's build our derived class.
#define BIRD(_base_) ((Bird *)_base_)
void bird_eat(Animal *super, char const *what, int howmuch) { Bird *self = BIRD(super); super->hungry -= howmuch * get_nutrition(what); }
bool bird_go(Animal *super, int direction, int howfar) { Bird *self = BIRD(super); if (collision()) return false;
super->x += cos(direction) * howfar; super->y += sin(direction) * howfar; super->hungry += howfar;
return true; }
int bird_fly(Bird *self, int howfar) { if (collision()) return 0; self->z += howfar; return 1; }
Here we define three methods for the bird. The macro is just to cast between Animal and Bird.
void bird_create(Animal *super, char const *name) { animal_create(super, name); super->vt = bird_interface; Bird *self = BIRD(super); self->z = 0; }
Animal *bird_new(char const *name) { Bird *self = calloc(1, sizeof *self); bird_create(&self->super, name); return &self->super; }
void bird_del(Animal *self) { animal_del(self); }
AnimalInterface *bird_interface; void bird_init(void) { bird_interface = calloc(1, sizeof *bird_interface); *bird_interface = *animal_interface; bird_interface->eat = bird_eat; bird_interface->go = bird_go; }
Here, we actually define the methods of the Bird object. The last function is the module init function again, which creates an interface and this time has both methods filled in. We copy over the animal interface but it would not be necessary in this case as there are no other methods.
We also have a constructor, again with a separate create function and a destructor. The object instance is always passed as the type of the base object (Animal) and not as the actual object (Bird) to conform to the method declaration. For the bird_fly method we could do both but since it only exists for birds we can as well use the Bird type. Since this is C it's all just pointers anyway. We could have the BIRD macro try and do some run-time type identification thogh - e.g. by deriving every possible class off a base Object class which has a type field.
If you remember the initial example:
Bird *bird = bird_new("Sparrow"); animal_eat(bird, "Apple", 1); animal_go(bird, 0, 1); bird_del(bird);
Then we could of course just call the bird methods directly, like:
Bird *bird = bird_new("Sparrow"); bird_eat(bird, "Apple", 1); bird_go(bird, 0, 1); bird_del(bird);
For the fly method we have to do it like that anyway since no animal_fly exists in the base:
bird_fly(bird, 1);
But the advantage of using the animal_ methods simply is that we might not know what type of animal it is, like, assume, this is a game involving 100 different animals who can eat things and go around. Then our main program can be written without needing to know the type of each animal - it just loops through all 100 and calls their methods.
There are also various possible variations, e.g. instead of the "super/self" and so on parameters we could have used "animal/bird" to directly know the kind of object. It is also possible to have deeper sub classing, e.g. a class Sparrow derived from Bird. And objects may be created in a different way than the constructor function, for example there could be a mapping of strings to classes, then a function "Animal *animal_create(char const *name) could create the appropriate one.
Also classes who don't need any new variables of their own don't actually need their own struct but just a new vtable.