База знаний · Язык скетчей

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 КБ ОЗУ — волнует: освободившиеся куски меньше следующих запросов и остаются дырами.

Одна строка «Temp=» + число + « raw, » + число — три переезда в куче«Temp=»дыра«Temp=512»дыра«Temp=512 raw, 51.2 C»куча растёт →← сюда спускается стекДыры меньше новой строки — в них ничего не поместится
Каждый «плюс» в выражении со String создаёт новую, более длинную строку и освобождает старую. Освободившийся кусок короче следующего запроса, поэтому остаётся дырой. На плате с 2 КБ памяти через несколько минут работы в loop() куча встречается со стеком, и плата перезагружается без единой ошибки при сборке.

Чем это кончается — зависанием или перезагрузкой через несколько минут работы, без единой ошибки при сборке. Причём отчёт сборки здесь не помощник: строка «Глобальные переменные используют…» кучу не считает. Мы собрали один и тот же вывод показаний двумя способами: через 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 — он разобран в статье про память.

Шпаргалка: из чего во что

ЗадачаКак
Число → StringString(42), String(3.14159, 2) → «3.14»
String → числоs.toInt(), s.toFloat(); для мусора вернут 0
String → char[]s.toCharArray(buf, sizeof(buf)) или s.c_str() для чтения
char[] → StringString(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 functions.length без скобок. Команды объекта вызывают со скобками: s.length().
switch quantity not an integerString в 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