OOP design based on C code
The following contents list an example implementation of OOP using C code. This is not the only way to implement OOP in C code, but this could an efficient way of utilizing RAM/ROM space. Three classes are shown below.
Parent class:
Shape/***************** Class Shape *********************** | attributes | | methods | *****************************************************/ // Attribute(s) typedef struct Shape_t { const struct Vtbl_t *VtblPtr; int32_t pos_x; int32_t pos_y; } Shape; typedef struct Vtbl_t { void (* draw)(Shape *me); void (* move_to)(Shape *me, int32_t pos_x, int32_t pos_y); } Vtbl; // Method(s) - draw. Abstract method. Shall never be called. void draw(Shape *me) { assert(0); } // Method(s) - move_to. Abstract method. Shall never tbe called. void move_to(Shape *me, int32_t pos_x, int32_t pos_y) { assert(0); } // Method(s) - ctor void ctor(Shape *me, int32_t pos_x, int32_t pos_y) { static const Vtbl vTable = { .draw = draw, .move_to = move_to, }; me->VtblPtr = &vTable; me->pos_x = pos_x; me->pos_y = pos_y; printf("Calling Shape ctor...\r\n"); }
Child class:
Rectangle, inherited fromShape./**************** Class Rectangle ******************** | attributes | | methods | *****************************************************/ // Attributes(s) typedef struct Rectangle_t { Shape super; // Inherited from Shape class. int32_t height; int32_t width; } Rectangle; // Method(s) - Overwrite draw(). void rec_draw(Shape *me) { // Explicit downcasting: ((Rectangle *)me)->height. printf("Draw a rectangle. " "Pos_x: %d, " "Pos_y: %d, " "height: %d, " "width: %d, " "\r\n", me->pos_x, me->pos_y, ((Rectangle *)me)->height, ((Rectangle *)me)->width); } // Method(s) - Overwrite move_to(). void rec_move_to(Shape *me, int32_t pos_x, int32_t pos_y) { printf("Rectangle move from: " "[%d, %d] to [%d, %d]" "\r\n", me->pos_x, me->pos_y, pos_x, pos_y); me->pos_x = pos_x; me->pos_y = pos_y; } // Method(s) - rectangle_ctor void rectangle_ctor(Rectangle *me, int32_t pos_x, int32_t pos_y, int32_t height, int32_t width) { static const Vtbl recVtable = { .draw = rec_draw, .move_to = rec_move_to, }; ctor((Shape *)me, pos_x, pos_y); me->super.VtblPtr = &recVtable; // Overwrite the virtual table. me->height = height; me->width = width; printf("Calling Rectangle ctor...\r\n"); }
Child class:
Circle, inherited fromShape./****************** Class Circle ********************* | attributes | | methods | *****************************************************/ // Attributes(s) typedef struct Circle_t { Shape super; int32_t radius; } Circle; // Method(s) - Overwrite draw(). void circle_draw(Shape *me) { printf("Draw a circle. " "Pos_x: %d, " "Pos_y: %d, " "radius: %d, " "\r\n", me->pos_x, me->pos_y, ((Circle *)me)->radius); } // Method(s) - Overwrite move_to(). void circle_move_to(Shape *me, int32_t pos_x, int32_t pos_y) { printf("Circle move from: " "[%d, %d] to [%d, %d]" "\r\n", me->pos_x, me->pos_y, pos_x, pos_y); me->pos_x = pos_x; me->pos_y = pos_y; } // Method(s) - circle_ctor void circle_ctor(Circle *me, int32_t pos_x, int32_t pos_y, int32_t radius) { static const Vtbl circleVtable = { .draw = circle_draw, .move_to = circle_move_to, }; ctor((Shape *)me, pos_x, pos_y); me->super.VtblPtr = &circleVtable; // Overwrite the virtual table. me->radius = radius; printf("Calling Circle ctor...\r\n"); }
Things worth noting
Firstly, the definition of Shape class (shown below) contains a virtual table structure. It is expected to be read-only, and hence the const specifier.
typedef struct Shape_t {
const struct Vtbl_t *VtblPtr;
int32_t pos_x;
int32_t pos_y;
} Shape;
Secondly, as the virtual table is defined after Shape definition, the correct way to encapsulate a virtual table pointer is to leverage struct specifier. Notice the compiler would complain about const Vtbl *VtblPtr as incomplete type. Similarly, think of Node definition used in a linked list.
typedef struct Node_t {
struct Node_t *left;
struct Node_t *right;
uint32_t value;
} Node;
Thirdly, the encapsulation of a class is more than just a structure. It should also include at least constructor method (i.e., ctor). The Shape class could serve as a abstract class, where draw() and move_to() method must be implemented by child classes. In our case, we issue asserts as run-time check.
Polymorphism in action
Since we abstract the common methods (i.e., draw() and move_to()) from each child class, we could call such functions in a for loop as below. Notice how each overwritten methods work.
void draw_any_shape(Shape *me)
{
me->VtblPtr->draw(me);
}
void move_to_any_shape(Shape *me, int32_t pos_x, int32_t pos_y)
{
me->VtblPtr->move_to(me, pos_x, pos_y);
}
int main(int argc, char **argv)
{
Rectangle rec_0;
Rectangle rec_1;
Circle circle_0;
Circle circle_1;
Shape *shapeLst[4] = {
[0] = (Shape *)&rec_0,
[1] = (Shape *)&rec_1,
[2] = (Shape *)&circle_0,
[3] = (Shape *)&circle_1,
};
rectangle_ctor(&rec_0, 12, 12, 3, 4);
rectangle_ctor(&rec_1, 15, 17, 6, 9);
circle_ctor(&circle_0, 7, 8, 9);
circle_ctor(&circle_1, 10, 23, 8);
for (int8_t i; i < 4; i++) {
draw_any_shape(shapeLst[i]);
move_to_any_shape(shapeLst[i], 29, 29);
}
return 0;
}
The related console outputs are:
Calling Shape ctor...
Calling Rectangle ctor...
Calling Shape ctor...
Calling Rectangle ctor...
Calling Shape ctor...
Calling Circle ctor...
Calling Shape ctor...
Calling Circle ctor...
Draw a rectangle. Pos_x: 12, Pos_y: 12, height: 3, width: 4,
Rectangle move from: [12, 12] to [29, 29]
Draw a rectangle. Pos_x: 15, Pos_y: 17, height: 6, width: 9,
Rectangle move from: [15, 17] to [29, 29]
Draw a circle. Pos_x: 7, Pos_y: 8, radius: 9,
Circle move from: [7, 8] to [29, 29]
Draw a circle. Pos_x: 10, Pos_y: 23, radius: 8,
Circle move from: [10, 23] to [29, 29]