If you've programmed in C, you've likely used functions like printf that accept a variable number of arguments. You might have wondered how this is possible. Well in this article, we will demystify the working of variadic function like printf.
Table of contents [Show]
❔ What is Variadic Function?
A variadic function is a function that accepts a variable number of arguments. In C, this is commonly seen with functions like printf and scanf.
These functions are useful in situations where the number of arguments needed is not known beforehand.
Variadic function are achieved through a special set of macros defined in the <stdarg.h> header file. These macros include va_list, va_start, va_arg, and va_end. These macros allows the programmer to access and manipulate the variable arguments passed to the function.
Variadic Function Declaration
To declare a variadic function, you use an ellipsis (...) in the parameter list to indicate that the function accepts a variable number of arguments.
// Example of a variadic function declaration
void printNumbers(int count, ...);In this example, printNumbers is a variadic function that takes an integer count followed by a variable number of additional arguments.
Accessing Arguments
To access the variable arguments, you need to include the <stdarg.h> header file and use the following macros:
va_list: A type to hold the information about the variable arguments.va_start: A macro to initialize theva_listvariable.va_arg: A macro to retrieve each argument from the list.va_end: A macro to clean up theva_listvariable.
Macros:
These macros are typically define in <stdarg.h> header file. We can define these macros right from scratch or could use the built-in macros provided by the gcc.
Below is the way to define them right from the scratch.
1 Own Implementation from Scratch:
1️⃣ va_list:
It is a type that acts as a pointer or handle to the variable argument list. Its definition is compiler-specific. Typically, it stores the current position in the argument list. It is a pointer type that can iterate over the arguments.
- Typically it is pointer to char data type.
typedef char* va_list;2️⃣ va_start:
It initializes the va_list variable to point to the first variable argument. It requires the last fixed argument to determine where the variable arguments start.
It takes two parameters:
- the
va_listvariable. - the last fixed parameter before the variable arguments.
Its Definition is as follows:
#define va_start(ap, last) (ap = (va_list)(&last + sizeof(last)))Explanation:
&lastgives the address of the last fixed argument.&last + lastmoves the pointer to the next argument in the list, which is the first variable argument.apis set to this address.
// OR
/*
* This macro calculates the size of a type n rounded
* up to the
nearest multiple of sizeof(int).
* This is necessary for
proper alignment.
*/
#define _INTSIZEOF(n) ((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1))
/* This macro initializes the va_list variable ap to
* point to the
first variable argument.
* It does this by advancing past the last
named
* parameter v.
*/
#define va_start(ap, v) (ap = (va_list)&v + _INTSIZEOF(v))Visualization:
Consider we have the function:
void printNumbers(int count, ...) {
va_list args;
va_start(args, count); // Initialize va_list to retrieve the arguments
//... Process arguments
}And we call this function as follows:
printNumbers(3, 10, 20, 30);
Here, 3 is the fixed argument, while 10, 20, 30 are variable arguments.
When the printNumbers(3, 10, 20, 30) is called, the stack layout in x86 architecture might look like this:
| Stack Address | Value |
|---|---|
| ... | ... |
| 0x…14 | 30 |
| 0x…10 | 20 |
| 0x…0C | 10 |
| 0x…08 | 3 |
| ... | ... |
Note:
- In x86 Architecture:
- The stack grows from higher memory to low memory.
- Arguments are passed onto the stack in order from right to left. The most left argument would be the first one pushed onto the stack followed by the second argument from the left and so on.
The arguments are pushed onto the stack in reverse order:
30at address0x...1420at address0x...1010at address0x...0C3(the count) at address0x...08
Next we declared a variable va_list args;, which is then passed to the va_start along with the fixed parameter.
va_list argsis declared, which is achar*.va_start(args, count)is called, whereargsis theva_listvariable, andcountis the last fixed argument.
Address Calculation:
- The address of
countis0x...08. &count + 1computes the address immediately followingcount.- In pointer arithmetic,
&countis0x...08. &count + 1moves to0x...08 + sizeof(int), which is0x...08 + 4 = 0x...0C.- After this step
argsvariable would be pointing to the very first variable argument.
- In pointer arithmetic,
1 Stack Before va_start:
Higher Memory Address
+--------------+ <--- Stack grows downwards (higher to
| ... | lower memory)
+--------------+
| 30 | <--- 0x...14 (arg3)
+--------------+
| 20 | <--- 0x...10 (arg2)
+--------------+
| 10 | <--- 0x...0C (arg1)
+--------------+
| 3 | <--- 0x...08 (count)
+--------------+
| ... |
+--------------+
Lower Memory Address
2 Stack After va_start(args, count):
va_start(args, count)setsargsto point to the first variable argument.- Address calculation:
&countis0x...08, so&count + 1correctly points to0x...0C.
Higher Memory Address
+--------------+ <--- Stack grows downwards (higher to
| ... | lower memory)
+--------------+
| 30 | <--- 0x...14 (arg3)
+--------------+
| 20 | <--- 0x...10 (arg2)
+--------------+
| 10 | <--- 0x...0C (arg1) <-- args
| | (after va_start)
+--------------+
| 3 | <--- 0x...08 (count)
+--------------+
| ... |
+--------------+
Lower Memory Address3️⃣ va_arg:
The va_arg macro is used to retrieve the next argument in the list of arguments provided to a variadic function. It updates the va_list to point to the next argument and returns the current argument, cast to the specified type.
It takes two parameters;
- The
va_listvariable. - The
typeof the argument to retrieve.
Its definition is as follows:
#define va_arg(ap, type) (*(type*)((ap += sizeof(type)) - sizeof(type)))
- It does the following things:
- Incrementing the pointer to point to the next argument.
- Casting the current pointer to the specified type.
- Returning the value at the current pointer.
// OR
/* This macro retrieves the next argument of type t
* from the va_list
and advances the va_list
* pointer ap.
*/
#define va_arg(ap, t) (*(t*)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)))Visualization:
Consider the example given in the va_start section. Let's modify that example to process the variable arguments using va_arg.
void printNumbers(int count, ...) {
va_list args;
va_start(args, count); // Initialize va_list to retrieve the arguments
for (int i = 0; i < count; ++i) {
int num = va_arg(args, int); // Retrieve the next argument
printf("%d ", num);
}
// Todo clean up the va_list
}In this example we are using va_arg macro in the loop, whose iteration is based on the count which is received as the fixed argument. The va_arg will be called three times for i = 0, i = 1, and i = 2.
We call this function as follows:
printNumbers(3, 10, 20, 30);Working:
As we call the function, the following stack memory layout is formed. As we all know that in x86 architecture parameters are pushed into the stack in reverse order (right to left) and stack grows from higher memory to lower memory. This means that the firstly pushed item would be at higher memory address than to items pushed after it.
| Stack Address | Value |
|---|---|
| ... | ... |
| 0x…14 | 30 |
| 0x…10 | 20 |
| 0x…0C | 10 |
| 0x…08 | 3 |
| ... | ... |
- Initialization with
va_start:va_start(args, count)setsargsto point to the first variable argument.argsinitially points to0x...0C. The address of the first variable argument.
- First
va_arg(args, int)Call, i = 0:va_arg(args, int):- Increment Pointer:
ap += sizeof(int): Movesargsfrom0x...0Cto0x...10.
- Adjust Pointer Back and Dereference:
(ap - sizeof(int))gives the current address.*(int*)(0x...0C): Dereferences the value at0x...0C, retrieving10.
- Increment Pointer:
Updated args:
Higher Memory Address
+--------------+<--- Stack grows downwards
| ... |
+--------------+
| 30 |<--- 0x...14 (arg3)
| |
+--------------+
| 20 |<--- 0x...10 (arg2) <--args (after
| | first va_arg)
+--------------+
| 10 |<--- 0x...0C (arg1)
| |
+--------------+
| 4 |<--- 0x...08 (count)
| |
+--------------+
| ... |
+--------------+
Lower Memory Address- Second
va_arg(args, int)call, i = 1:argspointing at0x...10va_arg(args, int):- Increment Pointer:
ap += sizeof(int)movesargsfrom0x...10to0x...14
- Subtract and Dereference:
(ap - sizeof(int))gives the current value which is0x...10by subtracting the size ofint.*(int*)(0x...10)= dereferences and returns the value at0x...10which is20.
- Increment Pointer:
Updated args:
Higher Memory Address
+--------------+<--- Stack grows downwards
| ... |
+--------------+
| 30 |<--- 0x...14 (arg3) <--- args (after second
| | va_arg)
+--------------+
| 20 |<--- 0x...10 (arg2)
| |
+--------------+
| 10 |<--- 0x...0C (arg1)
| |
+--------------+
| 4 |<--- 0x...08 (count)
| |
+--------------+
| ... |
+--------------+
Lower Memory Address- Third
va_arg(args, int)call, i = 2:argspointing at0x...14va_arg(args, int):- Increment Pointer
ap += sizeof(int)movesargsto point to the next integer address which is0x...18.
- Subtract the size of
intand dereference:(ap - sizeof(int))gives the current value which is0x...14by subtracting the size ofint.*(int*)(0x...14)= dereferences and returns the value at0x...14which is30.
- Increment Pointer
Updated args:
Higher Memory Address
+--------------+<--- Stack grows downwards
| ... |
+--------------|
| garbage value|<--- 0x...18 (garbage value) <-- args
| | (after third va_arg)
+--------------+
| 30 |<--- 0x...14 (arg3)
| |
+--------------+
| 20 |<--- 0x...10 (arg2)
| |
+--------------+
| 10 |<--- 0x...0C (arg1)
| |
+--------------+
| 4 |<--- 0x...08 (count)
| |
+--------------+
| ... |
+--------------+
Lower Memory Address4️⃣ va_end:
The va_end macro is used to clean up a va_list variable after it has been used in a variadic function. It is a necessary part of using variadic functions because it ensures that any resources allocated for the va_list are properly released.
It takes one parameter:
va_list
It's definition is as follows:
#define va_end(ap) (ap = (va_list)0)
//OR
#define va_end(ap) (ap = (va_list) NULL)- It sets received
argstoNULLindicating the end of argument processing.
Complete Example:
#include <stdio.h>
typedef char* va_list;
#define va_start(ap, last) (ap = (va_list)(&last + 1))
#define va_arg(ap, type) (*(type*)((ap += sizeof(type)) - sizeof(type)))
#define va_end(ap) (ap = (va_list)0)
void printNumbers(int count, ...) {
va_list args;
va_start(args, count); // Initialize va_list to retrieve the arguments
for (int i = 0; i < count; ++i) {
int num = va_arg(args, int); // Retrieve the next argument
printf("%d ", num);
}
va_end(args); // Clean up the va_list
printf("\n");
}
int main() {
printNumbers(4, 10, 20, 30, 40);
printNumbers(3, 5, 15, 25);
return 0;
}5️⃣ va_copy:
The va_copy macro is used to create a copy of a va_list variable. This can be particularly useful when you need to iterate over the same set of arguments multiple times within a function.
Syntax:
va_copy(dest, src); // Copies va_list variable src to the va_list variable dest.Implementation:
#define va_copy(dest, src) (dest = src)Example:
#include <stdio.h>
// Define a type for the argument list (assuming a pointer implementation)
typedef char* va_list;
// Define the size of a pointer for alignment
#define VA_ARG_SIZE(type) (((sizeof(type) + sizeof(int) - 1) / sizeof(int)) * sizeof(int))
// Define the custom macros for variadic argument handling
#define va_start(ap, last) (ap = (va_list)&last + VA_ARG_SIZE(last))
#define va_arg(ap, type) (*(type*)((ap += VA_ARG_SIZE(type)) - VA_ARG_SIZE(type)))
#define va_end(ap) (ap = (va_list)0)
#define va_copy(dest, src) (dest = src)
// Variadic function that sums all integer arguments twice for demonstration
int sum_twice(int count, ...) {
va_list args1, args2;
va_start(args1, count);
// Copy args1 to args2 using custom va_copy
va_copy(args2, args1);
int total1 = 0;
for (int i = 0; i < count; i++) {
total1 += va_arg(args1, int);
}
int total2 = 0;
for (int i = 0; i < count; i++) {
total2 += va_arg(args2, int);
}
va_end(args1);
va_end(args2);
return total1 + total2;
}
int main() {
printf("Sum twice: %d\n", sum_twice(4, 1, 2, 3, 4)); // Output: Sum twice: 20
return 0;
}
2 Implementation Using Built-in Macros:
Some compilers provide built-in macros that can be used to implement variadic functions in more direct way.
GCC provides built-in macros like __builtin_va_list, __builtin_va_start, __builtin_va_arg, and __builtin_va_end which can be used instead of defining own va macros.
// Define a type for the argument list
typedef __builtin_va_list va_list;
// Define the built-in macros for variadic argument handling
#define va_start(ap, last) __builtin_va_start(ap, last)
#define va_arg(ap, type) __builtin_va_arg(ap, type)
#define va_end(ap) __builtin_va_end(ap)
#define va_copy(dest, src) __builtin_va_copy(dest, src)Explanation:
typedef __builtin_va_list va_list;: This definesva_listusing GCC's built-in__builtin_va_list.#define va_start(ap, last) __builtin_va_start(ap, last): This definesva_startusing the built-in__builtin_va_start.#define va_arg(ap, type) __builtin_va_arg(ap, type): This definesva_argusing the built-in__builtin_va_arg.#define va_end(ap) __builtin_va_end(ap): This definesva_endusing the built-in__builtin_va_end.#define va_copy(dest, src) __builtin_va_copy(dest, src): This definesva_copyusing the built-in__builtin_va_copy.
Note:
- Using built-in macros can be compiler-specific. The example above is specific to GCC.
- Ensure that your compiler supports these built-in macros before using them.
#include <stddef.h>
#include <stdarg.h>
static void itoa(int value, char* str, int base) {
char* ptr = str;
char* ptr1 = str;
char tmp_char;
int tmp_value;
if (value == 0) {
*str++ = '0';
*str = '\0';
return;
}
while (value != 0) {
tmp_value = value % base;
*ptr++ = (tmp_value < 10) ? (tmp_value + '0') : (tmp_value - 10 + 'a');
value /= base;
}
*ptr-- = '\0';
while (ptr1 < ptr) {
tmp_char = *ptr;
*ptr = *ptr1;
*ptr1 = tmp_char;
ptr--;
ptr1++;
}
}
int vsprintf(char* buffer, const char* format, va_list args) {
char* buf_ptr = buffer;
const char* fmt_ptr = format;
char ch;
char tmp[32];
while ((ch = *fmt_ptr++) != '\0') {
if (ch != '%') {
*buf_ptr++ = ch;
continue;
}
ch = *fmt_ptr++;
switch (ch) {
case 'd': {
int value = va_arg(args, int);
itoa(value, tmp, 10);
for (char* tmp_ptr = tmp; *tmp_ptr != '\0'; tmp_ptr++) {
*buf_ptr++ = *tmp_ptr;
}
break;
}
case 'x': {
int value = va_arg(args, int);
itoa(value, tmp, 16);
for (char* tmp_ptr = tmp; *tmp_ptr != '\0'; tmp_ptr++) {
*buf_ptr++ = *tmp_ptr;
}
break;
}
case 's': {
char* str = va_arg(args, char*);
while (*str != '\0') {
*buf_ptr++ = *str++;
}
break;
}
case 'c': {
char value = (char)va_arg(args, int);
*buf_ptr++ = value;
break;
}
default: {
*buf_ptr++ = ch;
break;
}
}
}
*buf_ptr = '\0';
return buf_ptr - buffer;
}
void printf(const char* format, ...) {
va_list args;
va_start(args, format);
char buffer[256]; // Example buffer size
vsprintf(buffer, format, args);
va_end(args);
puts(buffer);
}void puts(const char* str) {
while (*str) {
putchar(*str++);
}
}







