"How can I make data point to an area I can use?"
I'm not sure if you mean what I'm going to explain below (and it won't be short :P), but if you are asking how you can distinguish the type of the data stored in Node->data
, then with that implementation you cannot.
You leave it up to the end-programmer to remember what type of data he has stored into the list (which is not a bad thing btw.. on the contrary, it is the norm). In other words, you trust the end-programmer that he/she will apply the proper casts when say printing the Node->data
.
If for some reason you wish to provide a more managed API for your list, you could add one more field in a List
struct, for identifying the type of the data stored in your list.
For example...
enum DataType {
DT_INVALID = 0,
DT_PTR
DT_CHAR,
DT_INT,
DT_FLOAT,
DT_DOUBLE,
...
/* not a data-type, just their total count */
DT_MAX
};
#define DT_IS_VALID(dt) ( (dt) > DT_INVALID && (dt) < DT_MAX )
typedef struct List List;
struct List {
enum DataType dt;
Node *head;
};
Of course you are free to support less or more data-types than the ones I listed in the enum
, above (even custom ones, say for strings, or whatever you see fit according to your project).
So first you'll need a constructor (or initializer) for a list, something like this...
List *new_list( enum DataType dt )
{
List *ret = NULL;
if ( !DT_IS_VALID(dt) )
return NULL;
ret = malloc( sizeof(List) );
if ( NULL == ret )
return NULL;
ret->dt = dt;
ret->head = NULL;
return ret;
}
and instantiate it, say like this...
int main( void )
{
List *listInt = new_list( DT_INT );
if ( NULL == list ) {
/* handle failure here */
}
Now that you have already stored the intended data-type into the list meta-data, you are free to choose how you will implement the Node
constructors. For example, a generic one could look something like this...
int list_add_node( List *list, const void *data )
{
Node *node = NULL;
size_t datasz = 0;
/* sanity checks */
if ( !list || !data || !DT_IS_VALID(list->dt) )
return 0; /* false */
node = malloc( sizeof(Node) );
if ( NULL == node )
return 0; /* false */
/* when data points to mem already reserved say for an array (TRICKY) */
if ( DT_PTR == list->dt ) {
node->data = data;
}
/* when data points to mem reserved for a primitive data-type */
else {
datasz = dt_size( list->dt ); /* implement dt_size() according to your supported data-types */
node->data = malloc( datasz );
if ( NULL == node->data ) {
free( node );
return 0; /* false */
}
memcpy(node->data, data, datasz );
}
/* add here the code dealing with adding node into list->head */
...
return 1; /* true */
}
For DT_PTR
(which I flagged as TRICKY in the example) it is more safe to implement a different Node
constructor, perhaps accepting 2 extra arguments, lets say elemsz
and nelems
, so the function can allocate elemsz * nelems
bytes for copying the data
contents into them, in case data
points to an array, a struct or any other non-primitive type. Or you can provide an extra DT_ARR
enum value specifically for arrays. You are free to do whatever suits you best.
In any case, for DT_PTR
the example above relies on the caller of list_add_node
to have properly allocated the passed data
, and in general context this is not a good thing at all.
The code is more complicated, but you know the data-type stored in your list. So for at least the primitive data-types you can add say a printing routine that automatically casts its output according to list->dt
(for non-primitive data types you should provide support for custom printing routines, usually via callback functions).
You can even take it to the extreme, and move the dt
field from List
to Node
. In that case you implement a list of heterogeneous data in the nodes, but it gets much more complicated and also its rarely useful (if ever).
All this ADT stuff via (void *) pointers have serious performance issues, that's why speed critical implementations utilize (or abuse if you prefer) the pre-processor instead, for this kind of stuff.