Appending Characters to Strings in C++
Learn efficient ways to append characters to C++ strings using push_back, +=, append, and +. Compare time complexity, performance, and memory usage for optimal string manipulation.
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]
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.
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.
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.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.
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.
typedef char* va_list;
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:
va_list
variable.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 |
... | ... |
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:
count
is 0x...08
.&count + 1
computes the address immediately following count
.&count
is 0x...08
.&count + 1
moves to 0x...08 + sizeof(int)
, which is 0x...08 + 4 = 0x...0C
.args
variable would be pointing to the very first variable argument.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)
sets args
to point to the first variable argument.&count
is 0x...08
, so &count + 1
correctly points to 0x...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
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;
va_list
variable.type
of the argument to retrieve.Its definition is as follows:
#define va_arg(ap, type) (*(type*)((ap += sizeof(type)) - sizeof(type)))
// 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);
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 |
... | ... |
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.va_arg(args, int)
Call, i = 0:va_arg(args, int)
:ap += sizeof(int)
: Moves args
from 0x...0C
to 0x...10
.(ap - sizeof(int))
gives the current address.*(int*)(0x...0C)
: Dereferences the value at 0x...0C
, retrieving 10
.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
va_arg(args, int)
call, i = 1:args
pointing at 0x...10
va_arg(args, int)
:ap += sizeof(int)
moves args
from 0x...10
to 0x...14
(ap - sizeof(int))
gives the current value which is 0x...10
by subtracting the size of int
.*(int*)(0x...10)
= dereferences and returns the value at 0x...10
which is 20
.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
va_arg(args, int)
call, i = 2:args
pointing at 0x...14
va_arg(args, int)
:ap += sizeof(int)
moves args
to point to the next integer address which is 0x...18
.int
and dereference:(ap - sizeof(int))
gives the current value which is 0x...14
by subtracting the size of int
.*(int*)(0x...14)
= dereferences and returns the value at 0x...14
which is 30
.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
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)
args
to NULL
indicating the end of argument processing.#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;
}
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.
va_copy(dest, src); // Copies va_list variable src to the va_list variable dest.
#define va_copy(dest, src) (dest = src)
#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;
}
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)
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
.#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++);
}
}
Learn efficient ways to append characters to C++ strings using push_back, +=, append, and +. Compare time complexity, performance, and memory usage for optimal string manipulation.
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.