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_list
variable.va_arg
: A macro to retrieve each argument from the list.va_end
: A macro to clean up theva_list
variable.
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_list
variable. - 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:
&last
gives the address of the last fixed argument.&last + last
moves the pointer to the next argument in the list, which is the first variable argument.ap
is 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:
30
at address0x...14
20
at address0x...10
10
at address0x...0C
3
(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 args
is declared, which is achar*
.va_start(args, count)
is called, whereargs
is theva_list
variable, andcount
is the last fixed argument.
Address Calculation:
- The address of
count
is0x...08
. &count + 1
computes the address immediately followingcount
.- In pointer arithmetic,
&count
is0x...08
. &count + 1
moves to0x...08 + sizeof(int)
, which is0x...08 + 4 = 0x...0C
.- After this step
args
variable 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)
setsargs
to point to the first variable argument.- Address calculation:
&count
is0x...08
, so&count + 1
correctly 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 Address
3️⃣ 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_list
variable. - The
type
of 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)
setsargs
to point to the first variable argument.args
initially 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)
: Movesargs
from0x...0C
to0x...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:args
pointing at0x...10
va_arg(args, int)
:- Increment Pointer:
ap += sizeof(int)
movesargs
from0x...10
to0x...14
- Subtract and Dereference:
(ap - sizeof(int))
gives the current value which is0x...10
by subtracting the size ofint
.*(int*)(0x...10)
= dereferences and returns the value at0x...10
which 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:args
pointing at0x...14
va_arg(args, int)
:- Increment Pointer
ap += sizeof(int)
movesargs
to point to the next integer address which is0x...18
.
- Subtract the size of
int
and dereference:(ap - sizeof(int))
gives the current value which is0x...14
by subtracting the size ofint
.*(int*)(0x...14)
= dereferences and returns the value at0x...14
which 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 Address
4️⃣ 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
args
toNULL
indicating 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_list
using GCC's built-in__builtin_va_list
.#define va_start(ap, last) __builtin_va_start(ap, last)
: This definesva_start
using the built-in__builtin_va_start
.#define va_arg(ap, type) __builtin_va_arg(ap, type)
: This definesva_arg
using the built-in__builtin_va_arg
.#define va_end(ap) __builtin_va_end(ap)
: This definesva_end
using the built-in__builtin_va_end
.#define va_copy(dest, src) __builtin_va_copy(dest, src)
: This definesva_copy
using 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++);
}
}