For a small toy project like yours, you're probably not going to see the advantage of separating your implementation from your headers. Doing development on a large, robust codebase, this can make all the difference in the world.
Also, if you use open source software and like to tinker with things, it may help a lot. This obviously doesn't apply much in the Windows world since Windows users typically want packaged binaries and setup applications. However, in the Linux world there are a lot of people who maintain systems that they compiled from scratch (Gentoo!). Here's a small list of programs that could take all day to compile, even if you have a fairly modern machine:
- OpenOffice
- The Linux kernel
- X
- KDE / Gnome
- Firefox
- The C runtime library (glibc)
- GCC
- etc...
Originally Posted by
Jesdisciple
But then if I happen to insert the wrong type into the list, I get a very confusing segfault later on.
Well, in the C world, the burden is usually on the programmer to make sure they don't do stupid things like this. So my response to you would be: don't insert the wrong type into the list!
If you're seriously looking for robust type-safety, bounds checking, exception handling, memory management etc... then C is not your language.
Now, that being said, I understand that this is more of a learning exercise. So here are two alternatives to what you were doing:
1) Implement the list by using void*'s as I suggested. Then, in the application, include wrapper functions around the insertion functions that accept the type you want to add to the list. That way you'll get compile time type checking, and you won't need to clutter your list with all that extra garbage. It doesn't even require that much code. Lets say we wanted to maintain a queue of Car structs in the program:
Code:
void enqueue_car(List *list, Car *car) {
list_add_first(list, car);
}
Car *dequeue_car(List *list) {
return list_remove_last(list);
}
2) If you insist on storing a string representing the element type of the list (which doesn't sound like a very elegant solution to me), at least store it at the list level, rather than per node. The size information really isn't needed unless your list is going to be allocating memory for its elements or accessing them for some reason.
Which brings me to another point that has already been mentioned: your list doesn't "own" its items. This is (usually) a bad design. When you store something in a list, you expect it to remain there until you free the list. The way you implemented it, the list just stores pointers to objects that some other code "owns". If that other code free's one of the objects, you now have a bad pointer stored somewhere in your list.
A solution to this, which again has already been posted, is to maintain a set of function pointers at the list level. When the user creates a list, they provide a set of function pointers that can manipulate the elements. Using the Car example again:
Code:
typedef struct List {
Node *first, *last;
size_t length;
void *(*copy_func)(void *element);
void (*free_func)(void *element);
};
...
Car my_car;
List *my_list = list_create(car_copy, car_free);
list_add_first(&my_car);
list_free(my_list);
Now when you go to add an item to the list, the add function can make a copy of the Car by using the car_copy() function in the List struct, and when you call list_free() it can free all of the Car copies it has by calling car_free(). And best of all, since the list is the ONLY thing that has pointers to these, no other code can free them.