No, and Some Systems Used the Same Convention
K&R C had only a single calling convention, which is how printf()
could be called without any special syntax or function prototype. In fact, since there were no function prototypes in C until ANSI C, there could not possibly have been different calling conventions for any implementation that also needed to compile K&R-style code, for example:
struct FILE;
struct FILE* fprintf();
int fopen();
int main() {
struct FILE *outfile;
outfile = fopen("hello.txt", 3);
/* WONTFIX: Real code should check for errors. */
fprintf(outfile, "%s\n", "hello, world!");
return 0;
}
There is simply no way for the compiler to tell that fprintf
is variadic and fopen
is not (at least, not for code other than the standard library, without doing more whole-program analysis than compilers back then did).
On 16-bit x86, this led to C having a different calling convention from Pascal (with C passing its arguments in reverse order, so that the first argument of a variadic function, such as its format string, would always be at a known location, immediately before the return address on top of the stack). This is why ANSI C required varargs functions to have at least one non-variadic parameter, and for the variadic parameters to come last.
The va_start
and va_arg
macros are the way they are to allow them to be implemented by taking the address of the last non-variadic argument on the call stack and successively adding the size of each variadic argument to obtain a pointer to the next.
Most of the special rules for variadic functions, such as the default argument promotions, exist solely for the sake of backward-compatibility with K&R C. For example, arguments narrower than an int
widen to int
because an int
represented the smallest object that could be passed on the stack on word-addressed machines, and because this saved an instruction on machines that had no byte or half-word instructions. Similarly, float
widens to double
partly to avoid rounding error, but mainly because that was how it worked on the DEC PDP-8. The rules for pointer conversions were intended to enable K&R-style cpde such as &dest == memcpy(&dest, &src, sizeof(dest));
that worked on K&R C (because there was no type-checking of function arguments, and all object pointers were cast to character pointers when passed as function arguments) to also compile on ANSI C, with the function prototype void* memcpy(const void*, void*, size_t);
.