Hands-On Object-Oriented C: From Structs to Design Patterns

Hands-On Object-Oriented C: From Structs to Design Patterns

C is often seen as a procedural language, but with careful design you can apply object-oriented (OO) principles—encapsulation, abstraction, polymorphism, and modularity—directly in C. This article walks through practical techniques that transform plain C programs into maintainable, testable, and reusable systems using structs, function pointers, and established design patterns.

Why use OO techniques in C?

  • Resource control: C gives deterministic memory and performance characteristics critical in embedded and systems programming.
  • Portability: C compiles everywhere—bringing OO structure without depending on C++ runtime or language features.
  • Incremental adoption: You can introduce OO idioms to existing C codebases progressively.

Core building blocks

1. Encapsulation with structs and opaque types

Encapsulation hides implementation details from users of a module.

  • Public header (mytype.h):
    • Declare an opaque pointer type: typedef struct MyType MyType;
    • Provide constructor/destructor and public methods.
  • Private source (mytype.c):
    • Define struct MyType { /fields */ };
    • Implement methods operating on MyType *.

This prevents callers from directly depending on internal fields and allows changing internals without breaking API.

2. Methods as functions with a “this” parameter

C functions emulate methods by taking a pointer to the instance as the first argument.

Example:

Code

void mytype_set_value(MyType *self, int v); int mytype_get_value(const MyType *self);

Use consistent naming (modulemethod) to avoid symbol collisions.

3. Constructors, destructors, and ownership

Provide creation and destruction functions:

Code

MyType *mytype_new(void); void mytype_free(MyType *self);

Clearly document ownership semantics (who frees memory). For safer code, pair each new with a matching free and prefer stack-based small objects when possible.

4. Inheritance-like composition

C lacks inheritance, but composition achieves code reuse:

  • Embed a “base” struct as the first field of a “derived” struct so pointer casting approximates polymorphism.
  • Alternatively, include a pointer to a base instance.

Example:

Code

typedef struct { int type_id;

void (*destroy)(void *); 

} Base;

typedef struct {

Base base; int derived_field; 

} Derived;

5. Polymorphism via function pointers (vtable)

Emulate virtual methods with a table of function pointers per type (vtable).

Pattern:

  • Define a vtable struct with function pointers.
  • Each instance stores a pointer to its type’s vtable.
  • Call methods via vtable to dispatch at runtime.

Example:

Code

typedef struct ShapeVTable { void (*draw)(void *self);

double (*area)(void *self); 

} ShapeVTable;

typedef struct {

ShapeVTable *vtable; // shape data 

} Shape;

This enables multiple concrete shapes (circle, rectangle) to implement the same interface.

Practical patterns and examples

Factory pattern

Encapsulate object creation logic in a factory function, returning an abstract type pointer. Use when creation varies by configuration.

Singleton pattern

For global resources, provide a function that returns a single shared instance. In C, ensure thread-safety with static initialization or synchronization primitives.

Strategy pattern

Encapsulate algorithms behind function-pointer-based interfaces to swap behavior at runtime (e.g., sorting strategies, logging backends).

Observer pattern

Implement subscription lists of function pointers for event notifications. Carefully manage lifetimes; prefer weak references or explicit unsubscribe to avoid dangling callbacks.

Adapter and Facade
  • Adapter wraps incompatible interfaces by translating calls.
  • Facade exposes a simplified API that composes multiple subsystems.

These are especially useful for modernizing legacy C APIs.

Memory safety and error handling

  • Prefer explicit error returns (int, enum, or pointer with NULL as failure).
  • Check all allocations; avoid silent failures.
  • Use RAII-like helper functions where possible (e.g., init/free pairs).
  • Consider reference counting for shared objects; implement atomic operations if multithreaded.

Testability and modularity

  • Design modules with small, pure functions where possible.
  • Use opaque types to write unit tests that exercise the public API without relying on internals.
  • Mock dependencies by swapping vtables or function pointers in tests.

Example: Minimal OO-style logger

Header (logger.h):

  • Opaque Logger type
  • Logger *logger_new(void (*write)(const char *));
  • void logger_log(Logger *l, const char *msg);
  • void logger_free(Logger *l);

Implementation:

  • Store function pointer write in struct, and call it from logger_log.
  • Swap write in tests to capture messages.

Performance considerations

  • Indirection from function pointers adds minimal overhead; acceptable in most applications but measure if in tight loops.
  • Inline small functions to reduce call overhead.
  • Keep data layout cache-friendly; prefer contiguous arrays and minimize pointer chasing.

When not to use OO in C

  • Small scripts or one-off programs where plain procedural code is simpler.
  • Performance-critical inner loops where virtual dispatch overhead is measurable and significant.

Quick checklist to refactor procedural C into OO-style

  1. Identify cohesive data + operations and make them modules.
  2. Create opaque types and public APIs.
  3. Move data into private structs.
  4. Add constructors/destructors and document ownership.
  5. Introduce vtables for polymorphism where needed.
  6. Write unit tests for public APIs.
  7. Review performance and simplify where necessary.

Conclusion Applying object-oriented design in C gives many structural benefits while retaining C’s control and portability. With opaque types, function-pointer dispatch, and careful module boundaries you can build clear, extensible systems without moving to a heavier language.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *