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
staticare 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 */
}