Simplifying the use of pointers in C code.

Thread Starter

ApacheKid

Joined Jan 12, 2015
1,609
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.

C:
typedef struct item_struct Item, * Item_ptr;
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:

C:
typedef struct item_struct
{
    Item_ptr ps; // pointer to an Item
};
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:

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;
};
(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:

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;
};
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:

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;

}
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:

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.
}
Then these too can be put in a header file giving us:

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 !

}
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!
 
Last edited:

bogosort

Joined Sep 24, 2011
696
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'd strongly recommend that identifiers associated with typedefs end with the '_t' suffix, as in 'size_t'.
 

Thread Starter

ApacheKid

Joined Jan 12, 2015
1,609
@ApacheKid Thanks for sharing, I did came across nesting header files problem. I find this discipline very helpful!
When I worked at the Financial Times in the 90s I defined a rule that our C code should never have nested headers, meaning our header files (existing system .h files are what they are) would never include other header files. This started to pay big dividends after several months (we had to gradually convert existing code as and when we could).

Our .c source files would then simply include whatever they needed to compile, we had a simple policy (like I mentioned above) that header files would end in things like _defs.h (#defines), _types.h (typedefs), _aliases.h (typedef aliases), _api.h (function signature definitions) etc.

Once we'd all embraced this our life became easier, we'd had a ton of legacy spaghetti C code that was a nightmare to work on but this all eased off eventually.

I recall a terrible few days where we had incomprehensible bugs all due to nested headers and one was being dragged in by # include 'whatever.h' and some other header was dragging in # include <whatever.h> but these were different files, some how one was a (close) copy of the other and both were being picked up, this was the reason I defined these dev rules.
 

bug13

Joined Feb 13, 2012
2,002
@ApacheKid I try to make a template for myself so I can use it. But I get this error:
Code:
typedef struct queue_struct
{
    int num_elements;
    Item_ptr head;
    Item_ptr tail;
};
^ <<=== here

queue_types.h:22:1: warning: useless storage class specifier in empty declaration
};
I can fix the error by this:
Code:
typedef struct queue_struct
{
    int num_elements;
    Item_ptr head;
    Item_ptr tail;
}Queue; /* this fix it */
Are you able to take a look at what I have done wrong??
Here are the files:
https://github.com/jmswu/useful-way-to-organize-files-in-C
 
Last edited:

402DF855

Joined Feb 9, 2013
271
Go back to #1 and look at:
C:
typedef struct item_struct Item, * Item_ptr;
You should have something similar preceding your struct.
 

bug13

Joined Feb 13, 2012
2,002
@402DF855

If you mean this, I still get the same error:
C:
typedef struct item_struct Item, * Item_ptr;
typedef struct item_struct
{
    Item_ptr next;
    Item_ptr prev;
};

typedef struct queue_struct
{
    int num_elements;
    Item_ptr head;
    Item_ptr tail;
};
Error messages:
gcc -c -o queue.o queue.c
In file included from queue.c:6:0:
queue_types.h:16:1: warning: useless storage class specifier in empty declaration
};
^
queue_types.h:23:1: warning: useless storage class specifier in empty declaration
};
^
gcc -Wall -std=c99 -o queue queue.o
 

402DF855

Joined Feb 9, 2013
271
C:
typedef struct queue_struct Queue, *Queue_ptr;

typedef struct queue_struct
{
    int num_elements;
    Item_ptr head;
    Item_ptr tail;
};
 

bug13

Joined Feb 13, 2012
2,002
Ooops, this fixed it, in my previous files, I typdef the same struct twice.
Code:
typedef struct queue_struct Queue, *Queue_ptr;

struct queue_struct
{
    int num_elements;
    Item_ptr head;
    Item_ptr tail;
};

;
 
Last edited:

bug13

Joined Feb 13, 2012
2,002
@ApacheKid
@402DF855

It looks like all the APIs are in the queue_api.h file, do I understand it correctly? So you don't put any function prototype in the .h and the body in .c file instead?
 
Last edited:

402DF855

Joined Feb 9, 2013
271
Personally, I prefer the style:
C:
typedef struct
{
    int num_elements;
    Item_ptr head;
    Item_ptr tail;
} Queue;
While I appreciate the suggestion of defining the pointer type as well (Queue_ptr in this case) I haven't utilized that technique often in my career.
It looks like all the APIs are in the queue_api.h file, do I understand it correctly? So you don't put any function prototype in the .h and the body in .c file instead?
I'm sorry, your question is confusing. Please add details.
 

bug13

Joined Feb 13, 2012
2,002
I'm sorry, your question is confusing. Please add details.
It looks like all the functions (APIs) in post #1 are written in a .h file. Is that correct? eg in the queue_api.h file, the functions body are in that file:
C:
/* queue_api.h */

Queue_ptr Queue_create(){
    // code etc...
}

void Queue_Add(){
    // code etc...
}
I would normally expect function prototypes in are in xxx.h file, and functions body are in xxx.c file. eg:
C:
/* xxx.h file, function prototypes */

Queue_ptr Queue_create();
void Queue_add(Queue q);

/* xxx.c file, functions body */
Queue_ptr Queue_create(){
    // some code here
}

void Queue_add(Queue q){
    // some code here
}
 

402DF855

Joined Feb 9, 2013
271
I would normally expect function prototypes in are in xxx.h file, and functions body are in xxx.c file. eg:
In general you are correct. But in practice sometimes programmers put function bodies in the .h file. I typically don't unless there is a good reason to do it that way. Having functions defined in the header can lead to better compiler optimization for example. I recommend putting any declarations needed by other modules in the header file and nothing else.

So if a typedef/struct is used only within a single .c file I would avoid exposing it in the header file. An exception to this would be to allow for unit testing, but that's a whole other issue.
 

Thread Starter

ApacheKid

Joined Jan 12, 2015
1,609
Hi,

There are no formal rules for how to organize C projects, the way I do it is based on experience of various environments and project complexities that I've worked with over the years.

The "strategy" is to decouple (so far as makes sense) the implementation of an API from consumers of the API.

So to use the API one includes xxx_alias.h, xxx_types.h and xxx_api.h but also the very same files are also then included at the start of xxx_api.c.

So consumers of an API as well as the implementation of the AP all include the same files in the same order.

You can also have xxx_macros.h for all #defines too, isolating these fundamental concepts into their own headers and enforcing an order on how they are included yields big benefits, making the consumer and the implementation include the same headers in the same order also greatly reduces the risk of subtle problems.

As I say there are no formal rules but doing something similar to this, some formal strategy pays huge benefits as code grows, for example if I were building an RTOS I'd establish these kinds of things from the outset, before I wrote a single line of code.

You can of course also establish headers that include these patterns, for example we could create xxx_library.h that just include xxx_alias.h, xxx_types.h and xxx_api.h and then consumers and the implementation would include one file: xxx_library.h. this too is fine so long as your entire project follows the same overall strategy, that's the most important thing I think - consistency.
 

Thread Starter

ApacheKid

Joined Jan 12, 2015
1,609
Here's a snapshot of one of the key source files for a large shared memory management API written in C for Windows. You can see the pattern well here and how the different "categories" of headers lead to a neat easy to follow uniformity:

vmp_headers.jpg

You can see that "_enums" are in their own headers as are "_defs" (macros) and the those named "_prior" are what I now call "_alias" and "_calls" is "_api" (these are the newer terms I now use; that code was started a decade ago).

What is less obvious is that (for example) "vmp_kernel_types.h" contains types that leverage other types like "vmp_spinlock_types." and so the ordering of these headers reflects a deeper structural layering of the APIs themselves, the "kernel" API leverages the "spinlock" API but the spinlock API does NOT leverage (or "know about") the kernel API - for example.
 
Last edited:

Thread Starter

ApacheKid

Joined Jan 12, 2015
1,609
wow! The codes I wrote are nowhere near complex like yours!

I put what you suggested in the post #1 in my github so I can reference to it when I need to, I hope you don't mind. If any concerns, just let me know and I will remove it.

https://github.com/jmswu/useful-way-to-organize-files-in-C
Well it's not intended to impress you, a large codebase like that grew over time, in increments.

One of the most important things (relatively) new software developers overlook is the importance of organization, the importance of simplicity. Once a medium or larger, codebase starts to grow in an irregular way the complexity can ramp and the organizational complexity can begin to hurt.
 
Top