GCC (and possibly other C compilers) keep track of argument types, at least in some situations. But the language is not designed that way.
The printf function is a regular function that takes variable arguments. Variable arguments require some kind of runtime type identification scheme, but in C, the values do not contain information about the type of runtime. (Of course, C programmers can create I / O circuits using structures or bit manipulation tricks, but they are not integrated into the language.)
When we develop such a function:
void foo(int a, int b, ...);
we can pass "any" number of additional arguments after the second, and it is up to us to decide how many there are and what their types are, using some protocol that is outside the function transfer mechanism.
For example, if we call this function as follows:
foo(1, 2, 3.0); foo(1, 2, "abc");
there is no way that the caller can distinguish between cases. There are only a few bits in the parameter transfer area, and we don’t know whether they represent a pointer to character data or a floating point number.
The possibilities for transmitting this type of information are numerous. For example, in POSIX, the exec family of functions uses variable arguments of the same type, char * , and a null pointer is used to indicate the end of the list:
#include <stdarg.h> void my_exec(char *progname, ...) { va_list variable_args; va_start (variable_args, progname); for (;;) { char *arg = va_arg(variable_args, char *); if (arg == 0) break; /* process arg */ } va_end(variable_args); /*...*/ }
If the caller forgets to pass the null pointer terminator, the behavior will be undefined because the function will continue to reference va_arg after it destroys all the arguments. Our my_exec function should be called as follows:
my_exec("foo", "bar", "xyzzy", (char *) 0);
Casting to 0 is required because there is no context for interpreting it as a null pointer constant: the compiler does not know that the intended type for this argument is a pointer type. Also, (void *) 0 incorrect because it will just be passed as a void * type, not a char * , although the two are almost certainly binary compatible, so it will work in practice. A common mistake with this type of exec function is the following:
my_exec("foo", "bar", "xyzzy", NULL);
where the NULL compiler NULL defined as 0 without quotes (void *) .
Another possible scheme is to require the caller to move a number that indicates how many arguments exist. Of course, this number may be incorrect.
In the case of printf the format string describes a list of arguments. The function parses it and extracts the arguments accordingly.
As mentioned earlier, some compilers, in particular the GNU C compiler, can parse format strings at compile time and perform a static type check on the number and type of arguments.
However, note that the format string may be different from the literal and may be calculated at startup time, which is immune to type checking schemes. Dummy example:
char *fmt_string = message_lookup(current_language, message_code); snprintf(buffer, sizeof buffer, fmt_string, arg1, arg2, arg3);