There are many different ways that you can do this.
Usually, defining a “common” struct for common information that also has a type
field.
Option #1:
Here’s a version that uses void *
pointers and a switch
in the print function:
#include <stdio.h>
#include <stdlib.h>
typedef struct Common {
int type;
const char *name;
} Common;
enum {
TYPE_CAR,
TYPE_ANIMAL,
};
typedef struct Car {
Common comm;
unsigned int cost;
} Car;
typedef struct Animal {
Common comm;
unsigned int age;
unsigned int weight;
} Animal;
Car *
car_new(const char *name,int cost)
{
Car *car = malloc(sizeof(*car));
car->comm.name = name;
car->comm.type = TYPE_CAR;
car->cost = cost;
return car;
}
void
car_print(void *obj)
{
Car *car = obj;
printf("The cost is: %d\n",car->cost);
}
Animal *
animal_new(const char *name,int age,int weight)
{
Animal *animal = malloc(sizeof(*animal));
animal->comm.name = name;
animal->comm.type = TYPE_ANIMAL;
animal->age = age;
animal->weight = weight;
return animal;
}
void
animal_print(void *obj)
{
Animal *animal = obj;
printf("The age is: %d\n",animal->age);
printf("The weight is: %d\n",animal->weight);
}
void
print_struct(void *obj)
{
Common *comm = obj;
printf("The name is: %s\n", comm->name);
switch (comm->type) {
case TYPE_ANIMAL:
animal_print(obj);
break;
case TYPE_CAR:
car_print(obj);
break;
}
}
int
main(void)
{
Animal *animal = animal_new("Dog",10,200);
Car *car = car_new("Ford",50000);
print_struct(animal);
print_struct(car);
return 0;
};
Option #2:
Passing around a void *
pointer isn’t as type safe as it could be.
Here’s a version that uses Common *
pointers and a switch
in the print function:
#include <stdio.h>
#include <stdlib.h>
typedef struct Common {
int type;
const char *name;
} Common;
enum {
TYPE_CAR,
TYPE_ANIMAL,
};
typedef struct Car {
Common comm;
unsigned int cost;
} Car;
typedef struct Animal {
Common comm;
unsigned int age;
unsigned int weight;
} Animal;
Common *
car_new(const char *name,int cost)
{
Car *car = malloc(sizeof(*car));
car->comm.name = name;
car->comm.type = TYPE_CAR;
car->cost = cost;
return (Common *) car;
}
void
car_print(Common *obj)
{
Car *car = (Car *) obj;
printf("The cost is: %d\n",car->cost);
}
Common *
animal_new(const char *name,int age,int weight)
{
Animal *animal = malloc(sizeof(*animal));
animal->comm.name = name;
animal->comm.type = TYPE_ANIMAL;
animal->age = age;
animal->weight = weight;
return (Common *) animal;
}
void
animal_print(Common *obj)
{
Animal *animal = (Animal *) obj;
printf("The age is: %d\n",animal->age);
printf("The weight is: %d\n",animal->weight);
}
void
print_struct(Common *comm)
{
printf("The name is: %s\n", comm->name);
switch (comm->type) {
case TYPE_ANIMAL:
animal_print(comm);
break;
case TYPE_CAR:
car_print(comm);
break;
}
}
int
main(void)
{
Common *animal = animal_new("Dog",10,200);
Common *car = car_new("Ford",50000);
print_struct(animal);
print_struct(car);
return 0;
};
Option #3:
Here’s a version that uses a virtual function callback table:
#include <stdio.h>
#include <stdlib.h>
typedef struct Vtable Vtable;
typedef struct Common {
int type;
Vtable *vtbl;
const char *name;
} Common;
enum {
TYPE_CAR,
TYPE_ANIMAL,
};
typedef struct Car {
Common comm;
unsigned int cost;
} Car;
void car_print(Common *obj);
typedef struct Animal {
Common comm;
unsigned int age;
unsigned int weight;
} Animal;
void animal_print(Common *obj);
typedef struct Vtable {
void (*vtb_print)(Common *comm);
} Vtable;
void
car_print(Common *obj)
{
Car *car = (Car *) obj;
printf("The cost is: %d\n",car->cost);
}
Vtable car_vtbl = {
.vtb_print = car_print
};
Common *
car_new(const char *name,int cost)
{
Car *car = malloc(sizeof(*car));
car->comm.name = name;
car->comm.type = TYPE_CAR;
car->comm.vtbl = &car_vtbl;
car->cost = cost;
return (Common *) car;
}
Vtable animal_vtbl = {
.vtb_print = animal_print
};
void
animal_print(Common *obj)
{
Animal *animal = (Animal *) obj;
printf("The age is: %d\n",animal->age);
printf("The weight is: %d\n",animal->weight);
}
Common *
animal_new(const char *name,int age,int weight)
{
Animal *animal = malloc(sizeof(*animal));
animal->comm.name = name;
animal->comm.type = TYPE_ANIMAL;
animal->comm.vtbl = &animal_vtbl;
animal->age = age;
animal->weight = weight;
return (Common *) animal;
}
void
print_struct(Common *comm)
{
printf("The name is: %s\n", comm->name);
comm->vtbl->vtb_print(comm);
}
int
main(void)
{
Common *animal = animal_new("Dog",10,200);
Common *car = car_new("Ford",50000);
print_struct(animal);
print_struct(car);
return 0;
};
UPDATE:
wow, that’s such a great answer thanks for putting in all the time. All three approaches are a bit over my head for where I’m at now…
You’re welcome. I notice you have a fair bit of python
experience. Pointers (et. al.) are a bit of an alien concept that can take some time to master. But, once you do, you’ll wonder how you got along without them.
But …
If there was one approach out of the three that you’d suggest to start with, which would it be?
Well, by a process of elimination …
Because option #1 uses void *
pointers, I’d eliminate that because option #2 is similar but has some type safety.
I’d eliminate option #2 because the switch
approach requires that the generic/common functions have to “know” about all the possible types (i.e.) we need each common function to have a switch
and it has to have a case
for each possible type. So, it’s not very extensible or scalable.
That leaves us with option #3.
Note that what we’ve been doing is somewhat similar to what c++
does for inherited classes [albeit with in a somewhat more verbose manner].
What is called Common
here would be termed the “base” class. Car
and Animal
would be “derived” classes of Common
.
In such a situation, c++
would [invisibly] place the Vtable
pointer as a transparent/hidden first element of the struct. It would handle all the magic with selecting the correct functions.
As a further reason as to why a Vtable
is a good idea, it makes it easy to add new functions/functionality to the structs.
For such an amorphous/heterogeneous collection, a good way to organize this is with a doubly linked list.
Then, once we have a list, we often wish to sort it.
So, I’ve created another version that implements a simple doubly linked list struct.
And, I’ve added a simple/crude/slow function to sort the list. To be able to compare different types, I added a Vtable
entry to compare list items.
Thus, it’s easy to add new functions. And, we can add new types easily enough.
… Be careful what you wish for–you may actually get it 🙂
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct Vtable Vtable;
typedef struct Common {
int type;
Vtable *vtbl;
struct Common *prev;
struct Common *next;
const char *name;
} Common;
typedef struct List {
Common *head;
Common *tail;
int count;
} List;
enum {
TYPE_CAR,
TYPE_ANIMAL,
};
typedef struct Car {
Common comm;
unsigned int cost;
} Car;
void car_print(const Common *obj);
int car_compare(const Common *lhs,const Common *rhs);
typedef struct Animal {
Common comm;
unsigned int age;
unsigned int weight;
} Animal;
void animal_print(const Common *obj);
int animal_compare(const Common *lhs,const Common *rhs);
typedef struct Vtable {
void (*vtb_print)(const Common *comm);
int (*vtb_compare)(const Common *lhs,const Common *rhs);
} Vtable;
void
car_print(const Common *obj)
{
Car *car = (Car *) obj;
printf("The cost is: %d\n",car->cost);
}
int
car_compare(const Common *lhsp,const Common *rhsp)
{
const Car *lhs = (const Car *) lhsp;
const Car *rhs = (const Car *) rhsp;
int cmp;
cmp = lhs->cost - rhs->cost;
return cmp;
}
Vtable car_vtbl = {
.vtb_print = car_print,
.vtb_compare = car_compare
};
Common *
car_new(const char *name,int cost)
{
Car *car = malloc(sizeof(*car));
Common *comm = &car->comm;
comm->name = name;
comm->type = TYPE_CAR;
comm->vtbl = &car_vtbl;
car->cost = cost;
return comm;
}
Vtable animal_vtbl = {
.vtb_print = animal_print,
.vtb_compare = animal_compare
};
void
animal_print(const Common *obj)
{
const Animal *animal = (const Animal *) obj;
printf("The age is: %d\n",animal->age);
printf("The weight is: %d\n",animal->weight);
}
int
animal_compare(const Common *lhsp,const Common *rhsp)
{
const Animal *lhs = (const Animal *) lhsp;
const Animal *rhs = (const Animal *) rhsp;
int cmp;
do {
cmp = lhs->age - rhs->age;
if (cmp)
break;
cmp = lhs->weight - rhs->weight;
if (cmp)
break;
} while (0);
return cmp;
}
Common *
animal_new(const char *name,int age,int weight)
{
Animal *animal = malloc(sizeof(*animal));
Common *comm = &animal->comm;
comm->name = name;
comm->type = TYPE_ANIMAL;
comm->vtbl = &animal_vtbl;
animal->age = age;
animal->weight = weight;
return comm;
}
void
common_print(const Common *comm)
{
printf("The name is: %s\n", comm->name);
comm->vtbl->vtb_print(comm);
}
int
common_compare(const Common *lhs,const Common *rhs)
{
int cmp;
do {
cmp = lhs->type - rhs->type;
if (cmp)
break;
cmp = strcmp(lhs->name,rhs->name);
if (cmp)
break;
cmp = lhs->vtbl->vtb_compare(lhs,rhs);
if (cmp)
break;
} while (0);
return cmp;
}
List *
list_new(void)
{
List *list = calloc(1,sizeof(*list));
return list;
}
void
list_add(List *list,Common *comm)
{
Common *tail;
tail = list->tail;
comm->prev = tail;
comm->next = NULL;
if (tail == NULL)
list->head = comm;
else
tail->next = comm;
list->tail = comm;
list->count += 1;
}
void
list_unlink(List *list,Common *comm)
{
Common *next;
Common *prev;
next = comm->next;
prev = comm->prev;
if (list->head == comm)
list->head = next;
if (list->tail == comm)
list->tail = prev;
if (next != NULL)
next->prev = prev;
if (prev != NULL)
prev->next = next;
list->count -= 1;
comm->next = NULL;
comm->prev = NULL;
}
void
list_sort(List *listr)
{
List list_ = { 0 };
List *listl = &list_;
Common *lhs = NULL;
Common *rhs;
Common *min;
int cmp;
while (1) {
rhs = listr->head;
if (rhs == NULL)
break;
min = rhs;
for (rhs = min->next; rhs != NULL; rhs = rhs->next) {
cmp = common_compare(min,rhs);
if (cmp > 0)
min = rhs;
}
list_unlink(listr,min);
list_add(listl,min);
}
*listr = *listl;
}
void
list_rand(List *listr)
{
List list_ = { 0 };
List *listl = &list_;
Common *del;
int delidx;
int curidx;
int cmp;
while (listr->count > 0) {
delidx = rand() % listr->count;
curidx = 0;
for (del = listr->head; del != NULL; del = del->next, ++curidx) {
if (curidx == delidx)
break;
}
list_unlink(listr,del);
list_add(listl,del);
}
*listr = *listl;
}
void
sepline(void)
{
for (int col = 1; col <= 40; ++col)
fputc('-',stdout);
fputc('\n',stdout);
}
void
list_print(const List *list,const char *reason)
{
const Common *comm;
int sep = 0;
printf("\n");
sepline();
printf("%s\n",reason);
sepline();
for (comm = list->head; comm != NULL; comm = comm->next) {
if (sep)
fputc('\n',stdout);
common_print(comm);
sep = 1;
}
}
int
main(void)
{
List *list;
Common *animal;
Common *car;
list = list_new();
animal = animal_new("Dog",10,200);
list_add(list,animal);
animal = animal_new("Dog",7,67);
list_add(list,animal);
animal = animal_new("Dog",10,67);
list_add(list,animal);
animal = animal_new("Cat",10,200);
list_add(list,animal);
animal = animal_new("Cat",10,133);
list_add(list,animal);
animal = animal_new("Cat",9,200);
list_add(list,animal);
animal = animal_new("Dog",10,200);
car = car_new("Ford",50000);
list_add(list,car);
car = car_new("Chevy",26240);
list_add(list,car);
car = car_new("Tesla",93000);
list_add(list,car);
car = car_new("Chevy",19999);
list_add(list,car);
car = car_new("Tesla",62999);
list_add(list,car);
list_print(list,"Unsorted");
list_rand(list);
list_print(list,"Random");
list_sort(list);
list_print(list,"Sorted");
return 0;
}
Here’s the program output:
----------------------------------------
Unsorted
----------------------------------------
The name is: Dog
The age is: 10
The weight is: 200
The name is: Dog
The age is: 7
The weight is: 67
The name is: Dog
The age is: 10
The weight is: 67
The name is: Cat
The age is: 10
The weight is: 200
The name is: Cat
The age is: 10
The weight is: 133
The name is: Cat
The age is: 9
The weight is: 200
The name is: Ford
The cost is: 50000
The name is: Chevy
The cost is: 26240
The name is: Tesla
The cost is: 93000
The name is: Chevy
The cost is: 19999
The name is: Tesla
The cost is: 62999
----------------------------------------
Random
----------------------------------------
The name is: Ford
The cost is: 50000
The name is: Chevy
The cost is: 26240
The name is: Dog
The age is: 10
The weight is: 200
The name is: Cat
The age is: 10
The weight is: 133
The name is: Dog
The age is: 10
The weight is: 67
The name is: Cat
The age is: 10
The weight is: 200
The name is: Cat
The age is: 9
The weight is: 200
The name is: Dog
The age is: 7
The weight is: 67
The name is: Tesla
The cost is: 93000
The name is: Tesla
The cost is: 62999
The name is: Chevy
The cost is: 19999
----------------------------------------
Sorted
----------------------------------------
The name is: Chevy
The cost is: 19999
The name is: Chevy
The cost is: 26240
The name is: Ford
The cost is: 50000
The name is: Tesla
The cost is: 62999
The name is: Tesla
The cost is: 93000
The name is: Cat
The age is: 9
The weight is: 200
The name is: Cat
The age is: 10
The weight is: 133
The name is: Cat
The age is: 10
The weight is: 200
The name is: Dog
The age is: 7
The weight is: 67
The name is: Dog
The age is: 10
The weight is: 67
The name is: Dog
The age is: 10
The weight is: 200