C supports three types of function argument forms:
- Known fixed arguments: this is when you declare a function with arguments:
foo(int x, double y) . - Unknown fixed arguments: this is when you declare it as empty parentheses:
foo() (not to be confused with foo(void) : this is the first form without arguments) or not to declare it at all. - Variable arguments: this is when you declare it with an ellipsis:
foo(int x, ...) .
When you see the standard function operation, then the function definition (which is in form 1 or 3) is compatible with form 2 (using the same calling rule). Many old std. library functions in the same way (as they were canceled) because they exist in early versions of C, where there were no function declarations, and all of them were in form 2. Another function may be inadvertently compatible with form 2 if they have arguments as declared in the argument promotion rules for this form. But some may not be like that.
But the programmer of form 2 must pass arguments of the same types everywhere, because the compiler cannot check the arguments with the prototype and must determine the calling arguments osing the actual arguments passed.
For example, on the MC68000, the first two integer arguments for the fixed arg functions (for both forms 1 and 2) will be passed to the registers D0 and D1 , the first two pointers to A0 and A1 , all the rest passed through the stack. So, for example, the function fwrite(const void * ptr, size_t size, size_t count, FILE * stream); will receive arguments like: ptr in A0 , size in D0 , count in D1 and stream in A1 (and will return the result in D0 ). When you include stdio.h , it will be so no matter what you pass.
When you do not enable stdio.h , something else happens. When you call fwrite with fwrite(data, sizeof(*data), 5, myfile) , the compiler looks at the arguments and sees that the function is called fwrite(*, int, int, *) . So what is he doing? It passes the first pointer to A0 , first int to D0 , second int to D1 and second pointer to A1 , so we need this.
But when you try to call it fwrite(data, sizeof(*data), 5.0, myfile) , with count has a double type, the compiler will try to pass count through the stack, since it is not an integer. But the function is required in D1 . Damn: " D1 contains garbage, not count , so the further behavior is unpredictable. But than you use the prototype defined in stdio.h , everything will be fine: the compiler automatically converts this argument to int and passes it as needed. This is not abstract an example, since double in arument might just be the result of a calculation using floating point numbers, and you can just skip this supposed int result.
Another example is the function of variable arguments (form 3), for example printf(char *fmt, ...) . This convention call requires the last named argument ( fmt here) to be passed through its type stack. So then you call printf("%d", 10) , it will put a pointer to "%d" and number 10 on the stack and call the function as needed.
But when you do not include stdio.h , the compiler will not know that printf is a vararg function and suppose that printf("%d", 10) calls a function with fixed arguments of type pointer and int. Thus, the MC68000 will put a pointer to A0 and int in D0 instead of the stack, and the result is again unpredictable.
Maybe good luck with the fact that the arguments were previously on the stack and sometimes read there, and you get the correct result ... this time ... but another time will fail. Another luck is that the compiler takes care if the undeclared function can be vararg (and somehow makes the call compatible with both forms). Or all arguments in all forms are simply passed through the stack on your computer, so fixed, unknown, and vararg forms are simply called identically.
So: do not do this, even if you are lucky and it works. An unknown form of fixed argument exists only for compatibility with old code and is strongly discouraged.
Also note: C++ will not allow this at all, since it requires a function to be declared using well-known arguments.