David542
David542

Reputation: 110382

Making two objects in C more object-oriented

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

Answers (1)

tstanisl
tstanisl

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

Related Questions