Manu
Manu

Reputation: 5794

Is declaring an header file essential?

Is declaring an header file essential? This code:

main()
{
  int i=100;

  printf("%d\n",i);
}

seems to work, the output that I get is 100. Even without using stdio.h header file. How is this possible?

Upvotes: 4

Views: 438

Answers (5)

Vovanium
Vovanium

Reputation: 3898

C supprots three types of function argument forms:

  1. Known fixed arguments: this is when you declare function with arguments: foo(int x, double y).
  2. Unknown fixed arguments: this is when you declare it with empty parentheses: foo() (not be confused with foo(void): it is the first form without arguments), or not declare it at all.
  3. Variable arguments: this is when you declare it with ellipsis: foo(int x, ...).

When you see standard function working then function definition (which is in form 1 or 3) is compatible with form 2 (using same calling convention). Many old std. library functions are so (as desugned to be), because they are there form early versions of C, where was no function declarations and they all was in form 2. Other function may be unintentionally be compatible with form 2, if they have arguments as declared in argument promotion rules for this form. But some may not be so.

But form 2 need programmer to pass arguments of same types everywhere, because compiler not able to check arguments with prototype and have to determine calling convention osing actual passed arguments.

For example, on MC68000 machine first two integer arguments for fixed arg functions (for both forms 1 and 2) will be passed in registers D0 and D1, first two pointers in A0 and A1, all others passed through stack. So, for example function fwrite(const void * ptr, size_t size, size_t count, FILE * stream); will get arguments as: ptr in A0, size in D0, count in D1 and stream in A1 (and return a result in D0). When you included stdio.h it will be so whatever you pass to it.

When you do not include stdio.h another thing happens. As you call fwrite with fwrite(data, sizeof(*data), 5, myfile) compiler looks on argruments and see that function is called as fwrite(*, int, int, *). So what it do? It pass first pointer in A0, first int in D0, second int in D1 and second pointer in A1, so it what we need.

But when you try to call it as fwrite(data, sizeof(*data), 5.0, myfile), with count is of double type, compiler will try to pass count through stack, as it is not integer. But function require is in D1. Shit happens: D1 contain some garbage and not count, so further behaviour is unpredictable. But than you use prototype defined in stdio.h all will be ok: compiler automatically convert this argument to int and pass it as needed. It is not abstract example as double in arument may be just result of computation involving floating point numbers and you may just miss this assuming result is int.

Another example is variable argument function (form 3) like printf(char *fmt, ...). For it calling convention require last named argument (fmt here) to be passed through stack regardess of its type. So, then you call printf("%d", 10) it will put pointer to "%d" and number 10 on stack and call function as need.

But when you do not include stdio.h comiler will not know that printf is vararg function and will suppose that printf("%d", 10) is calling to function with fixed arguments of type pointer and int. So MC68000 will place pointer to A0 and int to D0 instead of stack and result is again unpredictable.

There may be luck that arguments was previously on stack and occasionally read there and you get correct result... this time... but another time is will fail. Another luck is that compiler takes care if not declared function may be vararg (and somehow makes call compatible with both forms). Or all arguments in all forms are just passed through stack on your machine, so fixed, unknown and vararg forms are just called identically.

So: do not do this even you feel lucky and it works. Unknown fixed argument form is there just for compatibility with old code and is strictly discouraged to use.

Also note: C++ will not allow this at all, as it require function to be declared with known arguments.

Upvotes: 0

imaximchuk
imaximchuk

Reputation: 748

This is possible because when C compiler sees an undeclared function call (printf() in your case) it assumes that it has


int printf(...)

signature and tries to call it casting all the arguments to int type. Since "int" and "void *" types often have same size it works most of the time. But it is not wise to rely on such behavior.

Upvotes: 0

binW
binW

Reputation: 13712

As paxidiablo said its not necessary but this is only true for functions and variables but if your header file provides some types or macros (#define) that you use then you must include the header file to use them because they are needed before linking happens i.e during pre-processing or compiling

Upvotes: 0

gavinb
gavinb

Reputation: 20048

How is this possible? In short: three pieces of luck.

This is possible because some compilers will make assumptions about undeclared functions. Specifically, parameters are assumed to be int, and the return type also int. Since an int is often the same size as a char* (depending on the architecture), you can get away with passing ints and strings, as the correct size parameter will get pushed onto the stack.

In your example, since printf was not declared, it was assumed to take two int parameters, and you passed a char* and an int which is "compatible" in terms of the invocation. So the compiler shrugged and generated some code that should have been about right. (It really should have warned you about an undeclared function.)

So the first piece of luck was that the compiler's assumption was compatible with the real function.

Then at the linker stage, because printf is part of the C Standard Library, the compiler/linker will automatically include this in the link stage. Since the printf symbol was indeed in the C stdlib, the linker resolved the symbol and all was well. The linking was the second piece of luck, as a function anywhere other than the standard library will need its library linked in also.

Finally, at runtime we see your third piece of luck. The compiler made a blind assumption, the symbol happened to be linked in by default. But - at runtime you could have easily passed data in such a way as to crash your app. Fortunately the parameters matched up, and the right thing ended up occurring. This will certainly not always be the case, and I daresay the above would have probably failed on a 64-bit system.

So - to answer the original question, it really is essential to include header files, because if it works, it is only through blind luck!

Upvotes: 9

paxdiablo
paxdiablo

Reputation: 882566

You don't have to include the header file. Its purpose is to let the compiler know all the information about stdio, but it's by no means necessary if your compiler is smart (or lazy).

You should include it because it's a good habit to get into - if you don't, then the compiler has no real way to know if you're breaking the rules, such as with:

int main (void) {
    puts (7);       // should be a string.
    return 0;
}

which compiles without issue but rightly dumps core when running. Changing it to:

#include <stdio.h>
int main (void) {
    puts (7);
    return 0;
}

will result in the compiler warning you with something like:

qq.c:3: warning: passing argument 1 of ‘puts’ makes pointer
                 from integer without a cast

A decent compiler may warn you about this, such as gcc knowing about what printf is supposed to look like, even without the header:

qq.c:7: warning: incompatible implicit declaration of
                 built-in function ‘printf’

Upvotes: 14

Related Questions