OOP#

C has no classes, no inheritance, no methods. Object orientation in C is built from primitives the operator already has: struct for state, function pointers for behaviour (vtables), a self first parameter to thread the receiver, and opaque types to hide internal layout.

For the underlying function-pointer mechanics, see Functions. For struct memory layout, see Types.

The pattern#

A “class” is a struct with state plus a vtable of function pointers; the methods take a self pointer as their first argument.

/* counter.h */
typedef struct counter counter_t;

counter_t *counter_new(void);
void       counter_inc(counter_t *self);
int        counter_get(const counter_t *self);
void       counter_free(counter_t *self);

/* counter.c */
struct counter { int n; };

counter_t *counter_new(void) {
    counter_t *c = calloc(1, sizeof *c);
    return c;
}

void counter_inc(counter_t *self)        { self->n++; }
int  counter_get(const counter_t *self)  { return self->n; }
void counter_free(counter_t *self)       { free(self); }

The header exposes an opaque handle (typedef struct counter counter_t; without a struct body); only the source file knows the layout. The caller cannot read or write fields directly; everything goes through the function API.

Constructors and destructors#

By convention, a function named foo_new allocates and initialises; foo_free releases. Every new pairs with a free somewhere.

counter_t *c = counter_new();
counter_inc(c);
counter_free(c);

For constructors that can fail, return NULL and let the caller branch.

counter_t *counter_new(void) {
    counter_t *c = calloc(1, sizeof *c);
    if (!c) return NULL;
    /* … */
    return c;
}

vtables#

When you want polymorphic behaviour, a vtable of function pointers inside the struct dispatches at runtime.

typedef struct shape shape_t;

typedef struct {
    double (*area)(const shape_t *self);
    void   (*free)(shape_t *self);
} shape_vtable_t;

struct shape {
    const shape_vtable_t *vt;
    /* concrete subclasses add their own fields below */
};

double shape_area(const shape_t *s)  { return s->vt->area(s); }
void   shape_free(shape_t *s)        { s->vt->free(s); }

A concrete subtype.

typedef struct {
    shape_t base;
    double r;
} circle_t;

static double circle_area(const shape_t *s) {
    const circle_t *c = (const circle_t*)s;
    return 3.14159265 * c->r * c->r;
}
static void circle_free(shape_t *s) { free(s); }

static const shape_vtable_t circle_vt = {
    .area = circle_area,
    .free = circle_free,
};

circle_t *circle_new(double r) {
    circle_t *c = calloc(1, sizeof *c);
    c->base.vt = &circle_vt;
    c->r = r;
    return c;
}

The caller uses shape_area and never sees circle vs square; this is the “interface” mechanism.

Inheritance by embedding#

Embedding a parent struct as the first member lets the operator cast a child pointer back to the parent. The standard library does this for FILE; the kernel does it everywhere.

typedef struct { int kind; } event_t;

typedef struct {
    event_t base;
    int x, y;
} click_event_t;

void handle(event_t *e) {
    if (e->kind == EV_CLICK) {
        click_event_t *c = (click_event_t*)e;     /* "downcast" */
        process_click(c->x, c->y);
    }
}

Casting up (child to parent) is safe because of the first-member guarantee; casting down is safe only when the operator has already checked the discriminant.

Encapsulation#

Three levels.

  • Header / source split: declarations in .h, definitions in .c. Anyone who includes the header sees the API.

  • Static functions: helpers marked static are visible only inside the source file.

  • Opaque types: the public header declares the struct tag but never the body. Callers can hold pointers but cannot dereference fields.

/* public.h */
typedef struct conn conn_t;
conn_t *conn_open(const char *url);

/* public.c */
struct conn { int sock; const char *url; };

Resource ownership#

C has no RAII; thread ownership through the API explicitly.

  • Caller owns the result: the function returns a pointer the caller must free.

  • Callee owns the input: the operator passes a pointer the callee may keep; the caller must not free it.

  • Borrow: the operator passes a pointer for the duration of the call only; neither side frees.

Document the convention on every API; the compiler does not help. __attribute__((cleanup(fn))) (GCC) gives a scope-bound destructor.

static void cleanup_conn(conn_t **p) { if (*p) conn_close(*p); }
#define _cleanup_conn_ __attribute__((cleanup(cleanup_conn)))

void use(void) {
    _cleanup_conn_ conn_t *c = conn_open(url);
    /* c is closed automatically on scope exit */
}

References#