From the GCC docs:
Header files serve two purposes.
- System header files declare the interfaces to parts of the operating system. You include them in your program to supply the definitions and declarations you need to invoke system calls and libraries.
- Your own header files contain declarations for interfaces between the source files of your program. Each time you have a group of related declarations and macro definitions all or most of which are needed in several different source files, it is a good idea to create a header file for them.
Including a header file produces the same results as copying the header file into each source file that needs it. Such copying would be time-consuming and error-prone. With a header file, the related declarations appear in only one place. If they need to be changed, they can be changed in one place, and programs that include the header file will automatically use the new version when next recompiled. The header file eliminates the labor of finding and changing all the copies as well as the risk that a failure to find one copy will result in inconsistencies within a program.
My take is that C headers are legacy artefacts from the early days of C.
It's important to understand that headers are included verbatim into source files by the C preprocessor. So the question really becomes: Why do we need forward declaration?
In order to keep executables small, type information is not embedded in the object code generated by C compilers. Because of this, linking against an object file or library requires a definition of the types that each function provides, as this information is not available from the object file. Modern compilers get around this by inspecting the source code or libraries for definitions - Identifiers and signatures are grabbed directly from the source or the library symbols.
To keep compilers simple and efficient, it would have been inappropriate to type check definitions from the library they will be linked against. Indeed, it is perfectly valid to compile a file without the used library even being available on the compilation machine. Similarly, it would be onerous to delay the compilation of a type until its dependencies have been compiled, and downright impossible in case of cyclic relationships. This requires that the function signature be made available before the function is used (for type checking). For convenience, C defaults to int fun(...)
so that in the then-commonest case, the need to forward-declare functions is lessened.
To make everybody's life easier, manual forward declarations were delegated to the pre-processor. In effect, the C compiler does not have the concept of a header file. Instead, the declarations are organised logically into header files which are then added to the program pre-compilation by the preprocessor.
This saves the programmer from having to type all the needed declarations at the beginning of every compilation unit, but it is essentially what is happening.
All this contortion is really a result of the limitations that existed in the early days of C.
There are, however, some definite advantages that result in these contortions. Having the function definitions available outside the implementation code itself allows clear separation between interface and implementation, which actually allows much cleaner systems to be built. In an ideal world, you would only ever need the header file to use a library, with no prior knowledge of the implementation. (aside: I'll trade you an ideal world for an invisible pink unicorn, if you ever find one.)
Modern languages have higher level constructs to separate implementation form interface: interfaces in Java, duck typing in Python, Protocols in Clojure, contracts in Eiffel, etc...