I've seen several threads here that talk about pointers and pointers to data structures and data structures that contain pointers to other structures and so on. C has a rather terse syntax and can be confusing to the newcomer, frankly the C grammar is poor but it is what it is.
I've designed and developed huge codebases written in C this includes compilers for other languages as well as native 64 bit APIs on Windows that supported concurrent multi-process access to large blocks of shared memory (almost a mini OS).
One of the problems that comes up is settling on a disciplined way of handling basic stuff like header files, type declarations, pointer use and so on, without a solid disciplined set of principles one's code can become messy and cluttered and hard to work with over time.
A simple yet very helpful strategy is to discipline how we declare structures and pointers to them, the typedef keyword is very helpful here and is not used enough in most C code that I come across.
This begins by defining a typedef that aliases both the struct name and the struct pointer type name, e.g.
This must appear before the actual defintion of the struct, once we do this we can refer to the struct type as Item and the struct pointer type as Item_ptr.
Now here's the struct definition:
This is deliberately simple as is just an example but as you can see the struct definition is able to refer to the new type alias Item_ptr because we defined it first.
This may seem rather mundane but this really comes in useful as we add more and more types and establish more and more pointer relationships, for example lets define a queue:
(this is just an example we may not actually implement a queue this way but that's not important here).
The key benefit here is that * notation for declaring pointers is now out of the way, we can see very easily that a type is a pointer by virtue of the type's name ending in "_ptr" you can choose any naming convention you like of course, this is just one I use.
This can be improved more if one wants to also use "_ptr" in the member names as well as in their type names:
Now you'll notice that we must always define the aliases before we can refer to them inside struct defintions, well this leads to the nest strategic rule, namely confine all aliases to their own header file: queue_alias.h and the structs themselves to queue_types.h, when we do this our code might begin to look like this:
If we follow this line of reasoning we can eventually get to the stage where we define sets of headers and these headers all have a fixed order in which they must appear, for example we may have constants/defines associated with our queue and might put these in queue_defs.h.
Then we'd include queue_defs.h fitrst, followed by queue_alias.h and then queue_types.h.
We might also have a set of functions for managing a queue:
Then these too can be put in a header file giving us:
Of course we can even remove the haders queue_alias.h and queue_types.h and put these at the top of queue_api.h too, but be careful, you may have other code that also uses queue types that's nothing to do with the queue API. so I advise against this.
The reason I advise this is that we get huge benefits when we eliminate or strive to eliminate nested header files, I cannot stress enough how valuable this is once you adopt is as a discipline. I've seen this get out of control where someone cannot compile a project because some some header isn't being found or is conflicting with some other nested header and often the tools and compilers are unable to help pin down the cause.
This leads to a pattern. strategy where we routinely include xxx_defs.h, xxx_alias.h, xxx_types.h and xxx_api.h in that order in ever source file that needs to use the xxx API.
So this is my advice on how to organize type defintions, headers, alaises etc when using C, as a project grows having this disciplined strategy will make your life a lot easier!
I've designed and developed huge codebases written in C this includes compilers for other languages as well as native 64 bit APIs on Windows that supported concurrent multi-process access to large blocks of shared memory (almost a mini OS).
One of the problems that comes up is settling on a disciplined way of handling basic stuff like header files, type declarations, pointer use and so on, without a solid disciplined set of principles one's code can become messy and cluttered and hard to work with over time.
A simple yet very helpful strategy is to discipline how we declare structures and pointers to them, the typedef keyword is very helpful here and is not used enough in most C code that I come across.
This begins by defining a typedef that aliases both the struct name and the struct pointer type name, e.g.
C:
typedef struct item_struct Item, * Item_ptr;
Now here's the struct definition:
C:
typedef struct item_struct
{
Item_ptr ps; // pointer to an Item
};
This may seem rather mundane but this really comes in useful as we add more and more types and establish more and more pointer relationships, for example lets define a queue:
C:
typedef struct item_struct Item, * Item_ptr;
typedef struct queue_struct Queue, * Queue_ptr;
typedef struct item_struct
{
Item_ptr next;
Item_ptr prev;
};
typedef struct queue_struct
{
int num_elements;
Item_ptr head;
Item_ptr tail;
};
The key benefit here is that * notation for declaring pointers is now out of the way, we can see very easily that a type is a pointer by virtue of the type's name ending in "_ptr" you can choose any naming convention you like of course, this is just one I use.
This can be improved more if one wants to also use "_ptr" in the member names as well as in their type names:
C:
typedef struct item_struct Item, * Item_ptr;
typedef struct queue_struct Queue, * Queue_ptr;
typedef struct item_struct
{
Item_ptr next_ptr;
Item_ptr prev_ptr;
};
typedef struct queue_struct
{
int num_elements;
Item_ptr head_ptr;
Item_ptr tail_ptr;
};
C:
#include<stdlib.h>
#include "queue_alias.h"
#include "queue_types.h"
int main()
{
// Create and init a new Queue
Queue_ptr qp = malloc(sizeof(Queue));
qp->head_ptr = NULL;
qp->tail_ptr = NULL;
qp->num_elements = 0;
}
Then we'd include queue_defs.h fitrst, followed by queue_alias.h and then queue_types.h.
We might also have a set of functions for managing a queue:
C:
Queue_ptr CreateQueue()
{
// Create and init a new Queue
Queue_ptr qp = malloc(sizeof(Queue));
qp->head_ptr = NULL;
qp->tail_ptr = NULL;
qp->num_elements = 0;
return qp;
}
void AddToQueue(Queue_ptr queue_ptr, Item_ptr item_ptr)
{
if (queue_ptr == NULL)
; // error
if (item_ptr == NULL)
; // error
item_ptr->next_ptr = queue_ptr->head_ptr;
queue_ptr->head_ptr = item_ptr;
// etc etc etc.
}
Code:
#include<stdlib.h>
#include "queue_alias.h"
#include "queue_types.h"
#include "queue_api.h"
int main()
{
Queue_ptr queue_ptr = CreateQueue();
AddToQueue(queue_ptr, NULL); // in reality we'd never pass NULL !
}
The reason I advise this is that we get huge benefits when we eliminate or strive to eliminate nested header files, I cannot stress enough how valuable this is once you adopt is as a discipline. I've seen this get out of control where someone cannot compile a project because some some header isn't being found or is conflicting with some other nested header and often the tools and compilers are unable to help pin down the cause.
This leads to a pattern. strategy where we routinely include xxx_defs.h, xxx_alias.h, xxx_types.h and xxx_api.h in that order in ever source file that needs to use the xxx API.
So this is my advice on how to organize type defintions, headers, alaises etc when using C, as a project grows having this disciplined strategy will make your life a lot easier!
Last edited: