In Part1 we have laid the foundation of how a class could look like in C. This simple class is good for creating an Abstract Data Type but it doesn’t support polymorphism and inheritance…yet. Here is an UML diagram (made with dia) of what we are trying to achieve in this post:
Admitted, the example is not very original but it fits our purpose. So, Shape is the base class while Circle and Rectangle are both subclasses. All member attributes are protected which means that users of the class(es) can’t access them directly.
There are several ways to implement this in C. Here, the proposed solution follows a ‘C-like’ way of thinking. It is a bit hard to explain but it means the approach is pragmatic and avoids too much abstraction. Similar techniques are used a lot in (embedded) C code e.g. in kernels. It is though technically not the most sophisticated solution for OO (we elaborate on this in Part3) but it is sufficient for a lot of use cases.
The interface is very simple:
- only interface functions are exposed to the user. The interface file is not ‘polluted’ with protected and/or private members. And,
- only one type ‘struct shape’ is declared and visible. The user has no choice but to use the base class.
#ifndef SHAPE_H #define SHAPE_H struct shape; struct shape* circle_alloc(int id, int radius); struct shape* rectangle_alloc(int id, int w, int h); void shape_free(struct shape* s); int shape_get_id(const struct shape* s); int shape_get_area(const struct shape* s); #endif
Although not shown in this example, interface functions can be grouped in a separate struct (call it a vtable). E.g. in the Linux kernel, you can find lots of _ops and _operations structs.
The interface implementation is written as follows:
- struct shape contains virtual functions (functions pointers) and base class attributes (int id),
- struct shape has a void pointer to subclass data: this is private data of the subclass and must be set by the subclass,
- an interface function just calls the virtual function which must also be set by the subclass.
#include "shape.h" #include <math.h> #include <stdlib.h> /* shape */ struct shape { int id; /* virtual functions */ int (*get_area) (const struct shape* s); /* private data of subclass */ void* subclass_data; }; void shape_free(struct shape* s) { free(s->subclass_data); free(s); } int shape_get_id(const struct shape* s) { return s->id; } int shape_get_area(const struct shape* s) { return s->get_area(s); }
Subclasses then implement the base class (struct shape) by filling in the members (see circle_alloc() and rectangle_alloc() in the code below). Subclass attributes (e.g. circle radius) are part of the private subclass data.
/* rectange */ struct rectangle_subclass_data { int width, height; }; static int rectangle_get_area(const struct shape* s) { const struct rectangle_subclass_data* d = (const struct rectangle_subclass_data*) s->subclass_data; return d->width * d->height; } struct shape* rectangle_alloc(int id, int w, int h) { struct shape* s = (struct shape*)malloc(sizeof(struct shape)); struct rectangle_subclass_data* d = (struct rectangle_subclass_data*)malloc(sizeof(struct rectangle_subclass_data)); s->id = id; s->get_area = rectangle_get_area; s->subclass_data = d; d->width = w; d->height = h; return s; } /* circle */ struct circle_subclass_data { int radius; }; static int circle_get_area(const struct shape* s) { const struct circle_subclass_data* d = (const struct circle_subclass_data*) s->subclass_data; return d->radius * d->radius * M_PI; } struct shape* circle_alloc(int id, int r) { struct shape* s = (struct shape*)malloc(sizeof(struct shape)); struct circle_subclass_data* d = (struct circle_subclass_data*)malloc(sizeof(struct circle_subclass_data)); s->id = id; s->get_area = circle_get_area; s->subclass_data = d; d->radius = r; return s; }
The code can be extended to support class hierarchies, you only need to think carefully about how to avoid code duplication and how to organize your subclass data. But that is not different from any other code. A drawback of this technique is that the class hierarchy is implicit (for class hierarchies, we therefor advise the solution that we will present in Part3).
It is not difficult to cleanly organize the code. Here is an example:
- shape/
- export/shape.h: public header file
- shape_p.h: private header file, to be included by subclasses
- shape.c: interface implementation
- circle.c: subclass definition
- rectangle.c
- …
Speak Your Mind