As long as Strings are allocated and de-allocated in a LIFO (last in, first out) fashion, there are no problems. However, in most assignments or concatenations, this is not the case.
Imagine the simple case of concatenating the Strings "Hello,·", "World" and '!':
String res = String("Hello, ") + "World" + '!';
There's much more going on than you might think at first sight:
-
String("Hello, ")
is constructed and allocates two blocks of heap memory*. This is String 0
Memory:
┌── String 0
[color=red]XX[/color]
-
A new String is constructed to store the result of the concatenation String("Hello, ") + "World"
. The heap memory doesn't change yet. This is String 1
.
-
String 0
is assigned to String 1
. This means that two blocks of memory are allocated by String 1
.
Memory:
┌───── String 0
│ ┌── String 1
[color=red]XX XX[/color]
- The string literal
"World"
is concatenated to String 1
using a StringSumHelper. Another block of memory is allocated by String 1
.
Memory:
┌────── String 0
│ ┌─── String 1
[color=red]XX XXX[/color]
-
The character '!'
is concatenated to String 1
. In this case, no further allocation is needed, because the String class had allocated some more than it really needed.
-
String res
is constructed. It doesn't allocate any memory yet. This is String 2
.
-
String 1
(the temporary result of the concatenation) is assigned to String 2
. This means that String 2
has to allocates three blocks of memory.
Memory:
┌────────── String 0
│ ┌─────── String 1
│ │ ┌─── String 2
[color=red]XX XXX XXX[/color]
-
String 1
is destructed and its three blocks of heap memory are de-allocated.
┌────────── String 0
│ ┌─────── [s]String 1[/s]
│ │ ┌─── String 2
[color=red]XX[/color] [color=green]···[/color] [color=red]XXX[/color]
-
String 0
is destructed and its two blocks of heap memory are de-allocated.
┌────────── [s]String 0[/s]
│ ┌─────── [s]String 1[/s]
│ │ ┌─── String 2
[color=green]·· ···[/color] [color=red]XXX[/color]
As you can see, now there are 5 blocks of memory that are squeezed between the start of the heap and the String res
. If you want to allocate more than 5 blocks of memory, you have to increase the heap size, you cannot use the free space at the start of the heap because it's not large enough.
This is called heap fragmentation, and it's a huge problem on microcontrollers with little RAM.
A simple solution would be to scope the concatenation in such a way that the assignment to the result variable happens after the temporary Strings have been destructed:
String res;
{
String temp = String("Hello, ") + "World" + '!';
res = temp;
}
Obviously, this is rather cumbersome.
Also, the peak heap usage will be higher, because there are more temporaries.
Another approach that overcomes the problem is pre-allocating space. In this case, you essentially get all String functionality, but with the memory benefits of fixed-length char arrays (as long as you don't have to increase the buffer size during the lifetime of the reserved String).
String reserved;
reserved.reserve(32);
reserved = String("Hello, ") + "World" + '!';
For more information, check my previous post on the topic as well as this repository to check String memory layout yourself: ArduinoStringExperiments.
Pieter
(*) I tested this on a 64-bit computer, so the pointer size (minimum free list entry size) is 8 bytes. On an 8-bit Arduino, this will be only 2 bytes.