Skip to main content

Section 8.12 Object-Oriented Programming with C

In the previous section, we have seen the differences between an object-oriented language and a purely imperative language like C. In this section, we outline how object-orientation can be realized in C.

Consider the example in Listing 8.10.3 and Listing 8.10.2. For simplicity's sake, we assume that a class can only inherit from/implement a single class/interface. Allowing to implement several interfaces would slightly complicate the implementation without contributing fundamentally to the understanding.

Instead of writing functions where every class is handled as a branch (like in Listing 8.10.2), we create a function for every method. For every class, there is a table of functions (more specifically: function pointers). This table is called virtual method table, which is often abbreviated as VMT, vtable, or vtab. The VMT contains for each method of the class (and every super class) the address of the corresponding function. Since every class has its own VMT, the address of the VMT identifies the class uniquely. It corresponds to the “tag” op of the struct exp_t in Listing 8.10.2. Every object obtains during its initialization (where the concrete type is known) a pointer to the VMT of the class in a field that is hidden to the programmer (vtab in Listing 8.12.1). Since we need to access the VMT without knowing the concrete type, the pointer to the VMT is at the same position for every object (usually at offset 0). This allows us to identify the concrete type of any object at run time.

The address of the virtual method required at a given call site results from a two-step process: First, the right entry in the VMT needs to be determined. This information is given by the method name (and the signature, in case of method overloading) and can therefore be derived statically at compile time. In our example in Listing 8.12.1, the selected entry corresponds to a field in the ...vtab_t structs. Java's static semantics guarantees that there is an entry for the called method in every possible VMT. In the second step, the concrete VMT is determined. Since the VMT address has been written to the object's hidden vtab field during initialization, we only need to load this address at run time to obtain the right VMT.

One such call occurs in exp_add_eval in Listing 8.12.1 in the return statement. The expression l->vtab->eval determines the address of the overridden method eval via the VMT.

typedef struct {
    int  (*eval)(void* this, env_t const* e);
    void (*print)(void* this, FILE* f);
} exp_vtab_t;
typedef struct { exp_vtab_t* vtab; } exp_t;

// Var and Add have the same VMT, as they add no methods.
typedef exp_vtab_t exp_add_vtab_t;
typedef struct {
    exp_add_vtab_t const* vtab; exp_t* opnds[2]; } exp_add_t;

// Const has one more method. The methods need to be listed
// in the same order as in exp_vtab_t.
typedef struct {
    int  (*eval)(void* this, env_t const* e);
    void (*print)(void* this, FILE* f);
    int  (*get_value)(void* this);
} exp_const_vtab_t;
typedef struct {
  exp_const_vtab_t const* vtab; int value; } exp_const_t;

int exp_add_eval(void* this, env_t const *e) {
    exp_add_t* add = this;
    exp_t* l = add->opnds[0]; exp_t* r = add->opnds[1];
    return l->vtab->eval(l, e) + r->vtab->eval(r, e);

// VMT for Add
static const exp_add_vtab_t exp_add_vtab = {
    exp_add_eval, exp_add_print };

// Constructor for Add
exp_add_t* exp_init_add(exp_add_t* this, exp_t* l, exp_t* r) {
    this->vtab = &exp_add_vtab;
    this->opnds[0] = l; this->opnds[1] = r;
    return this;

int exp_const_eval(void* this, env_t const *e) {
    exp_const_t* t = this;
    return t->value;

// VMT for Const
static const exp_const_vtab_t exp_const_vtab = {
    exp_const_eval, exp_const_print, exp_const_get_value };

// Constructor for Const
exp_const_t* exp_init_const(exp_const_t* this, int value) {
    this->vtab  = &exp_const_vtab;
    this->value = value;
    return this;
Listing 8.12.1. “Object-oriented” variant in C for the example from Listing 8.10.3.