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.