I had a reader offer me a puzzle the other day. His code ran well without the #include
directive and he wondered why. I did, too.
Many beginning C programmers are confused regarding the #include
preprocessor directives. Some believe that these lines add the code that makes the program run, which may be true in other languages but not in C.
An #include
directive inserts the contents of the named header file into the source code. While these files contain prototypes, definitions, macros, and so on, in C they typically don’t contain programming code. Yes, a macro is code. But header files in C don’t contain C language statements, which is popular in C++. (You can write C code in a header file, no problem, but doing so tells me that you are trained in C++.)
In C, it’s the library files contain the actual code, helping build the object files and are eventually linked to form a program.
What the header files do is to save time. Because the compiler desires to have every function prototyped, the header file saves you from knowing (or having to look up) each function’s prototype. Minus the prototype, the compiler guesses, which is what happens with this code the reader sent me:
2025_05_17-Lesson-a.c
int main() { puts("How do I work?"); return 0; }
When building this code, the following warning is generated:
0517b.c:3:2: warning: implicit declaration of function 'puts' is invalid in C99
The code builds. It runs:
How do I work?
The reader wanted to know why the program runs without the #include
directive. The answer is that the compiler assumes that the first use of a function is its declaration. Further, the assumption is made that the function returns an integer.
In the case of puts(), the compiler sees that the function contains a string, therefore it assumes its definition includes a string as its argument. The return value is an int, which is happenstance.
It’s pure luck that the program runs properly. If you used instead:
puts(2);
The same warning appears. The compiler assumes that puts() requires an integer as its argument. The program builds. When it runs, however, it suffers from a segmentation fault. Integer value 2 is not a string.
Consider this modification:
puts("How do I work?",4.5);
The same implicit declaration warning appears when this statement is compiled. The program builds — and it runs with the same output. That’s because the computer just ignores the extra argument on the stack. Consider it luck.
If, on the other hand, you include the stdio.h
header file and then add a bonus argument to the puts() function, you see an error. The function’s use doesn’t match its prototype declared in the stdio.h
header file.
So how does the program work without including the header file? Because the linker doesn’t care what’s passed to the function. The effect is trivial for puts(), but it can be devastating for other functions.
Proper C language procedure requires that you prototype functions, which is the point of including a header file. You can always get around this requirement by setting your own prototype:
2025_05_17-Lesson-b.c
int puts(const char *s); int main() { puts("How do I work?"); return 0; }
This code builds without warnings or errors. The function prototype is supplied without the need to include the header file. Here, if you add an extra parameter to the puts() function now, the compiler throws an error as the function doesn’t match the prototype.
Messing around with this stuff is fun, yet the point is to use the header files. Sure, you can get by without them, but the risk is that you write unstable and non-portable code. It’s best to follow the proper way of doing things, despite the neat-o aspect of bending the rules and getting away with it.
int puts(const char *s); /* builds without warnings or errors. */
I think it should be noted though, that it usually is not a good idea to forego header files. Thatʼs because one never knows what additional attributes might get applied to function declarations in platform-specific header files.
For example, under Windows
puts()
is prototyped like so:
__declspec(dllimport) int __cdecl puts (const char * _Str);
That's because puts follows the “C” calling convention and resides in msvcrt.dll (which needs to be loaded dynamically during program startup).
But even under *nix operating systems, such functions are often adorned with various __attribute__((…)) annotations, and may have
restrict
and other modifiers. (Or may even be macros, not actually functions.)Long story short for anyone reading this: please use header files to get at the correct declarations of things outside your program.
Not to mention that the “function” could be a macro.
As always, good info. Thanks!
you are not spying on my system about topics I’m actually struggeling with? 😉
Topic “libraries” … I had an ugly dog fight with it when trying to put code in libraries and use in a complex project not owned by me, either functions were “not declared”, “implicit declared”, had no or multiple “prototypes”, “redifinitions” … I got confused.
In an attempt to gain understanding I – tried to – puzzle(d) together an universal structure, using different libraries and compiling in different ways / with different options.
It’s still in an early state of WIP, however you might like to have a look in it?
P.S. Contrary to your assumption, I am not contaminated with C++, it has Chinese qualities for me. I only liked the idea of “header only” because this #define, #include, include guard, Makefile, path setting … fiddling around had been bothering me for years.
Add. to previous comment, the link didn’t make it through …
https://gitlab.gnome.org/newbie-02/uni_struct
Evtl. this editor could benefit from a preview function?
@newbie-02 I’m curious about the Chinese qualities you mention.
I apologize for being accusatory. Many C programmers borrow heavily from the experience with C++. Nothing says you can’t put a function or other code into a header, it’s just unusual to me.
Chinese: it neither starts nor stops at things like ( AI comment ):
a = b + c;
This line might seem straightforward, but b and c could be of types that overload the + operator, and a could be of a type that overloads the assignment operator, leading to unexpected behavior.