My basic practice:
Header contains anything that the caller needs to know to use the module and nothing more.
Any globals shared by functions in the module are declared "static" to restrict them to file scope.
If there is information (e.g. array sizes) which multiple modules require that goes in a separate file.
If using function putfoo() requires using function getfoo() both belong in the same module. Anything that might be used without ever using another function belongs in a separate module.
The reasoning is as follows:
Every piece of information should exist in one place and one place only. This prevents making changes which are incompatible and will cause an error.
The linker includes everything contained in a file whether it is used or not. So if all the functions are in a single file, all of the functions are incorporated in the executable and loaded into memory if a single function is referenced. (NB: I have seen claims that this is not true with the Gnu linker. I don't know. But it has been the case on over a dozen Unix systems I've worked on)
If you have multiple related functions which may be used independently of each other, rather than a header for each module you put the modules in a library and have a single library header file. If several functions need to share a global variable, those go in a module with its own header which includes the library header.
You should only use global variables if it makes the code more readable or if the object is too large to be allocated on the stack as an automatic variable (i.e. large arrays). If you use a global,restrict the scope of the variable as tightly as possible using "static".
Remember, you are writing something which will need to be read at some time in the future. You want to communicate as clearly as possible to the reader.
I rarely use globals unless forced to by the limitations on the size of automatic arrays. However, I once wrote a parser for an ad hoc data format that what complex. The resulting code was a loop with a long If-elseif block. Each conditional contained a function call. As I was finishing it up I observed that of a dozen or so functions, all of which took the same 4 parameters, two of them had 6 parameters. So I made the 4 shared parameters file scope globals so that anyone reading the code in the future would immediately see those two functions were different.
So I changed from:
if( foo_1(a,b,c,d) ){
}elseif( foo_2(a,b,c,d){
}elseif( foo_3(a,b,c,d){
}elseif( foo_4(a,b,c,d){
}elseif( foo_5(a,b,c,d,e){
}
to
if( foo_1() ){
}elseif( foo_2(){
}elseif( foo_3(){
}elseif( foo_4(){
}elseif( foo_5(e){
}
If you have a number of related functions which can all be used without using any of the others, put each function in its own .c file and put all the function prototypes and other information needed by the caller such as sizes in a library .h file. If some group of modules need to share information that the other modules don't need, create a separate header for those.
In summary, a large library might have several dozen .c files, a library .h file and a couple of .h files only included by a few files.
You should get a copy of the C standard if you don't already have one and read it any time you are doing anything you haven't been doing all day. Study the varied effects of "static" at file and function scope. Pay close attention to anything that is "undefined" or "implementation defined" and avoid doing the former at all times and the latter unless you absolutely have to, in which case guard it with a #ifdef _foo_system_ so that it will have to be looked at to make it compile on a different system.
As for the comments made while I was writing this. Having had to fix a few million lines of code written with that mindset, *please* don't do that. It causes all sorts of headaches. If you program well, you write the code and then use it for years without ever making any changes. Having written a pair of 15,000 line libraries which continued in service for over 15 years with only one change and *no* bugs reported I know ti can be done. The one change was in a function that did the following:
errno = 0;
str = getcwd( str ,len(str) );
if( ernno){
fprintf( stderr, "getcwd() failed/n" );
}
An OS update by Sun resulted in errno being set non-zero even though the call had succeeded. The friend who was still maintaining the code changed it to test if a null was returned. He told me about it over a lunch several years after I wrote the code.