While C is not an object-oriented language, objects, inheritance, and polymorphism can be implemented in C.
Table of Contents:
Inheritance
The core idea behind inheritance relies on structures. A base class or API can be represented by a struct
definition that includes all relevant interfaces and data members.
For example, consider this example of an event base “class” from an Electron Vector article:
typedef enum {
EVENT_BUTTON_PRESSED,
EVENT_BUTTON_RELEASED,
EVENT_KNOB_SET,
EVENT_SWITCH_SET,
} event_type_t;
typedef struct {
event_type_t type;
} event_t;
To simulate inheritance in C, we define additional event structures which include the base structure as the first element. The base structure must always be the first element.
typedef struct {
event_t event;
button_t button;
} event_button_pressed_t;
typedef struct {
event_t event;
button_t button;
} event_button_released_t;
The key detail is that a pointer to the derived structures (event_button_pressed_t
) is also a pointer to the base structure (event_t
). You can safely cast between the two structures, and any derived type can be used with APIs that require only the base type.
Just remember this foundational pattern, outlined in Dmitry Frank’s article:
/* base class */
typedef struct S_Base {
/* ... */
} T_Base;
/* derived class */
typedef struct S_Derived {
T_Base base;
/* ... */
} T_Derived
Polymorphism
Polymorphism is the ability to substitute objects using matching interfaces for one-another at runtime. With C++, polymorphism is handled through virtual interfaces and the virtual table (vtable). The example code used below is adapted from Miro Samek’s excellent overview.
For example, we can define a base “class” for a shape, along with a vtable which contains function pointers. Derived classes populate the vtable pointers with functions that are specific to the derived class.
struct ShapeVTable; // Forward declare the vtable
// Base class definition
typedef struct {
struct ShapeVTable const *vptr;
int32_t x; // x-coord of shape's position
int32_t y; // y-coord of shape's position
} Shape;
struct ShapeVTable
{
uint32_t (*area)(Shape const * const me);
void (*draw)(Shape const * const me);
};
Non-virtual functions for the base class are implemented as a standard functions which take a base class pointer as the first parameter:
/* Shape's operations (Shape's interface)... */
void Shape_ctor(Shape * const me, int16_t x, int16_t y);
void Shape_moveBy(Shape * const me, int16_t dx, int16_t dy);
One such required non-virtual function is a constructor, whose most important job is to supply the proper vtable for the class:
void Shape_ctor(Shape * const me, int16_t x, int16_t y) {
static struct ShapeVtbl const vtbl = { /
* vtbl of the Shape class */
&Shape_area_,
&Shape_draw_
};
me->vptr = &vtbl; /* "hook" the vptr to the vtbl */
me->x = x;
me->y = y;
Any base class or derived function can reference the virtual functions using the vtable:
static inline uint32_t Shape_area(Shape const * const me)
{
return (*me->vptr->area)(me);
}
To derive from the base class, we simply use the base class Shape
as the first element of the derived structure:
typedef struct {
Shape base; /* <== inherits Shape */
/* attributes added by this subclass... */
uint16_t width;
uint16 _t height;
} Rectangle;
As a result, we can cast Rectangle
so it can be used with Shape
interfaces. We can also create Rectangle
-specific interfaces.
Of course, our derived class needs its own constructor:
void Rectangle_ctor(Rectangle * const me, int16_t x, int16_t y,
uint16_t width, uint16_t height)
{
static struct ShapeVtbl const vtbl = {
/* vtbl of the Rectangle class */
&Rectangle_area_,
&Rectangle_draw_
};
// We always call the base class constructor
Shape_ctor(&me->base, x, y);
// But we must override the base vtable with Rectangle's
me->base.vptr = &vtbl;
// Rectangle-specific values
me->width = width;
me->height = height;
}
Because we defined a new class, we need Rectangle
-specific implementations of the virtual functions. These are added to a new vtable.
Whenever a Rectangle
is constructed, the base class constructor is called first. Then, the vtable
is set to Rectangle
’s table. Any virtual function calls will dereference the proper Rectangle
-specific implementation, even when using base class interfaces.
This is the basic overview of implementing polymorphism and virtual functions in C. For a detailed overview of implementing full polymorphism, see the links in Further Reading.
As a final note, if you are relying on these concepts in your C programs, you will likely benefit from switching to C++, if only for the fact that the compiler handles so much of this overhead for you. Additionally, you can rely on optimizations from the C++ compiler that won’t work in the C implementation.
Further Reading
For more on Inheritance, Polymorphism, and OOP in C:
- Technique: Objects in C
- Technique: Encapsulation and Information Hiding in C
- Programming embedded systems: polymorphism in C by Miro Samek
- Programming embedded systems: inheritance in C and C++ by Miro Samek
- Object-Oriented Programming in C, by Miro Samek, walks through using encapsulation, inheritance, and polymorphism in C. Companion code is also available.
- Object-oriented Techniques in C by Dmitry Frank provides an example implementation of polymorphism
- Nathan Jones has a take on Simple Inheritance in his Comparison of OOP Techniques in C project. The rest of the project focuses on different polymorphism techniques that can be applied beyond what is described here.
- One of the most detailed guides for implementing objects in C is From C Function Pointers to C++ Objects by Hayo Thielecke. This paper will walk you through the creation of objects using structures and function pointers.
- Event-based Interfaces for Testability shows a practical use of inheritance using event-oriented design
- How-To Implement Subtype Polymorphism in C provides another example