One of the most consequential choices in Arduino programming is whether to use the built-in String class or plain C-style char arrays. The arduino string vs char array debate matters enormously on memory-constrained microcontrollers: the wrong choice can cause subtle crashes, heap fragmentation, and mysteriously failing code on an Arduino Uno with just 2 KB of SRAM. This guide gives you a clear mental model, concrete benchmarks, and practical patterns to write memory-efficient Arduino code that actually runs reliably.
Table of Contents
- Arduino Memory Model: SRAM, Flash, EEPROM
- The Arduino String Class: Convenience at a Cost
- C-Style char Arrays: Low-Level but Reliable
- Head-to-Head Comparison
- When to Use String
- When to Use char Arrays
- Practical Memory-Efficient Patterns
- Frequently Asked Questions
Arduino Memory Model: SRAM, Flash, EEPROM
To understand the String vs char array debate, you must first understand where data lives on an Arduino:
- Flash (Program Memory): 32 KB on Uno, 256 KB on Mega. Stores your compiled sketch. Read-only at runtime. String literals go here unless copied to SRAM.
- SRAM (Static RAM): 2 KB on Uno, 8 KB on Mega. Stores global variables, local variables (stack), and dynamically allocated memory (heap). This is the critical bottleneck.
- EEPROM: 1 KB on Uno. Non-volatile storage, limited write cycles. Slow to access.
The SRAM is divided into three regions that grow toward each other:
- Static/global variables: Allocated at compile time, grow upward from address 0x0100.
- Heap: Dynamic allocations (
malloc,new,String) grow upward. - Stack: Function call frames and local variables grow downward from the top of SRAM.
When heap and stack collide, you get undefined behaviour — corrupted variables, random resets, or hard lock-ups. On a 2 KB Uno, this can happen with as little as 200–300 bytes of dynamic allocation.
The Arduino String Class: Convenience at a Cost
The String class (capital S) is part of the Arduino core library. It provides a high-level interface similar to Java or Python strings:
String name = "Zbotic";
String greeting = "Hello, " + name + "!";
Serial.println(greeting);
String sensor = "Temp: " + String(25.6, 1) + "°C";
int len = greeting.length();
bool found = greeting.indexOf("Hello") >= 0;
String upper = greeting;
upper.toUpperCase();
Internally, String uses dynamic heap allocation. Every time you concatenate, create, or resize a String, the class calls realloc() — which may allocate a new block, copy the data, and free the old block. Over time, this creates heap fragmentation: the free heap becomes a patchwork of small unusable gaps even though the total free memory appears adequate.
Fragmentation Example
// This loop causes fragmentation:
void loop() {
String msg = "Sensor: ";
msg += analogRead(A0); // realloc #1
msg += " Raw, ";
msg += analogRead(A0) * 5.0 / 1023.0; // realloc #2
msg += " V";
Serial.println(msg);
// msg destroyed here, its heap block freed
// but the freed block may not be contiguous with other free memory
// After hundreds of iterations: heap is fragmented, random crash
}
On a 2 KB Uno, this sketch typically crashes after a few hundred iterations. On an 8 KB Mega it may run for thousands of iterations before crashing.
C-Style char Arrays: Low-Level but Reliable
A C-style char array is a fixed-size block of memory, declared at compile time or as a local variable on the stack. No dynamic allocation, no fragmentation.
char name[] = "Zbotic"; // stack, 7 bytes (6 chars + null)
char greeting[50]; // stack, exactly 50 bytes
sprintf(greeting, "Hello, %s!", name);
Serial.println(greeting);
char sensor[30];
float temp = 25.6;
dtostrf(temp, 4, 1, sensor); // float → char array (no printf %f on AVR!)
Serial.println(sensor);
int len = strlen(greeting); // length (not including null)
bool found = strstr(greeting, "Hello") != NULL;
Standard C string functions (strcpy, strcat, strcmp, strlen, strstr, sprintf, snprintf) all work on Arduino. They live in <string.h> which is automatically included.
Important: printf and sprintf on AVR Arduino do NOT support %f for float formatting by default (it is stripped to save Flash). Use dtostrf() for float-to-string conversion, or link with printf_flt (adds ~1.5 KB Flash).
Head-to-Head Comparison
| Feature | String Class | char Array |
|---|---|---|
| Memory allocation | Dynamic (heap) | Static/stack (fixed) |
| Fragmentation risk | High | None |
| Ease of use | Very easy | Moderate (C stdlib) |
| Concatenation | + operator (easy) | strcat / snprintf |
| Float conversion | String(val, decimals) | dtostrf() |
| Overflow protection | Automatic resize | Manual (use snprintf) |
| Long-running stability | Poor (fragmentation) | Excellent |
| Flash overhead | ~1.5 KB library code | Minimal (C stdlib) |
| SRAM per variable | 6–8 bytes metadata + heap | Exactly as declared |
When to Use String
Despite its drawbacks, the String class is acceptable in specific situations:
- One-time setup code: Building a WiFi SSID, constructing an HTTP request once at startup. No fragmentation builds up from a single allocation.
- Boards with abundant SRAM: ESP32 (520 KB SRAM), Arduino Mega (8 KB), Arduino Nano 33 IoT. Fragmentation is still possible but crashes are far less likely.
- Prototyping and testing: When correctness matters more than reliability, use String for rapid development, then refactor to char arrays before deployment.
- Bounded repetition: If the loop runs at most 10–20 times (not indefinitely), fragmentation may not accumulate enough to cause problems.
When to Use char Arrays
Always prefer char arrays for:
- Continuously running loops: Any sketch that runs loop() indefinitely and processes strings should use char arrays.
- Serial/UART message parsing: Incoming data buffers should be fixed-size char arrays.
- ATmega328P / ATmega168 boards (Uno, Nano, Pro Mini): 2 KB SRAM is too small for safe dynamic allocation in loops.
- Network message formatting: HTTP headers, JSON payloads, MQTT topics — all benefit from snprintf into fixed buffers.
- Any safety- or reliability-critical application: Production hardware must not crash randomly. Use char arrays.
Practical Memory-Efficient Patterns
Pattern 1: Use F() Macro for String Literals
Every string literal in your sketch consumes SRAM at runtime (copied from Flash). The F() macro keeps the string in Flash and reads it directly:
// BAD: consumes 13 bytes of SRAM
Serial.println("Hello World!");
// GOOD: stays in Flash, 0 bytes SRAM
Serial.println(F("Hello World!"));
// Also works with print:
Serial.print(F("Sensor reading: "));
Serial.println(analogRead(A0));
On a sketch with many Serial.print statements, the F() macro alone can free 200–500 bytes of SRAM.
Pattern 2: snprintf for Safe Formatting
char buf[64];
float temp = 25.6;
char tempStr[8];
dtostrf(temp, 5, 1, tempStr);
snprintf(buf, sizeof(buf), "Temp: %s C, Humidity: %d%%", tempStr, 65);
Serial.println(buf); // safe, no overflow possible
Pattern 3: Reuse Buffers
// Declare once globally or as static local:
static char msgBuf[128];
void sendStatus() {
snprintf(msgBuf, sizeof(msgBuf), "RPM:%d Temp:%d", rpm, temp);
Serial.println(msgBuf);
// msgBuf reused next call — no new allocation
}
Pattern 4: Check Free RAM
int freeRam() {
extern int __heap_start, *__brkval;
int v;
return (int)&v - (__brkval == 0 ? (int)&__heap_start : (int)__brkval);
}
void loop() {
Serial.print(F("Free RAM: "));
Serial.println(freeRam());
// If this keeps decreasing, you have a memory leak or fragmentation issue
}
If free RAM monotonically decreases over time, you have a memory leak. If it bounces erratically, fragmentation is occurring. Either way, switch to char arrays.
Pattern 5: Serial Input Parsing with char Arrays
char inputBuf[64];
int inputPos = 0;
void loop() {
while (Serial.available()) {
char c = Serial.read();
if (c == 'n' || inputPos >= sizeof(inputBuf) - 1) {
inputBuf[inputPos] = ''; // null terminate
processCommand(inputBuf);
inputPos = 0;
} else {
inputBuf[inputPos++] = c;
}
}
}
void processCommand(const char* cmd) {
if (strcmp(cmd, "ON") == 0) digitalWrite(LED_BUILTIN, HIGH);
else if (strcmp(cmd, "OFF") == 0) digitalWrite(LED_BUILTIN, LOW);
else Serial.println(F("Unknown command"));
}
Frequently Asked Questions
Is the String class always bad on Arduino?
Not always — it depends on board and usage. On an ESP32 with 520 KB SRAM, String concatenation in a loop rarely causes problems in practice. On a 2 KB Uno running indefinitely, it almost always causes eventual crashes. The rule is: on AVR Arduino boards with 2–8 KB SRAM, avoid String in repeating loops. On 32-bit boards with abundant RAM, String is generally fine.
How do I convert an integer to char array on Arduino without sprintf?
Use itoa() — a fast AVR integer-to-ASCII function: char buf[12]; itoa(myInt, buf, 10); The third argument is the base (10 for decimal, 16 for hex). It is faster and smaller than sprintf for simple integer formatting.
What is the maximum safe char array size on an Uno?
There is no single answer — it depends on your total sketch’s global variables and stack depth. As a rule, leave at least 200–300 bytes of SRAM free at peak stack depth. Use the freeRam() function to check. For most Uno sketches, individual char arrays up to 64–128 bytes are fine; avoid arrays larger than 512 bytes on the stack (use global or static instead).
Can I use std::string on Arduino?
Only on 32-bit Arduino cores (SAMD21, RP2040, ESP32, STM32) where the full C++ standard library is available. On AVR (Uno, Nano, Mega), std::string is not available — only Arduino’s String class and C-style char arrays. On supported boards, std::string is a valid alternative with standard semantics, but still uses dynamic allocation.
How do I compare two char arrays for equality?
Never use == — this compares pointer addresses, not contents, and always returns false. Use strcmp(str1, str2) == 0 for case-sensitive comparison, or strcasecmp(str1, str2) == 0 for case-insensitive comparison. For comparing a char array to a string literal: strcmp(buf, "ON") == 0.
The golden rule is simple: use char arrays for production Arduino code on AVR boards, reserve the String class for prototyping and 32-bit boards with plenty of RAM. Combined with the F() macro, snprintf(), and global buffer reuse, you can write Arduino sketches that run for months without memory issues.
Find the right Arduino board for your project at Zbotic. Browse our full Arduino microcontrollers collection — genuine boards shipped across India with GST invoices.
Add comment