Question
What are the common ways to write object-oriented style code in C, especially when you need polymorphism?
In particular, how can C programs model ideas such as:
- objects with data and behavior
- encapsulation
- inheritance-like composition
- polymorphic behavior
Although C is not an object-oriented language, many codebases still use object-oriented design patterns. What practical techniques are used to achieve this in C?
Short Answer
By the end of this page, you will understand how C can emulate object-oriented programming using structs, functions, opaque types, and function pointers. You will learn how to represent objects, group data with behavior, hide implementation details, and build polymorphic interfaces in a way that is common in real C codebases.
Concept
C does not have built-in classes, inheritance, or virtual methods. However, it gives you low-level building blocks that let you model many object-oriented ideas manually.
The main techniques are:
structfor object state: astructholds the data for an object.- Functions for methods: regular functions operate on a pointer to that
struct. - Opaque pointers for encapsulation: users see only a forward declaration, not the internal fields.
- Function pointers for polymorphism: a table of function pointers can act like virtual methods.
- Composition for reuse: instead of inheritance, C often nests one struct inside another.
For example, in C you might represent a Shape with function pointers like area and draw. A Circle and a Rectangle can each provide their own implementations of those functions. Then code can work with any Shape through a common interface.
This matters because many systems programs, embedded projects, graphics libraries, kernels, and game engines are written in C but still need clean modular design. Object-oriented style in C helps organize code, reduce duplication, and support extensibility without requiring a different language.
Mental Model
Think of a C struct as a device, and function pointers as the buttons attached to it.
- The data fields inside the
structare the device's internal parts. - The functions are the actions the device can perform.
- A pointer to the struct is the actual device you are holding.
- A function pointer table is like a label that says, "When someone presses the
drawbutton, use this specific implementation."
So instead of a class with methods built into the language, you build the same idea yourself:
- data lives in a struct
- behavior lives in functions
- dynamic behavior is selected through function pointers
It is manual, but the design idea is very similar to object-oriented programming.
Syntax and Examples
In C, the most common object-oriented pattern looks like this:
#include <stdio.h>
typedef struct {
int x;
int y;
} Point;
void Point_move(Point *p, int dx, int dy) {
p->x += dx;
p->y += dy;
}
void Point_print(const Point *p) {
printf("Point(%d, %d)\n", p->x, p->y);
}
int main(void) {
Point p = {2, 3};
Point_print(&p);
Point_move(&p, 5, -1);
Point_print(&p);
return 0;
}
This gives you an object-like design:
Pointis the object dataPoint_moveandPoint_printare methods- the first parameter,
Point *p, plays the role ofthisin object-oriented languages
Adding polymorphism with function pointers
#include <stdio.h>
#include <stdlib.h>
typedef struct Shape Shape;
typedef struct {
double (*area)(const Shape *self);
void (*describe)(const Shape *self);
} ShapeVTable;
struct Shape {
const ShapeVTable *vtable;
};
typedef struct {
Shape base;
double radius;
} Circle;
double Circle_area(const Shape *self) {
const Circle *circle = (const Circle *)self;
return 3.14159 * circle->radius * circle->radius;
}
void Circle_describe(const Shape *self) {
const Circle *circle = (const Circle *)self;
printf("Circle(radius=%.2f)\n", circle->radius);
}
const ShapeVTable circle_vtable = {
Circle_area,
Circle_describe
};
int main(void) {
Circle c;
c.base.vtable = &circle_vtable;
c.radius = 4.0;
Shape *shape = (Shape *)&c;
shape->vtable->describe(shape);
printf("Area: %.2f\n", shape->vtable->area(shape));
return 0;
}
What this example shows
Shapeis the common base interfaceShapeVTablestores function pointers for shared operationsCirclecontainsShapeas its first field- a
Shape *can point to aCircle - calling through
shape->vtable->area(shape)gives polymorphic behavior
This is one of the closest C equivalents to virtual methods in class-based languages.
Step by Step Execution
Consider this small example:
#include <stdio.h>
typedef struct Animal Animal;
typedef struct {
void (*speak)(const Animal *self);
} AnimalVTable;
struct Animal {
const AnimalVTable *vtable;
};
typedef struct {
Animal base;
const char *name;
} Dog;
void Dog_speak(const Animal *self) {
const Dog *dog = (const Dog *)self;
printf("%s says woof!\n", dog->name);
}
const AnimalVTable dog_vtable = {
Dog_speak
};
int main(void) {
Dog d;
d.base.vtable = &dog_vtable;
d.name = "Rex";
Animal *a = (Animal *)&d;
a->vtable->speak(a);
return 0;
}
Step by step
AnimalVTabledefines one operation:speak.Animalcontains a pointer to its vtable.Dogstarts with anAnimal basefield, then addsname.Dog_speakreceivesconst Animal *selfbecause it matches the common interface.- Inside
Dog_speak, the pointer is cast back toconst Dog *so the function can accessdog->name. - In
main,d.base.vtable = &dog_vtable;connects this object to the dog implementation. Animal *a = (Animal *)&d;treats the dog as a generic animal.a->vtable->speak(a);looks up the correct function in the vtable and callsDog_speak.- The output is:
Rex says woof!
The important part is that the calling code only knows about Animal, but the correct Dog behavior still runs.
Real World Use Cases
Object-oriented style in C is useful when code needs a clear interface but different implementations.
Common use cases
- Device drivers
- Different hardware devices expose the same operations such as
init,read, andwrite.
- Different hardware devices expose the same operations such as
- GUI or graphics systems
- Different widgets or shapes may all support
draw,resize, orhandle_event.
- Different widgets or shapes may all support
- Parsers and compilers
- Different node types in an abstract syntax tree can support operations like
print,evaluate, orfree.
- Different node types in an abstract syntax tree can support operations like
- Networking libraries
- Different transport types may implement a common send/receive interface.
- Embedded systems
- Sensors, actuators, and communication modules often share a small interface but have different implementations.
- Game engines
- Entities may share operations such as
updateandrenderwhile storing different data.
- Entities may share operations such as
Why C developers use this style
- It keeps modules organized.
- It allows code to depend on interfaces instead of concrete implementations.
- It makes it easier to add new types without rewriting all calling code.
Real Codebase Usage
In real C projects, object-oriented style is usually applied selectively, not everywhere.
Common patterns
1. Opaque structs for encapsulation
In a header:
typedef struct FileReader FileReader;
FileReader *FileReader_create(const char *path);
void FileReader_destroy(FileReader *reader);
int FileReader_read_line(FileReader *reader, char *buffer, int size);
In the .c file:
struct FileReader {
FILE *fp;
int line_count;
};
This hides implementation details from users.
2. Constructor/destructor-style functions
C has no constructors, so code often uses:
create/destroyinit/cleanupopen/close
This makes ownership explicit.
3. Guard clauses and validation
int Shape_draw(const Shape *shape) {
if (shape == NULL || shape->vtable == NULL || shape->vtable->describe == NULL) {
return -1;
}
shape->vtable->describe(shape);
return 0;
}
This is important because C does not protect you from invalid pointers.
4. Interface structs with function pointers
Libraries often define a small API table:
typedef struct {
int (*open)(void *ctx);
int (*close)(void *ctx);
int (*read)(void *ctx, char *buf, int size);
} StreamOps;
This lets different backends plug into the same API.
5. Composition over inheritance
Instead of deep inheritance trees, C code often stores reusable components as fields inside structs.
typedef struct {
int x;
int y;
} Position;
typedef struct {
Position pos;
int health;
} Player;
This is simple, explicit, and easy to maintain.
Common Mistakes
1. Forgetting to initialize function pointers
Broken code:
Shape s;
s.vtable->area(&s); // undefined behavior
Why it fails:
s.vtablewas never assigned.
Fix:
s.vtable = &some_vtable;
2. Casting to the wrong derived type
Broken code:
const Rectangle *r = (const Rectangle *)self;
Why it fails:
- If
selfactually points to aCircle, the cast is invalid and leads to wrong memory access.
How to avoid it:
- Ensure the object's vtable matches the implementation being called.
- Keep object creation and vtable assignment tightly controlled.
3. Assuming C gives real inheritance
C does not understand inheritance rules. This pattern only works because you manually design memory layout and interfaces.
Avoid assumptions like:
- automatic method dispatch
- runtime type safety
- base-class destructors
You must implement these yourself.
4. Exposing struct internals when you want encapsulation
If users can access all fields directly, they can break invariants.
Instead of this in the header:
typedef struct {
int size;
char *buffer;
} Buffer;
Prefer an opaque type when internal details should be hidden.
5. Mixing ownership rules
Broken design:
- One function allocates memory
- Another function sometimes frees it
- The caller is not sure who owns the object
How to avoid it:
- Define clear ownership rules
- Use consistent naming such as
create/destroyorinit/cleanup
6. Overengineering small programs
Not every C program needs vtables and object hierarchies.
For simple tasks, plain structs and functions are usually better.
Use object-oriented patterns when they genuinely improve structure or extensibility.
Comparisons
| Concept | In C | In class-based OOP languages |
|---|---|---|
| Object data | struct | class instance fields |
| Method | regular function taking self pointer | member function / method |
| Encapsulation | opaque struct + header/source split | private / public |
| Polymorphism | function pointers / vtable | virtual methods / interfaces |
| Inheritance | manual composition or embedded base struct | built-in inheritance |
| Type safety | manual and limited | language enforced |
Function pointers vs direct function calls
| Approach | Best for | Trade-off |
|---|---|---|
| Direct function call | simple code with fixed behavior | not polymorphic |
| Function pointer | interchangeable implementations | more complexity |
Composition vs inheritance-like embedding
| Approach | Example | When to use |
|---|---|---|
| Composition | Player has a Position field | reuse data cleanly |
| Embedded base struct | Circle starts with Shape base | shared interface with polymorphism |
C-style OOP vs just procedural C
| Style | Good for | Limitation |
|---|---|---|
| Plain procedural C | straightforward programs | less flexible abstraction |
| OOP-style C | modular libraries and interchangeable implementations | more boilerplate |
Cheat Sheet
Quick reference
Object-like design in C
typedef struct {
int value;
} Counter;
void Counter_increment(Counter *c) {
c->value++;
}
- Use a
structfor data - Use functions for behavior
- Pass
StructName *selfas the first argument
Encapsulation
In header:
typedef struct MyType MyType;
MyType *MyType_create(void);
void MyType_destroy(MyType *obj);
In source:
struct MyType {
int hidden;
};
Polymorphism pattern
typedef struct Base Base;
typedef struct {
void (*do_work)(Base *self);
} BaseVTable;
struct Base {
const BaseVTable *vtable;
};
Derived type:
typedef struct {
Base base;
int extra;
} Derived;
Call polymorphically:
base_ptr->vtable->do_work(base_ptr);
Rules to remember
- Initialize every vtable pointer before use
- Keep the embedded base struct in a predictable place, usually first
- Use opaque types when hiding implementation details
- Use
create/destroyorinit/cleanupconsistently - Validate pointers before calling through function pointers
Edge cases
- A wrong cast can cause undefined behavior
- Uninitialized function pointers are dangerous
- C does not provide automatic destructors or runtime type checks
FAQ
How can C support object-oriented programming if it has no classes?
C does not support OOP directly, but you can build similar patterns using structs for data and functions for behavior.
How do you implement polymorphism in C?
The usual technique is to store function pointers in a struct, often called a vtable, and call behavior through that table.
Is inheritance possible in C?
Not as a language feature. You can simulate parts of it by embedding one struct inside another and designing a shared interface.
What is the C equivalent of a method?
A regular function that takes a pointer to the object as its first parameter.
How do you hide private fields in C?
Use an opaque struct: forward-declare the type in the header and define the full struct only in the source file.
Should all C code be written in an object-oriented style?
No. Many programs are clearer with simple procedural code. Use OOP-style patterns when you need modularity, encapsulation, or interchangeable implementations.
Are function pointers in C the same as virtual methods in C++?
They serve a similar purpose for dynamic dispatch, but in C you must build and manage the mechanism manually.
What is the biggest risk when emulating OOP in C?
Manual memory management and manual type handling. Mistakes can easily lead to undefined behavior.
Mini Project
Description
Build a small shape system in C that demonstrates object-oriented style design. You will create a common Shape interface and two implementations: Circle and Rectangle. Each shape will support the same operations, but each type will calculate its area differently. This mirrors how real C libraries use structs and function pointers to support interchangeable behavior.
Goal
Create a C program where different shape types can be stored and used through one shared interface, then print each shape and its area polymorphically.
Requirements
- Define a base
Shapetype with a vtable containingareaanddescribefunction pointers. - Create
CircleandRectanglestructs that includeShapeas a base field. - Implement type-specific
areaanddescribefunctions for each shape. - Store both shape types in an array of
Shape *and process them through the shared interface. - Print each shape description and computed area.
Keep learning
Related questions
2D Array Loop Order and Cache Performance in C
Learn why swapping nested loops changes 2D array performance in C, using row-major memory layout, cache locality, and practical benchmarks.
Array-to-Pointer Conversion in C and C++ Explained
Learn what array-to-pointer conversion means in C and C++, how array decay works, and how it differs from a pointer to an array.
Building More Fault-Tolerant Embedded C++ Applications for Radiation-Prone ARM Systems
Learn practical C++ and compile-time techniques to reduce soft-error damage in embedded ARM systems exposed to radiation.