Reputation: 110382
First, this is merely an academic question. I know C is not the job for doing OOP programming, but this is more of a learning exercise for a beginner learning what's possible and what's not (or perhaps, what might be possible but is not a good idea).
Let's take the following as a starting place, where I have two different objects but I want to give each of them the same two methods: create
and print
. I've omitted any error checking, freeing, etc. just to simplify matters:
#include <stdio.h>
#include <stdlib.h>
struct Person {
char* name;
int age;
};
struct Car {
char* make;
char* model;
int year;
};
struct Person* create_person(void)
{
struct Person *new = malloc(sizeof (struct Person));
return new;
}
void print_person(struct Person* person)
{
printf("<Person: %s (%d)>\n", person->name, person->age);
}
struct Car* create_car(void)
{
struct Car *new = malloc(sizeof (struct Car));
return new;
}
void print_car(struct Car* car)
{
printf("<Car: %s - %s (%d)>\n", car->make, car->model, car->year);
}
int main(void)
{
struct Car *car = create_car();
*car = (struct Car) {.make="Chevy", .model="Eldorado", .year=2015};
print_car(car);
struct Person *person = create_person();
*person = (struct Person) {.name="Tom", .age=30};
print_person(person);
}
I would think that the first part would be to group the 'methods' into the struct itself. So then we would have:
#include <stdio.h>
#include <stdlib.h>
struct Person {
char* name;
int age;
void (*print)(struct Person*);
};
struct Car {
char* make;
char* model;
int year;
void (*print)(struct Car*);
};
void print_car(struct Car* car);
void print_person(struct Person* person);
struct Person* create_person(void)
{
struct Person *new = malloc(sizeof (struct Person));
return new;
}
void print_person(struct Person* person)
{
printf("<Person: %s (%d)>\n", person->name, person->age);
}
struct Car* create_car(void)
{
struct Car *new = malloc(sizeof (struct Car));
return new;
}
void print_car(struct Car* car)
{
printf("<Car: %s - %s (%d)>\n", car->make, car->model, car->year);
}
int main(void)
{
struct Car *car = create_car();
*car = (struct Car) {.make="Chevy", .model="Eldorado", .year=2015, .print=print_car};
car->print(car);
struct Person *person = create_person();
*person = (struct Person) {.name="Tom", .age=30, .print=print_person};
person->print(person);
}
What would be the next step in making it "more OOP like"? Perhaps using preprocessor glue and generics? What would be an example of the most OOP-like that the two objects could become? Again, I know this isn't what C is meant for, but it's more a learning experience.
Upvotes: 2
Views: 171
Reputation: 14157
You could use approach applied by the Linux kernel. The implementation of OOP is based using a composition for inheritance, and embedding interfaces into new classes.
The macro container_of
lets easily alternate between a pointer to class and a pointer to one of its members. Usually an embedded object will be an interface of the class. To find more details about container_of
macro see my answer to other but related question.
https://stackoverflow.com/a/66429587/4989451
This methodology was used to create a huge complex object oriented software, i.e. Linux kernel.
In the examples from the question, the Car and Person classes use an printing interface that we can call struct Printable
. I strongly suggest to produce a fully initialized objects in create_...
functions. Let it make a copy of all strings. Moreover, you should add destroy_...
methods to release resources allocated by create_...
.
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <stddef.h>
#define container_of(ptr, type, member) \
(type*)(void*)((char*)ptr - offsetof(type, member))
struct Printable {
void (*print)(struct Printable*);
};
struct Person {
char* name;
int age;
struct Printable printable;
};
void print_person(struct Printable *printable) {
struct Person *p = container_of(printable, struct Person, printable);
printf("<Person: %s (%d)>\n", p->name, p->age);
}
struct Person *create_person(char *name, int age) {
struct Person *p = malloc(sizeof *p);
p->name = strdup(name);
p->age = age;
p->printable.print = print_person;
return p;
}
struct Car {
char* make;
char* model;
int year;
struct Printable printable;
};
void print_car(struct Printable *printable) {
struct Car *c = container_of(printable, struct Car, printable);
printf("<Car: %s - %s (%d)>\n", c->make, c->model, c->year);
}
struct Car *create_car(char *make, char *model, int year) {
struct Car *c = malloc(sizeof *c);
c->make = strdup(make);
c->model = strdup(model);
c->year = year;
c->printable.print = print_car;
return c;
}
void print(struct Printable *printable) {
printable->print(printable);
}
int main() {
struct Car *car = create_car("Chevy", "Eldorado", 2015);
struct Person *person = create_person("Tom", 30);
print(&car->printable);
print(&person->printable);
return 0;
}
produces output:
<Car: Chevy - Eldorado (2015)>
<Person: Tom (30)>
Note that function print()
takes a pointer to the Printable
interface. The function does not need to know anything about the original class. No preprocessor is used. All casts are done on "library" side, not on clients side. The library initializes the Printable interface, therefore it cannot be misused.
You can easily add other base classes or interfaces like i.e. RefCounted to solve memory management. The i-face would contain a pointer to destructor and refcount itself. Other examples are intrusive linked lists or binary trees.
Upvotes: 3