База знаний · Язык скетчей
String и строки в Arduino: склеить, разобрать, сравнить и не съесть память
Что умеет String, как разобрать команду из монитора порта, когда String опасен на Uno и как обходиться массивом char, snprintf и F().
Текст в Arduino живёт в двух видах. String — класс с удобными командами: склеил плюсом, отрезал, превратил в число. И массив char — строка «по-старому», как в языке C: быстрее, экономнее и строже. Новички берут String, потом читают, что он «ест память», пугаются и переписывают всё на char. Правда посередине, и ниже она со скетчами и замерами.
String: что умеет
// Что умеет String: склеить, измерить, найти, вырезать, превратить в число.
void setup() {
Serial.begin(9600);
String name = "Trema";
String line = "Плата " + name + ", датчик " + String(7) + ", t=" + String(23.456, 1);
Serial.println(line); // Плата Trema, датчик 7, t=23.5
Serial.println(line.length()); // длина в байтах: русская буква — два байта
String cmd = " PWM 128 ";
cmd.trim(); // убрали пробелы по краям: "PWM 128"
cmd.toLowerCase(); // "pwm 128"
if (cmd.startsWith("pwm")) {
int value = cmd.substring(4).toInt(); // всё после "pwm " → 128
Serial.println(value * 2); // 256 — это уже число
}
int at = line.indexOf("датчик"); // позиция слова или -1, если его нет
Serial.println(at);
Serial.println(name.charAt(0)); // 'T' — первый символ; с кириллицей так нельзя
}
void loop() {
}
Главные команды: + склеивает, length() — длина, trim() срезает пробелы по краям, toLowerCase() и toUpperCase() меняют регистр, startsWith() и endsWith() проверяют начало и конец, indexOf() ищет и возвращает позицию или −1, substring(от, до) вырезает кусок, charAt(i) даёт один символ, toInt() и toFloat() делают число. Сравнивать можно просто ==: у String это сравнение текста, а не адресов. Число в строку — String(23.456, 1), вторая цифра — сколько знаков после запятой. Скетч на Uno занимает около 6 КБ: класс String тянет за собой заметный кусок кода.
Про русские буквы: монитор порта их покажет, но length() посчитает по два байта на букву, а charAt() вернёт половинку символа. Если текст надо резать по буквам — пишите его латиницей или работайте с готовыми строками целиком.
Скетч: команды из монитора порта
Самое частое применение String — разобрать, что прислал человек или телефон. readStringUntil('\n') читает до перевода строки, trim() убирает хвостовой \r, который монитор порта добавляет в режиме «NL и CR», и дальше — сравнения. На Uno — 4 728 байт.
// Команды из монитора порта: "led on", "led off", "pwm 0…255".
const byte LED = 9; // вывод с ШИМ
void setup() {
pinMode(LED, OUTPUT);
Serial.begin(9600);
Serial.setTimeout(50); // readStringUntil не будет ждать секунду
Serial.println(F("Команды: led on, led off, pwm 0-255"));
}
void loop() {
if (Serial.available() == 0) {
return;
}
String cmd = Serial.readStringUntil('\n'); // до перевода строки
cmd.trim(); // срезаем пробелы и \r
cmd.toLowerCase();
if (cmd == "led on") {
digitalWrite(LED, HIGH);
} else if (cmd == "led off") {
digitalWrite(LED, LOW);
} else if (cmd.startsWith("pwm ")) {
int value = cmd.substring(4).toInt(); // toInt даёт 0 для мусора
value = constrain(value, 0, 255);
analogWrite(LED, value);
Serial.print(F("ШИМ = "));
Serial.println(value);
} else if (cmd.length() > 0) {
Serial.print(F("не понял: "));
Serial.println(cmd);
}
}
Два места, где обычно спотыкаются. Без Serial.setTimeout(50) команда readStringUntil ждёт перевод строки целую секунду, и если в мониторе порта стоит «Нет конца строки», каждая команда доходит с опозданием. И toInt() не умеет жаловаться: для «abc» он вернёт 0, так что ноль на выходе — ещё не значит, что прислали ноль.
Почему String ругают: память
String хранит текст не в самой переменной, а в куче — общем запасе памяти, который берут по мере надобности. Каждый «плюс» в выражении создаёт новую, более длинную строку и отдаёт старую назад. На компьютере это никого не волнует, на Uno с 2 КБ ОЗУ — волнует: освободившиеся куски меньше следующих запросов и остаются дырами.
Чем это кончается — зависанием или перезагрузкой через несколько минут работы, без единой ошибки при сборке. Причём отчёт сборки здесь не помощник: строка «Глобальные переменные используют…» кучу не считает. Мы собрали один и тот же вывод показаний двумя способами: через String — 5 508 байт флеша и 214 байт ОЗУ по отчёту, через char[] с snprintf() — 5 228 и 206. Разница по отчёту смешная: snprintf() с dtostrf() сами весят около полутора килобайт, и там, где они не нужны, разрыв по флешу в пользу char[] именно такой — он измерен в статье про типы. Настоящая разница в другом: второй вариант не трогает кучу вовсе. Как устроены три памяти платы и как измерить свободную прямо во время работы — в статье про память Arduino.
Правила, по которым String на Uno живёт спокойно. Опаснее всего долгоживущие строки, которые понемногу растут: глобальный журнал, буфер «всего принятого». Временная строка, которая создаётся в loop() и тут же умирает — как cmd в скетче выше, — возвращает память целиком и дыр почти не оставляет. Не склеивайте по кусочку в цикле, перед длинной сборкой вызывайте reserve(64), чтобы место выделилось один раз, и не держите десяток строковых переменных одновременно. На ESP32 с сотнями килобайт и ESP8266 с полусотней всё это куда мягче: там String — нормальный рабочий инструмент.
char[]: строки по-старому
Массив char — это обычный массив с договорённостью: текст заканчивается нулевым байтом. char line[40] вмещает 39 знаков и ноль. Никакой кучи, размер известен заранее, память видна в отчёте сборки. Плата за это — функции с именами из прошлого века: strlen(), strcpy(), strcmp(), strcat(). И собирать строку из чисел удобнее всего через snprintf(): буфер, его размер, шаблон с %d для целых и %s для текста. Размер вторым аргументом — это и есть защита: длинный текст обрежется, а не вылезет за край.
// Та же работа без String: массив char, snprintf и dtostrf.
#include <string.h>
char line[40]; // буфер под строку: 39 знаков плюс завершающий ноль
char tempText[8]; // "-12.3" и запас
void setup() {
Serial.begin(9600);
}
void loop() {
int raw = analogRead(A0);
float temp = raw * 0.1;
dtostrf(temp, 1, 1, tempText); // %f на AVR не работает — число в текст так
snprintf(line, sizeof(line), "raw=%d t=%s C", raw, tempText);
Serial.println(line);
if (strcmp(tempText, "51.1") == 0) { // сравнивать char[] только через strcmp
Serial.println(F("ровно 51.1"));
}
delay(1000);
}
Засада номер один на AVR: %f в sprintf() не работает. Скетч собирается, а вместо числа печатается знак вопроса — поддержку дробных чисел из библиотеки выкинули ради размера. Поэтому dtostrf(число, ширина, знаков после запятой, буфер) сначала делает из float текст, а уже его подставляют через %s. На ESP32 %f работает.
Засада номер два: char[] нельзя сравнивать и присваивать знаками == и =. Сравнение — только strcmp(a, b) == 0, копирование — strcpy() или snprintf(). Что говорит компилятор, если забыть, — в таблице ниже.
F(): текст, который не занимает ОЗУ
Каждая строка в кавычках, которую вы печатаете, копируется из флеша в ОЗУ при старте — просто потому, что так устроен AVR. Два русских сообщения в setup() у нас съели 350 байт из 2 048; те же сообщения внутри F("…") — 188 байт, а прошивка выросла на 16 байт. F() оставляет текст во флеше и годится везде, где строка печатается или выводится на экран: Serial.print, lcd.print, display.print. Присвоить такой текст переменной char* нельзя — он не в той памяти. Для таблиц текстов и больших массивов во флеше есть PROGMEM — он разобран в статье про память.
Шпаргалка: из чего во что
| Задача | Как |
|---|---|
| Число → String | String(42), String(3.14159, 2) → «3.14» |
| String → число | s.toInt(), s.toFloat(); для мусора вернут 0 |
| String → char[] | s.toCharArray(buf, sizeof(buf)) или s.c_str() для чтения |
| char[] → String | String(buf) |
| Число → char[] | itoa(n, buf, 10), dtostrf(f, 5, 1, buf), snprintf(buf, sizeof(buf), "%d", n) |
| char[] → число | atoi(buf), atof(buf) |
| Один символ | 'a' в одинарных кавычках — это char; "a" в двойных — уже строка из двух байт |
Ошибки компилятора про строки
| Сообщение | Что случилось |
|---|---|
incompatible types in assignment of 'const char [3]' to 'char [10]' | Попытка написать buf = "on". Массив так не заполняют: strcpy(buf, "on"). |
comparison with string literal results in unspecified behavior | Предупреждение на if (cmd == "on") с char[]: сравниваются адреса, не текст. Нужен strcmp(). Для String оператор == работает. |
cannot convert 'String' to 'char*' in initialization | Функция ждёт char*, а дали String. Передайте s.c_str() или скопируйте через toCharArray(). |
invalid conversion from 'const char*' to 'char' | char c = "a" — двойные кавычки дают строку, одинарные — символ. Это только предупреждение: сборка пройдёт, а в c окажется мусор. |
invalid use of non-static member function | s.length без скобок. Команды объекта вызывают со скобками: s.length(). |
switch quantity not an integer | String в switch не кладут. Цепочка if … else if или сравнение первого символа через charAt(0) — тогда switch снова можно. |
Два из шести — предупреждения, которые Arduino IDE по умолчанию не показывает: сборка проходит, а программа делает не то. Включите в «Файл → Параметры...» уровень предупреждений «Все». Как устроены массивы и циклы, на которых стоит char[], — в статье про массивы и циклы; что у строки с типами и размерами — в статье про типы.
Частые вопросы
Как превратить String в число и обратно?
В число: s.toInt() для целых, s.toFloat() для дробных; для мусора обе вернут 0. В строку: String(42) или String(3.14159, 2) — вторая цифра задаёт знаки после запятой. Для char[] — atoi(), atof(), itoa() и dtostrf().
Как сравнить строки в Arduino?
Два объекта String сравнивают обычным ==: это сравнение текста. Массивы char так сравнивать нельзя — сравнятся адреса, и компилятор предупредит «comparison with string literal». Для них strcmp(a, b) == 0.
Почему говорят, что String ест память?
String хранит текст в куче, и каждая склейка плюсом создаёт новую строку, освобождая старую. На Uno с 2 КБ освободившиеся куски остаются дырами, куча растёт навстречу стеку, и через несколько минут плата перезагружается — без ошибок при сборке. Не создавайте String в loop() каждый проход, вызывайте reserve(), а в тесных местах берите char[]. На ESP32 с сотнями килобайт это почти не важно.
Почему sprintf с %f печатает знак вопроса?
На платах AVR (Uno, Nano, Mega) из библиотеки убрана поддержка дробных чисел в sprintf ради размера. Сначала превратите число в текст через dtostrf(число, ширина, знаков, буфер), потом подставьте его через %s. На ESP32 %f работает.
Что делает F() в Serial.print?
Оставляет строку во флеш-памяти, а не копирует в ОЗУ при старте. Два русских сообщения у нас заняли 350 байт ОЗУ без F() и 188 с ним — освободилось 162 байта, восемь процентов памяти Uno. Годится везде, где строка печатается: Serial, lcd, display. Присвоить такой текст переменной char* нельзя.
Как прочитать команду из монитора порта целиком?
Serial.readStringUntil('\n') читает до перевода строки; перед этим поставьте Serial.setTimeout(50), иначе каждая команда ждёт секунду. После чтения вызовите trim() — он срежет пробелы и символ \r, который добавляет Windows, — и сравнивайте через == или startsWith().
Читайте также
Обновлено: 22 октября 2026
