25.2 Writing a ``varargs'' Function

In ANSI C, the head of a varargs function looks just like its prototype. We will illustrate by writing our own, stripped-down version of printf. The bare outline of the function definition will look like this:

	void myprintf(const char *fmt, ...)
	{
	}
printf's job, of course, is to print its format string while looking for % characters and treating them specially. So the main loop of printf will look like this:
	#include <stdio.h>

	void myprintf(const char *fmt, ...)
	{
	const char *p;

	for(p = fmt; *p != '\0'; p++)
		{
		if(*p != '%')
			putchar(*p);
		else	{
			handle it specially
			}
		}
	}
In this stripped-down version, we won't worry about width and precision specifiers and other modifiers; we'll always look at the very next character after the % and assume that it's the primary format character. Continuing to flesh out our outline, we get this:
	#include <stdio.h>

	void myprintf(const char *fmt, ...)
	{
	const char *p;

	for(p = fmt; *p != '\0'; p++)
		{
		if(*p != '%')
			{
			putchar(*p);
			continue;
			}

		switch(*++p)
			{
			case 'c':
				fetch and print a character
				break;

			case 'd':
				fetch and print an integer
				break;

			case 's':
				fetch and print a string
				break;

			case 'x':
				print an integer, in hexadecimal
				break;

			case '%':
				print a single %
				break;
			}
		}
	}
(For clarity, we've rearranged the former if/else statement slightly. If the character we're looking at is not %, we print it out and continue immediately with the next iteration of the for loop. This is a good example of the use of the continue statement. Everything else in the body of the loop then takes care of the case where we are looking at a %.)

Printing these various argument types out will be relatively straightforward. The $64,000 question, of course, is how to fetch the actual arguments. The answer involves some specialized macros defined for us by the standard header <stdarg.h>. The macros we will use are va_list, va_start(), va_arg(), and va_end(). va_list is a special ``pointer'' type which allows us to manipulate a variable-length argument list. va_start() begins the processing of an argument list, va_arg() fetches arguments from it, and va_end() finishes processing. (Therefore, va_list is a little bit like the stdio FILE * type, and va_start is a bit like fopen.)

Here is the final version of our myprintf function, illustrating the fetching, formatting, and printing of the various argument types. (For simplicity--of presentation, if nothing else--the formatting step is deferred to a version of the nonstandard but popular itoa function.)

#include <stdio.h>
#include <stdarg.h>

extern char *itoa(int, char *, int);

void myprintf(const char *fmt, ...)
{
const char *p;
va_list argp;
int i;
char *s;
char fmtbuf[256];

va_start(argp, fmt);

for(p = fmt; *p != '\0'; p++)
	{
	if(*p != '%')
		{
		putchar(*p);
		continue;
		}

	switch(*++p)
		{
		case 'c':
			i = va_arg(argp, int);
			putchar(i);
			break;

		case 'd':
			i = va_arg(argp, int);
			s = itoa(i, fmtbuf, 10);
			fputs(s, stdout);
			break;

		case 's':
			s = va_arg(argp, char *);
			fputs(s, stdout);
			break;

		case 'x':
			i = va_arg(argp, int);
			s = itoa(i, fmtbuf, 16);
			fputs(s, stdout);
			break;

		case '%':
			putchar('%');
			break;
		}
	}

va_end(argp);
}

Looking at the new lines, we have:

	#include <stdarg.h>
This header file is required in any file which uses the variable argument list (va_) macros.
	va_list argp;
This line declares a variable, argp, which we use while manipulating the variable-length argument list. The type of the variable is va_list, a special type defined for us by <stdarg.h>.
	va_start(argp, fmt);
This line initializes argp and initiates the processing of the argument list. The second argument to va_start() is simply the name of the function's last fixed argument. va_start() uses this to figure out where the variable arguments begin.
	i = va_arg(argp, int);
And here's the heart of the matter. va_arg() fetches the next argument from the argument list. The second argument to va_arg() is the type of the argument we expect. Notice carefully that we must supply this argument, which implies that we must somehow know what type of argument to expect next. The variable-length argument list machinery does not know. In this case, we know what the type of the next argument should be because it's supposed to match the format character we're processing. We can see, then, why such havoc results when printf's arguments do not match its format string: printf tells the va_arg machinery to grab an argument of one type, with the type determined by one of the format specifiers, but since the va_arg machinery doesn't know what the actual argument type is, there's no way for it to do any automatic conversion. If the actual argument has the right type for the va_arg call which grabs it (as of course it's supposed to), it works, otherwise it doesn't.

(You may have noticed that we fetched the character to print for %c as an int, not a char. That's deliberate, and is explained in the next section.)

	s = va_arg(argp, char *);
Here's another invocation of va_arg(), this time fetching a string, represented as a character pointer, or char *.
	va_end(argp);
Finally, when we're all finished processing the argument list, we call va_end(), which performs any necessary cleanup.


Read sequentially: prev next up top

This page by Steve Summit // Copyright 1996-1999 // mail feedback