I have just put this little sketch together to look how String behaves in memory
char x = 0;
String s1;
char y = 0;
void setup()
{
Serial.begin(115200);
}
void loop()
{
char z = 0;
String s2 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
Serial.print("global x @ 0x");
Serial.println((unsigned long)&x, HEX);
Serial.print("global s1 @ 0x");
Serial.print((unsigned long)&s1, HEX);
Serial.print(" sizeof(s1) ");
Serial.print(sizeof(s1), DEC);
Serial.print(" s1.buffer @ 0x");
Serial.print((unsigned long)(s1.buffer), HEX);
Serial.print(" allocated buffer ");
Serial.println(s1.capacity + 1, DEC);
Serial.print("global y @ 0x");
Serial.println((unsigned long)&y, HEX);
Serial.print("local z @ 0x");
Serial.println((unsigned long)&z, HEX);
Serial.print("local s2 @ 0x");
Serial.print((unsigned long)&s2, HEX);
Serial.print(" sizeof(s2) ");
Serial.print(sizeof(s2), DEC);
Serial.print(" s2.buffer @ 0x");
Serial.print((unsigned long)(s2.buffer), HEX);
Serial.print(" allocated buffer ");
Serial.println(s2.capacity + 1, DEC);
Serial.println("");
delay(1000);
}
This produced this output:
global x @ 0x200009C4
global s1 @ 0x200009C8 sizeof(s1) 16 s1.buffer @ 0x20002358 allocated buffer 1
global y @ 0x200009D8
local z @ 0x20004FCF
local s2 @ 0x20004FD0 sizeof(s2) 16 s2.buffer @ 0x20002368 allocated buffer 37
So it seems that each String instance consumes 16 byte for itself plus some memory to hold the actual string on the heap (via realloc). And as it seems although s1 is an empty string (+ \0) realloc for s2 places its buffer 16 byte further on (0x20002358 vs. 0x20002368).
Furthermore
the first global user var x is placed at 0x200009C4 (“global/user” section)
the first user heap object s1.buffer is placed at 0x20002358 (heap section) and
the first local user var z is placed at 0x20004FCF (stack section)
One other interesting thing to be noticed is the placement of s1 relative to x.
Although x should only occupy one byte in RAM it seems to take up four (0x9C4 - 0x9C7).
But in fact it does not. As @psb777 found out in this thread it actually only does use one single byte in RAM - for itself that is!
The “wasted” three byte here are caused by s1 - or more correctly s1.buffer, the first field of String class to be allocated in RAM.
Since this is declared as char *buffer it is a pointer and as such a four byte entity which demands to be placed on a four byte boundary, hence causing the three byte gap between x and s1.
This might also be the reason why @peekay123 sees 44 (= 1 (+3) + 16 + 16 + 1 (+3) + 4) bytes instead of 38 which you would expect once you add up the the datatype sizeof() (1 + 16 + 16 + 1 +4).
So to save some space you could rearrange your uint8_t to be placed adjacent wich would result in (1 + 1 (+2) + 16 + 16 + 4) 40 bytes.