OOP in C
Object-Oriented Programming in C: Yes, It’s Actually Possible
So you’ve been working with C for a while, and someone mentioned that you can do object-oriented programming in it. Your first reaction was probably “Wait, what? C doesn’t have classes!” And you’re right - C doesn’t have built-in OOP features like C++ or Java. But here’s the thing: OOP is more about concepts and patterns than specific language features.
In this post, I’ll show you how to implement OOP concepts in plain C. It’s not as crazy as it sounds, and it can actually make your C code more organized and maintainable.
Table of Contents
Why OOP in C?
Before we dive in, let’s talk about why you’d want to do this. C is great for system programming, but as your projects get bigger, you start running into some issues:
- Code becomes harder to organize
- You end up with lots of global functions
- It’s easy to accidentally mess with data you shouldn’t
- Code reuse becomes a pain
OOP helps solve these problems by giving you better ways to organize and structure your code. Plus, if you’re working on embedded systems or kernel code where C++ isn’t an option, these techniques can be lifesavers.
Encapsulation with Structs
Encapsulation is about bundling data and the functions that work on that data together. In C, we can simulate this using structs and careful organization.
Here’s a simple example - let’s create a “Counter” object:
1
2
3
4
5
6
7
8
9
10
11
12
13
// counter.h
typedef struct {
int value;
} Counter;
// Constructor
Counter* counter_create(int initial_value);
// Methods
void counter_increment(Counter* self);
void counter_decrement(Counter* self);
int counter_get_value(Counter* self);
void counter_destroy(Counter* self);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// counter.c
#include <stdlib.h>
#include "counter.h"
Counter* counter_create(int initial_value) {
Counter* self = malloc(sizeof(Counter));
if (self) {
self->value = initial_value;
}
return self;
}
void counter_increment(Counter* self) {
if (self) {
self->value++;
}
}
void counter_decrement(Counter* self) {
if (self) {
self->value--;
}
}
int counter_get_value(Counter* self) {
return self ? self->value : 0;
}
void counter_destroy(Counter* self) {
free(self);
}
Notice how we use self as the first parameter - this is our version of the this pointer from other languages. It’s just a convention, but it makes the code more readable.
Creating “Classes” with Function Pointers
We can take this further by embedding function pointers directly in our structs. This makes our objects more self-contained:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// Better counter with methods inside the struct
typedef struct {
int value;
// Method pointers
void (*increment)(struct Counter* self);
void (*decrement)(struct Counter* self);
int (*get_value)(struct Counter* self);
} Counter;
// Implementation functions
static void counter_increment_impl(Counter* self) {
self->value++;
}
static void counter_decrement_impl(Counter* self) {
self->value--;
}
static int counter_get_value_impl(Counter* self) {
return self->value;
}
// Constructor
Counter* counter_create(int initial_value) {
Counter* self = malloc(sizeof(Counter));
if (self) {
self->value = initial_value;
self->increment = counter_increment_impl;
self->decrement = counter_decrement_impl;
self->get_value = counter_get_value_impl;
}
return self;
}
Now you can use it like this:
1
2
3
Counter* my_counter = counter_create(10);
my_counter->increment(my_counter);
printf("Value: %d\n", my_counter->get_value(my_counter));
Inheritance Through Composition
C doesn’t have inheritance, but we can simulate it using composition. The trick is to put the “parent” struct as the first member of the “child” struct:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Base "Animal" class
typedef struct {
char name[50];
void (*make_sound)(struct Animal* self);
void (*destroy)(struct Animal* self);
} Animal;
// Dog "inherits" from Animal
typedef struct {
Animal base; // Must be first member!
char breed[30];
} Dog;
// Cat "inherits" from Animal
typedef struct {
Animal base; // Must be first member!
int lives_left;
} Cat;
The magic here is that because base is the first member, a pointer to a Dog can be cast to a pointer to an Animal safely. This lets us treat dogs and cats as animals.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// Dog implementation
static void dog_make_sound(Animal* self) {
Dog* dog = (Dog*)self;
printf("%s says: Woof! I'm a %s\n", self->name, dog->breed);
}
static void dog_destroy(Animal* self) {
free(self);
}
Dog* dog_create(const char* name, const char* breed) {
Dog* self = malloc(sizeof(Dog));
if (self) {
strcpy(self->base.name, name);
strcpy(self->breed, breed);
self->base.make_sound = dog_make_sound;
self->base.destroy = dog_destroy;
}
return self;
}
// Cat implementation
static void cat_make_sound(Animal* self) {
Cat* cat = (Cat*)self;
printf("%s says: Meow! I have %d lives left\n", self->name, cat->lives_left);
}
static void cat_destroy(Animal* self) {
free(self);
}
Cat* cat_create(const char* name, int lives) {
Cat* self = malloc(sizeof(Cat));
if (self) {
strcpy(self->base.name, name);
self->lives_left = lives;
self->base.make_sound = cat_make_sound;
self->base.destroy = cat_destroy;
}
return self;
}
Polymorphism with Function Pointers
Now here’s where it gets really cool. Because both Dog and Cat have Animal as their first member, we can treat them polymorphically:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void animal_demo() {
// Create different animals
Dog* buddy = dog_create("Buddy", "Golden Retriever");
Cat* whiskers = cat_create("Whiskers", 9);
// Store them as Animal pointers
Animal* animals[] = {
(Animal*)buddy,
(Animal*)whiskers
};
// Call methods polymorphically
for (int i = 0; i < 2; i++) {
animals[i]->make_sound(animals[i]);
}
// Clean up
for (int i = 0; i < 2; i++) {
animals[i]->destroy(animals[i]);
}
}
This will output:
1
2
Buddy says: Woof! I'm a Golden Retriever
Whiskers says: Meow! I have 9 lives left
A Complete Example: Shape System
Let’s put it all together with a more complete example - a shape drawing system:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// shape.h
typedef struct Shape Shape;
struct Shape {
double x, y; // Position
// Virtual methods
double (*area)(Shape* self);
void (*draw)(Shape* self);
void (*move)(Shape* self, double dx, double dy);
void (*destroy)(Shape* self);
};
// Rectangle
typedef struct {
Shape base;
double width, height;
} Rectangle;
Rectangle* rectangle_create(double x, double y, double width, double height);
// Circle
typedef struct {
Shape base;
double radius;
} Circle;
Circle* circle_create(double x, double y, double radius);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
// shape.c
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include "shape.h"
// Base shape methods
static void shape_move(Shape* self, double dx, double dy) {
self->x += dx;
self->y += dy;
}
// Rectangle implementation
static double rectangle_area(Shape* self) {
Rectangle* rect = (Rectangle*)self;
return rect->width * rect->height;
}
static void rectangle_draw(Shape* self) {
Rectangle* rect = (Rectangle*)self;
printf("Drawing rectangle at (%.1f,%.1f): %.1f x %.1f\n",
self->x, self->y, rect->width, rect->height);
}
static void rectangle_destroy(Shape* self) {
free(self);
}
Rectangle* rectangle_create(double x, double y, double width, double height) {
Rectangle* self = malloc(sizeof(Rectangle));
if (self) {
self->base.x = x;
self->base.y = y;
self->width = width;
self->height = height;
// Set up virtual function table
self->base.area = rectangle_area;
self->base.draw = rectangle_draw;
self->base.move = shape_move;
self->base.destroy = rectangle_destroy;
}
return self;
}
// Circle implementation
static double circle_area(Shape* self) {
Circle* circle = (Circle*)self;
return M_PI * circle->radius * circle->radius;
}
static void circle_draw(Shape* self) {
Circle* circle = (Circle*)self;
printf("Drawing circle at (%.1f,%.1f): radius %.1f\n",
self->x, self->y, circle->radius);
}
static void circle_destroy(Shape* self) {
free(self);
}
Circle* circle_create(double x, double y, double radius) {
Circle* self = malloc(sizeof(Circle));
if (self) {
self->base.x = x;
self->base.y = y;
self->radius = radius;
// Set up virtual function table
self->base.area = circle_area;
self->base.draw = circle_draw;
self->base.move = shape_move;
self->base.destroy = circle_destroy;
}
return self;
}
Usage:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
int main() {
// Create different shapes
Rectangle* rect = rectangle_create(10, 20, 5, 3);
Circle* circ = circle_create(0, 0, 4);
// Store as base Shape pointers
Shape* shapes[] = {
(Shape*)rect,
(Shape*)circ
};
// Use polymorphically
for (int i = 0; i < 2; i++) {
shapes[i]->draw(shapes[i]);
printf("Area: %.2f\n", shapes[i]->area(shapes[i]));
shapes[i]->move(shapes[i], 1, 1);
shapes[i]->draw(shapes[i]);
printf("\n");
}
// Clean up
for (int i = 0; i < 2; i++) {
shapes[i]->destroy(shapes[i]);
}
return 0;
}
When to Use This Approach
This OOP style in C is powerful, but it’s not always the right choice. Here’s when you should consider it:
Good for:
- Large, complex systems where organization matters
- When you need polymorphism (like plugin systems)
- Code that multiple people will work on
- When you’re modeling real-world entities with clear relationships
Maybe avoid for:
- Simple, straightforward programs
- Performance-critical code (function pointers have overhead)
- When the C standard library approach works fine
Tips for success:
- Keep your interfaces simple
- Always check for NULL pointers
- Be consistent with naming conventions
- Don’t over-engineer - sometimes a simple struct and functions are enough
Wrapping Up
OOP in C isn’t as weird as it first seems. You’re basically using the same concepts that higher-level languages give you, just implemented manually. It takes more work, but it gives you fine control over memory layout and behavior.
The key insight is that OOP is about organizing code and data, not about specific language features. With structs, function pointers, and some careful design, you can build surprisingly sophisticated object systems in C.
Just remember - use these techniques when they make your code clearer and more maintainable. Don’t use them just because you can. Sometimes the simple C approach is still the best approach.