Function Arguments
In the olden days, (K&R C), certain function arguments were promoted:
Although this is mostly irrelevant for any new code you write, this will become an important issue when you use variable argument lists. (like printf or scanf)If the compiler doesn't know the type of the formal parameters when it generates code to call a function, the default argument promotions are performed.
From the C Standard 6.3.2.2:
If the expression that denotes the called function has a type that does not include a prototype, the integer promotions are performed on each argument, and arguments that have type float are promoted to double. These are called the default argument promotions. If the number of arguments does not equal the number of parameters, the behavior is undefined. If the function is defined with a type that includes a prototype, and either the prototype ends with an ellipsis (, ...) or the types of the arguments after promotion are not compatible with the types of the parameters, the behavior is undefined. If the function is defined with a type that does not include a prototype, and the types of the arguments after promotion are not compatible with those of the parameters after promotion, the behavior is undefined, except for the following cases:
Swap two integers | Swap two addresses | Swap two integers |
void swap1(int a, int b) { int temp; temp = a; a = b; b = temp; } |
void swap2(int *a, int *b) { int *temp; temp = a; a = b; b = temp; } |
void swap3(int *a, int *b) { int temp; temp = *a; *a = *b; *b = temp; } |
All of the swap functions are passing their arguments by value. How does it work?
int x = 5, y = 8; swap1(x, y); /* Doesn't work right */ swap1(5, 8); /* Won't work either and would be an abomination if it did! */ swap2(&x, &y); /* Still doesn't work quite right */ swap2(5, 8); /* Compile error */ swap3(&x, &y); /* Ok, works as expected */
What does the following code print out?
Version 1 Version 2 void change_pointer1(char *ptr) { ptr = "Bye"; } int main(void) { char *p = "Hello"; printf("p = %s\n", p); change_pointer1(p); printf("p = %s\n", p); return 0; } void change_pointer2(char **ptr) { *ptr = "Bye"; } int main(void) { char *p = "Hello"; printf("p = %s\n", p); change_pointer2(&p); printf("p = %s\n", p); return 0; }
It is ABSOLUTELY ESSENTIAL that you understand EXACTLY why the program above works the way it does. This is the whole key to understanding pointers and function parameters. Two things that you must master to be successful C and C++ programmers.
Variable Argument Lists
How does the compiler deal with printf and these calls? In other words, what is the signature of printf?
It seems that printf has multiple signatures. Actually, it has just one (simplified):char c = 'A'; int age = 20; float pi = 3.1415F; printf("c = %c, ", c); printf("age = %i, ", age); printf("pi = %f\n", pi); printf("pi = %f, age = %i\n", pi, age); printf("c = %c, age = %i, pi = %f\n", c, age, pi);
This syntax is for functions with a variable argument list.int printf(const char *format, ...);
int fn(...); /* Illegal. No named argument. Legal in C++, but can't portably access the args. */
Given this code:
We can visualize passing the arguments to printf something like this:char ch = 'A'; /* 'A' is ASCII 65 */ int age = 20; float pi = 3.1415F; const char *format = "c = %c, age = %i, pi = %f\n"; printf(format, ch, age, pi);
Because printf "knows" where to find the format argument on the stack, it can easily find all of the other arguments. The format string specifies the order and type (size) of the arguments.
Now can you see why bad things happen if the format specifier doesn't match the
type of the argument?
The shaded area shows which bytes are going to be read from the stack according to
the format specifier given to printf.
Using %f with ch Using %f with age Using %i with pi
A First Example
Let's create a function that takes the average of "a bunch" of integers. The size of "a bunch" can vary with each call. Here's what the prototype for our function might look like:
We would like to be able to call average like this:double average(int count, ...);
Output:double ave1, ave2, ave3; ave1 = average(5, 1, 2, 3, 9, 10); ave2 = average(7, 5, 8, 9, 2, 4, 4, 5); ave3 = average(3, 11, 2, 10); printf("ave1 = %f, ave2 = %f, ave3 = %f\n", ave1, ave2, ave3);
In the example above, the first argument to the average function was a count of how many numbers that the function should expect. So, how do we write such a function?ave1 = 5.000000, ave2 = 5.285714, ave3 = 7.666667
Here is one way:
In the example above, we specified the number of arguments that the function should expect.double average(int count, ...) { /* va_list is usually a typedef for char * */ va_list args; int i, total = 0; /* Initialize pointer to variable-length list. * args points into the stack after count's address. */ va_start(args, count); /* Sum all values in list */ for (i = 0; i < count; i++) { int next = va_arg(args, int); /* Next integer */ total += next; } /* Reset, required cleanup (may free memory, may do nothing) */ va_end(args); return (double)total / count; }
Output:int sum1, sum2, sum3; sum1 = sum(2, 4, 5, 7, 8, 0); sum2 = sum(12, 17, 28, 0); sum3 = sum(21, 41, 25, 17, 18, 3, 8, 23, 0); printf("sum1 = %i, sum2 = %i, sum3 = %i\n", sum1, sum2, sum3); /* Need to be careful! (What if sum1, or sum2, or sum3 is 0?) */ printf("sum = %i\n", sum(sum1, sum2, sum3, 0));
Implementation:sum1 = 26, sum2 = 57, sum3 = 156 sum = 239
Our sentinel value is 0. Choosing a good sentinel value is important and should be something that can never be a valid value in the list of args. A possible better choice than zero might be one of these:int sum(int first, ...) { va_list args; int total = 0; int next = first; /* first value is now data */ /* Initialize pointer to variable-length list */ va_start(args, first); /* Sum all values in the list (0 is the sentinel) */ while (next != 0) { total += next; next = va_arg(args, int); /* Next value */ } /* Reset, required cleanup */ va_end(args); return total; }
Both INT_MIN and INT_MAX are defined in stdint.h.INT_MIN, which has a value of -2147483648. INT_MAX, which has a value of +2147483647.
Realize also that, the examples used literal constants, but the inputs to the functions could have all been variables (that aren't known until runtime).
Suppose we wanted to write a function to print any number of strings. We'd use it like this:
And expect to see:print_strings("one", "two", "three", "four", NULL); print_strings("one", NULL); print_strings("one", "two", NULL, "four", NULL); print_strings(NULL);
The code would look something like this:one two three four one one two [empty line here]
void print_strings(const char *first, ...)
{
va_list args;
const char *next = first; /* first value */
/* Initialize pointer to args on the stack */
va_start(args, first);
/* Print all strings */
while (next != NULL)
{
printf("%s ", next);
next = va_arg(args, const char *); /* Next value */
}
printf("\n");
/* Reset, required cleanup */
va_end(args);
}
The va_ macros are defined in stdarg.h and might look like this (32-bit system):
It is not necessary to understand the details in order to use them. They are compiler-dependent, so you can't rely on any particular implementation.typedef char * va_list; #define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) ) #define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) ) #define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) ) #define va_end(ap) ( ap = (va_list)0 )
Since you use the ellipsis to "prototype" your function, the compiler doesn't know the types (sizes) of the parameters. The default argument promotion will occur so you will have to remember to retrieve int and double (never char, short, or float) in the function.
For example, this function won't work:no matter how you try to use it:float f_average(int count, ...) { va_list args; /* char* */ int i = 1; float total = 0; float next; /* Initialize pointer to first parameter in var list */ va_start(args, count); /* Sum all values in list */ for (i = 0; i < count; i++) { next = va_arg(args, float); /* Next float (+4 bytes) */ total += next; } /* Reset, required cleanup */ va_end(args); return total / count; }
This is because:/* Try to pass floats */ float f1 = f_average(5, 1.0F, 2.0F, 3.0F, 9.0F, 10.0F); /* Try to pass doubles */ float f2 = f_average(5, 1.0, 2.0, 3.0, 9.0, 10.0); /* Displays: f1 = 0.775000, f2 = 0.775000 */ printf("f1 = %f, f2 = %f\n", f1, f2);
should be this:next = va_arg(args, float); /* Won't work, only retrieves 4 bytes */
next = va_arg(args, double); /* Correct, retrieves 8 bytes */
This is the output from Clang at all warning levels:In function 'f_average': warning: 'float' is promoted to 'double' when passed through '...' next = va_arg(args, float); // Next float (+4 bytes) ^ note: (so you should pass 'double' not 'float' to 'va_arg') note: if this code is reached, the program will abort
Sadly, Microsoft's compiler says nothing, even with warnings set to the maximum.warning: second argument to 'va_arg' is of promotable type 'float'; this va_arg has undefined behavior because arguments will be promoted to 'double' [-Wvarargs] next = va_arg(args, float); // Next float (+4 bytes) ^~~~~
Usage:int sum2(int *array, int size) { int i, sum = 0; for (i = 0; i < size; i++) sum += array[i]; return sum; }
But this would require you to create an array instead of just sending all of the constants (or variables) to the function. Also, what if all of the types were not integers but include floating-point numbers as well? You can't mix types with an array.int array[] = {21, 41, 25, 17, 18, 3, 8, 23}; printf("sum = %i\n", sum2(array, 8));
Another Example: Mixing Types
The previous examples all passed the same data types to the functions. Suppose we want to write a function that can calculate the average of bunch of different types. We will assume the return value will be a double, since it's likely to include a fractional part. We need some way communicating the types of the arguments to the variadic function. Let's borrow the idea from printf. That is, we will provide a sort of "format string".
We will be able to pass integral types and floating-point types. For example, this is an example of such a call (on a 32-bit system):
Output:double ave = average2("ifif", 1, 2.0F, 3L, 4.0); printf("ave is %f\n", ave);
The type string is just a NUL-terminated string of characters. The only valid characters are i, for integral values and f for floating-point values. Notice that we no longer need to provide the actual count of arguments. The count is implied by the length of the type string. (This is similar to how printf knows how many arguments were provided.)ave is 2.500000
This is what the function might look like:
Notes:double average2(const char *types, ...) { va_list args; double total = 0; int count = 0; /* Initialize pointer to variable-length list */ va_start(args, types); /* Sum all values in list */ while (*types) { double next = 0; /* Only supports two types, no error checking is done */ if (*types == 'i') next = va_arg(args, int); else if (*types == 'f') next = va_arg(args, double); total += next; count++; /* Next format character */ types++; } /* Reset, required cleanup */ va_end(args); /* Prevent divide by 0 (unlikely) */ if (count) return total / count; else return 0.0; }
On most 32-bit systems, this will work fine because integers and longs are the same size (4 bytes). On 64-bit systems, things get more complicated:
Here's a more robust version:
/* Supports three types, no error checking is done */ if (*types == 'i') next = va_arg(args, int); else if (*types == 'l') next = va_arg(args, long); /* sizeof(long) varies */ else if (*types == 'f') next = va_arg(args, double);
Note: If long long is available (and is the same size as a long), it will work for GNU and Microsoft./* Supports three types, no error checking is done */ if (*types == 'i') next = va_arg(args, int); else if (*types == 'l') next = va_arg(args, long long); /* See note below */ else if (*types == 'f') next = va_arg(args, double);
Other Real World™ Uses
Suppose you have a variadic function that needs to pass all of the arguments to another function. You don't know what the types of the arguments are, nor do you care. You just need to forward them to a function that takes a variable number of arguments (like printf).
Here's an example of a program that wants to log all kinds of information about the state of the program. You may want to log integers, longs, strings, doubles, etc. Basically, any number and type of values. This sounds like a perfect case for a variadic function.
This is how the program may want to log information. (The sleep function just causes the program to pause for the specified number of seconds, simulating the time between log events.) Instead of just using printf to print the information, we want to put a time and date stamp on the output. In practice, you could add any kind of other information, as well.
The output might look like this:#include <stdio.h> /* prototype for logging */ void logit(const char *fmt_string, ...); int main(void) { /* Arbitrary data */ int i = 10; char c = 65; float f = 1.24F; double d = 3.14; char *s = "foobarbaz"; /* Pretend to do stuff ... */ logit("i is %i, c is %c, f is %f, d is %f, s is %s\n", i, c, f, d, s); sleep(1); logit("i is %i, c is %c, f is %f, d is %f, s is %s\n", i * 3, c + 7, f / 1.1, d / 1.2, s + 3); sleep(2); logit("i is %i, c is %c, f is %f, d is %f, s is %s\n", i + 8, c + 3, f * 3, d + 6, s + 6); sleep(1); logit("i is %i, c is %c, f is %f, d is %f, s is %s\n", i + 9, c + 2, f * 2, d + 5, s + 3); return 0; }
Here's what the logit function might look like within the program:Wednesday, February 23, 2022 12:14:33 PM PST: i is 10, c is A, f is 1.240000, d is 3.140000, s is foobarbaz Wednesday, February 23, 2022 12:14:34 PM PST: i is 30, c is H, f is 1.127273, d is 2.616667, s is barbaz Wednesday, February 23, 2022 12:14:36 PM PST: i is 18, c is D, f is 3.720000, d is 9.140000, s is baz Wednesday, February 23, 2022 12:14:37 PM PST: i is 19, c is C, f is 2.480000, d is 8.140000, s is barbaz
#include <stdio.h> /* printf, vprintf */
#include <stdarg.h> /* va_list, va_start, va_end */
#include <time.h> /* time, strftime, localtime */
#include <unistd.h> /* sleep (non-standard) */
#define MAX_TIME_LEN 256
void logit(const char *fmt_string, ...)
{
va_list args; /* to access the arguments passed in */
struct tm *pt; /* to convert the date/time */
time_t now; /* to hold the current date/time */
char buf[MAX_TIME_LEN]; /* buffer to hold formatted date/time */
/* Get the current system time (number of seconds since January 1, 1970) */
now = time(NULL);
/* Convert to local time */
pt = localtime(&now);
/* Format: Weekday, Month Day, Year HH:MM:SS AM/PM Timezone */
strftime(buf, sizeof(buf), "%A, %B %d, %Y %I:%M:%S %p %Z", pt);
/* Print formatted date/time */
printf("%s: ", buf);
/* Fetch the rest of the arguments and print them.
* This is where the "magic" happens.
*/
va_start(args, fmt_string);
vprintf(fmt_string, args);
/* Reset, required cleanup */
va_end(args);
}
int main(void)
{
/* Fake data */
int i = 10;
char c = 65;
float f = 1.24F;
double d = 3.14;
char *s = "foobarbaz";
/* Pretend to do stuff ... */
logit("i is %i, c is %c, f is %f, d is %f, s is %s\n", i, c, f, d, s);
sleep(1);
logit("i is %i, c is %c, f is %f, d is %f, s is %s\n", i * 3, c + 7, f / 1.1, d / 1.2, s + 3);
sleep(2);
logit("i is %i, c is %c, f is %f, d is %f, s is %s\n", i + 8, c + 3, f * 3, d + 6, s + 6);
sleep(1);
logit("i is %i, c is %c, f is %f, d is %f, s is %s\n", i + 9, c + 2, f * 2, d + 5, s + 3);
return 0;
}
There are a family of functions for this purpose: