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.
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 the va_list variable.
va_arg: A macro to retrieve each argument from the list.
va_end: A macro to clean up the va_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 address 0x...14
20 at address 0x...10
10 at address 0x...0C
3 (the count) at address 0x...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 a char*.
va_start(args, count) is called, where args is the va_list variable, and count is the last fixed argument.
Address Calculation:
The address of count is 0x...08.
&count + 1 computes the address immediately following count.
In pointer arithmetic, &count is 0x...08.
&count + 1 moves to 0x...08 + sizeof(int), which is 0x...08 + 4 = 0x...0C.
After this step args variable would be pointing to the very first variable argument.
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.
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) sets args to point to the first variable argument.
args initially points to 0x...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): Moves args from 0x...0C to 0x...10.
Adjust Pointer Back and Dereference:
(ap - sizeof(int)) gives the current address.
*(int*)(0x...0C): Dereferences the value at 0x...0C, retrieving 10.
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 to NULL 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 defines va_list using GCC's built-in __builtin_va_list.
#define va_start(ap, last) __builtin_va_start(ap, last): This defines va_start using the built-in __builtin_va_start.
#define va_arg(ap, type) __builtin_va_arg(ap, type): This defines va_arg using the built-in __builtin_va_arg.
#define va_end(ap) __builtin_va_end(ap): This defines va_end using the built-in __builtin_va_end.
#define va_copy(dest, src) __builtin_va_copy(dest, src): This defines va_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.
Localhost refers to the local computer, mapped to IP `127.0.0.1`. It is essential for development, allowing testing and debugging services on the same machine. This article explains its role, shows how to modify the hosts file in Linux and Windows.