Object-Oriented Programming in C: Implementing Classes, Inheritance, and Polymorphism

Class Structure in C

Implementing object-oriented concepts in C requires a structured approach using structs and function pointers. A class consists of two primary components:

  • Instance Type: A struct containing data members (instance variables) and function pointers (instance methods). Variables of this type are called instances.
  • Class Object: A global const struct containing class variables and methods that belong to the class itself, not to any specific instance.

For a class named Vector2D, the instance type would be struct Vector2D and the class object would be Vector2D. The interface goes in Vector2D.h and implementation in Vector2D.c.

Vector2D.h:

struct Vector2D {
    // Instance variables and methods
};

extern const struct Vector2DClass {
    // Class methods
} Vector2D;

Vector2D.c:

#include "Vector2D.h"

const struct Vector2DClass Vector2D = {
    // Class method implementations
};

Implementing Constructors

Constructors initialize instances when they're declared. They must be class methods and typically return an instance type or a pointer to it.

For a Vector2D class with x and y components and a constructor named create:

Vector2D.h:

struct Vector2D {
    double x, y;
};

extern const struct Vector2DClass {
    struct Vector2D (*create)(double x_coord, double y_coord);
} Vector2D;

Vector2D.c:

#include "Vector2D.h"

static struct Vector2D create(double x_coord, double y_coord) {
    return (struct Vector2D){.x = x_coord, .y = y_coord};
}

const struct Vector2DClass Vector2D = {
    .create = &create
};

Usage:

struct Vector2D v = Vector2D.create(3.0, -4.0);

Adding Instance Methods

Instance methods are function pointers within the instance struct. They must receive a pointer to the instance (typically named this) as their first parameter to access instance data.

Adding a magnitude method to calculate vector length:

Vector2D.h:

struct Vector2D {
    double x, y;
    double (*magnitude)(struct Vector2D *this);
};

extern const struct Vector2DClass {
    struct Vector2D (*create)(double x_coord, double y_coord);
} Vector2D;

Vector2D.c:

#include "Vector2D.h"
#include <math.h>

static double magnitude(struct Vector2D *this) {
    return sqrt(this->x * this->x + this->y * this->y);
}

static struct Vector2D create(double x_coord, double y_coord) {
    return (struct Vector2D){
        .x = x_coord,
        .y = y_coord,
        .magnitude = &magnitude
    };
}

const struct Vector2DClass Vector2D = {
    .create = &create
};

Usage:

struct Vector2D v = Vector2D.create(3.0, -4.0);
printf("Length: %g\n", v.magnitude(&v));

Implementing Inheritance

Inheritance is achieved by embedding the base struct as the first member of the derived struct. Method overriding provides polymorphism.

Consider a Person base class and Employee derived class:

Person.h:

struct Person {
    const char *name;
    char *(*get_description)(struct Person *this, char *buffer, size_t size);
};

extern const struct PersonClass {
    struct Person (*create)(const char *name);
} Person;

Person.c:

#include "Person.h"
#include <string.h>
#include <stdio.h>

static char *get_description(struct Person *this, char *buffer, size_t size) {
    snprintf(buffer, size, "Person: %s", this->name);
    return buffer;
}

static struct Person create(const char *name) {
    return (struct Person){
        .name = strdup(name),
        .get_description = &get_description
    };
}

const struct PersonClass Person = {
    .create = &create
};

Employee.h:

#include "Person.h"

struct Employee {
    struct Person person;
    const char *department;
};

extern const struct EmployeeClass {
    struct Employee (*create)(const char *name, const char *dept);
} Employee;

Employee.c:

#include "Employee.h"
#include <stddef.h>

static char *get_description(struct Person *base, char *buffer, size_t size) {
    struct Employee *this = (struct Employee *)((void *)base - offsetof(struct Employee, person));
    snprintf(buffer, size, "Employee: %s, Department: %s", 
             base->name, this->department);
    return buffer;
}

static struct Employee create(const char *name, const char *dept) {
    struct Employee result = {.department = dept};
    result.person = Person.create(name);
    result.person.get_description = &get_description;
    return result;
}

const struct EmployeeClass Employee = {
    .create = &create
};

Usage demonstrating polymorphism:

struct Employee emp = Employee.create("Alice", "Engineering");
struct Person *poly = &emp.person;
char buffer[100];
printf("%s\n", poly->get_description(poly, buffer, sizeof(buffer)));

Access Control Conventions

Since C lacks built-in access modifiers, use comments to indicate visibility:

struct Vector2D {
    double x, y;  // public
    
    // protected:
    int reference_count;
    
    // private:
    void *internal_data;
};

Abstract Classes and Interfaces

Abstract classes in C are conceptual - rely on documentation and null-initialized method pointers:

/* interface */
struct Drawable {
    void (*draw)(struct Drawable *this);
};

/* abstract class */
struct Shape {
    struct Drawable drawable;
    double (*area)(struct Shape *this);  // Must be implemented
};

Namespacing Techniques

Implement namespacing through prefix conventions and file organization. For a math_utils_vector namespace:

File location: math/utils/vector.h

#ifndef MATH_UTILS_VECTOR_H
#define MATH_UTILS_VECTOR_H

struct math_utils_vector {
    double components[3];
    double (*length)(struct math_utils_vector *this);
};

extern const struct math_utils_vector_class {
    struct math_utils_vector (*create)(double x, double y, double z);
} math_utils_vector;

#endif

Usage with alias:

#include "math/utils/vector.h"
#define Vector math_utils_vector

struct Vector v = Vector.create(1.0, 2.0, 3.0);

Complete Example: Queue Implementation

A practical example showing a bounded queue extending a basic queue interface.

queue.h:

#ifndef QUEUE_H
#define QUEUE_H

struct queue_element {};

#define QUEUE_MAX_SIZE 256

struct queue {
    void (*enqueue)(struct queue *this, struct queue_element *item);
    struct queue_element *(*dequeue)(struct queue *this);
    
    // protected:
    int size;
    struct queue_element *items[QUEUE_MAX_SIZE];
};

extern const struct queue_class {
    struct queue (*create)(void);
} queue;

#endif

bounded_queue.h:

#include "queue.h"

struct bounded_queue {
    struct queue base;
};

extern const struct bounded_queue_class {
    struct bounded_queue (*create)(int max_items);
} bounded_queue;

bounded_queue.c:

#include "bounded_queue.h"
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>

static int max_capacity;
static void (*base_enqueue)(struct queue *this, struct queue_element *item);
static struct queue_element *(*base_dequeue)(struct queue *this);

static void enqueue(struct queue *base, struct queue_element *item) {
    if (base->size >= max_capacity) {
        fprintf(stderr, "Queue capacity exceeded\n");
        exit(EXIT_FAILURE);
    }
    base_enqueue(base, item);
}

static struct queue_element *dequeue(struct queue *base) {
    if (base->size <= 0) {
        fprintf(stderr, "Cannot dequeue from empty queue\n");
        exit(EXIT_FAILURE);
    }
    return base_dequeue(base);
}

static struct bounded_queue create(int max_items) {
    max_capacity = max_items;
    struct bounded_queue result;
    result.base = queue.create();
    
    base_enqueue = result.base.enqueue;
    result.base.enqueue = &enqueue;
    
    base_dequeue = result.base.dequeue;
    result.base.dequeue = &dequeue;
    
    return result;
}

const struct bounded_queue_class bounded_queue = {
    .create = &create
};

test.c:

#include "bounded_queue.h"
#include <stdio.h>

struct message {
    struct queue_element element;
    const char *content;
};

int main(void) {
    struct bounded_queue bq = bounded_queue.create(3);
    struct message m1 = {.content = "First"};
    struct message m2 = {.content = "Second"};
    
    bq.base.enqueue(&bq.base, &m1.element);
    bq.base.enqueue(&bq.base, &m2.element);
    
    struct message *retrieved = (struct message *)bq.base.dequeue(&bq.base);
    printf("Message: %s\n", retrieved->content);
    
    // This will trigger the capacity check
    bq.base.dequeue(&bq.base);
    bq.base.dequeue(&bq.base);
    bq.base.dequeue(&bq.base);
    
    return 0;
}

This comprehensive approach enables robust object-oriented programming in C while maintaining type safety and clear code organization.

Tags: c programming Object-Oriented Programming Structures Function Pointers Inheritance

Posted on Fri, 26 Jun 2026 17:46:33 +0000 by Steveo31