The problem you are struggling with - how to create reusable generic functionality, such as containers - is one which is addressed easily with object oriented programming languages such as C++. However they can be implemented in C, even if not as easily and flexibly.
The key is to identify which functionality is common and generic, and which is specific to the types of nodes in the list.
Generic functionality would be the functions such as creating/initializing the list, adding nodes, deleting nodes, iterating through the list, etc.
Create stand alone functions for handling lists, together with a struct that models the generic nodes.
Then, create structures representing the different object types in the list. Embed one of the generic list node structs as the first element in each of these structs, and write functions as needed to provide the functionality for handling each of the different types of objects you need to deal with.
In that way you end up with generic, re-usable lists, and as an added bonus you keep your code for dealing with the specific object types clean and easily maintainable.
Here is a simple program I whipped up to demonstrate the principle. It allows creating a list of objects representing apples and oranges. The apples have functionality for tracking their weight, and the oranges have functionality for tracking their price.
#include <stdio.h>
#include <stdlib.h>
/* First we start with the definition of a generic list node */
struct list_node {
struct list_node *next;
struct list_node *prev;
/* The 'type' field is important - it allows us to have a list
* containing a number of different types of node, and to be able
* to find out what type each node is */
int type;
};
/* Some variables to keep track of the beginning and end of the list. */
struct list_node* head;
struct list_node* tail;
/* Now some generic functions for dealing with lists - initializing the list,
* adding nodes through it, iterating through the list */
void list_init() {
head=tail=NULL;
}
void list_add_node(struct list_node* node) {
if (NULL==tail) {
head=tail=node;
node->next=NULL;
node->prev=NULL;
} else {
tail->next=node;
node->next=NULL;
node->prev=tail;
tail=node;
}
}
struct list_node* list_get_next(struct list_node* node) {
if (NULL==node)
return head;
else
return node->next;
};
/* Great, now we have a generic set of functions for dealing with generic lists.
* But how do we use that to contain different kinds of objects? The answer
* is composition - we include the list_node as the first element of each of the
* structs that we want to add to the list
*
*
* Here we define a struct for recording the weight of apples, together with
* functions specific to dealing with apples
*/
struct apple {
struct list_node node;
int weight;
};
struct apple* new_apple(int weight) {
struct apple* a = (struct apple*)malloc(sizeof(struct apple));
/* Apples are considered type 1 */
a->node.type = 1;
a->weight = weight;
return a;
};
void apple_printweight(struct apple* a) {
printf("This is an apple and it weighs %d grams\n", a->weight);
}
/* And here we define a struct for recording the price of oranges, together with
* functions specific for dealing with oranges
*/
struct orange {
struct list_node node;
double price;
};
struct orange* new_orange(double price) {
struct orange* o = (struct orange*)malloc(sizeof(struct orange));
/* Oranges are type 2 */
o->node.type = 2;
o->price=price;
return o;
};
void orange_printprice(struct orange* o) {
printf("This is an orange and it costs $%6.2f dollars\n", o->price);
}
/* Now to use our oranges and apples */
int main() {
list_init();
/* Create an orange, add it to the list
*
* Note the need to cast the orange to a list_node
* so we can call the 'list_add_node' function.
* This makes use of a property of pointers to structs:
* you can always convert them to point to the first element of
* the struct.
*/
struct orange* myOrange = new_orange(12.50);
list_add_node((struct list_node*)myOrange);
/* Create an apple, add it to the list */
struct apple* myApple = new_apple(150);
list_add_node((struct list_node*)myApple);
/* Iterate through the list */
struct list_node* n = NULL;
while (n = list_get_next(n)) {
/* For each node we come to, it could be an apple or an orange.
* Inspect the type to find out what type it is, and use it
* accordingly */
if (n->type == 1) {
apple_printweight((struct apple*)n);
} else if (n->type == 2) {
orange_printprice((struct orange*)n);
}
}
/* In a real program you would want to free the list objects here
* to avoid a memory leak
*/
}
Here is the output:
This is an orange and it costs $ 12.50 dollars
This is an apple and it weighs 150 grams
Bear in mind that this is just a simple example, and by no means does it illustrate best practice in all aspects. In real life you would separate the declarations into header files, and you would probably do something more elegant than the if/then branching in the main loop to handle the different types.