Текстовые данные в Arduino


Мы с вами уже познакомились с символами в уроке про типы данных. Напомню: символ является переменной (или константой) типа char и хранит в себе код буквы в таблице символов. Создан для удобства программиста, чтобы он мог работать не кодами, а с читаемыми символами. Как и в жизни, символы соединяются в слова, здесь они называются строки. У нас есть два набора инструментов по работе с ними: обычные строки (массивы символов) и String-строки.

  • Массив символов (с англ. char array) – это просто массив данных типа char, про массивы мы уже недавно говорили и вы должны понять, о чём идёт речь. Основные особенности: максимальный размер массива строки должен быть известен заранее, и к каждому элементу такой строки можно обратиться при помощи квадратных скобок. Любой текст, явно заключённый в "двойные кавычки", воспринимается программой как массив символов.
    • Информация для тех, кто читал урок про указатели: будучи обычным массивом, строка является указателем на свой первый элемент (то есть программа конкретно знает только о том, где строка начинается). Встроенные функции по работе со строками ориентируются на нулевой символ, который обязательно находится в конце строки. Таким образом и определяется длина строки: от начала и до нулевого символа.
  • Основным отличием String-строки от массива символов является то, что строка – динамический массив, у которого не нужно указывать размер, он может меняться в процессе работы программы. Также строка является не просто типом данных, а объектом очень мощного класса одноимённой библиотеки String, которая автоматически подключается в код и добавляет огромную кучу удобных инструментов для работы с текстом: разделением, обрезкой, поиском и заменой и т.д. Строка может быть создана из любого типа данных и преобразована обратно почти во все из них.

String-строки


Давайте рассмотрим большой пример, из которого будет понятно, как объявить строку и как с ней работать, а также учтены некоторые тонкости:

  String string0 = "Hello String";              // заполняем словами в кавычках
  String string1 = String("lol ") + String("kek");  // сумма двух строк
  String string2 = String('a');                 // строка из символа в одинарных кавычках
  String string3 = String("This is string");    // конвертируем строку в String
  String string4 = String(string3 + " more");   // складываем строку string3 с текстом в кавычках
  String string5 = String(13);                  // конвертируем из числа в String
  String string6 = String(20, DEC);             // конвертируем из числа с указанием базиса (десятичный)
  String string7 = String(45, HEX);             // конвертируем из числа с указанием базиса (16-ричный)
  String string8 = String(255, BIN);            // конвертируем из числа с указанием базиса (двоичный)
  String string9 = String(5.698, 3);            // из float с указанием количества знаков после запятой (тут 3)

  // строки можно складывать друг с другом
  String string10 = string0 + string1;          // string10 равна Hello Stringlol kek

  // можно формировать название из кусочков, например для работы с файлами. Даже из дефайнов
#define NAME "speed"
#define TYPE "-log"
#define EXT ".txt"

  // при сложении достаточно указать String 1 раз для первой строки
  String filename = String(NAME) + TYPE + EXT;  // filename будет равна speed-log.txt

  // доступ к элементу строки работает по такому же механизму, как массив
  string0[0] = 'a';  // одинарные кавычки, т.к. присваиваем ОДИНОЧНЫЙ СИМВОЛ!
  // теперь вместо Hello String у нас aello String

Как вы могли заметить, строки можно объявлять большим количеством способов, а также буквально складывать строки, как числа, оператором +. Я уже говорил, что строки являются объектами класса String, и у этого класса есть огромное количество удобных методов по работе со строками, далее мы их все рассмотрим с некоторыми примерами. Но для начала запомните вот что: строки – очень тяжёлый инструмент, очень медленный и занимающий кучу памяти: уже просто само наличие строк (от одной и более) в прошивке занимает +5% Flash памяти, т.к. подключается сам “инструмент” – класс String. Для небольших проектов это не страшно, памяти всегда будет навалом. Также неаккуратное использование строк может приводить к фрагментации оперативной памяти и зависанию программы, подробнее читайте ниже.

Инструменты для String


Помимо набора методов, библиотека String имеет несколько перегруженных операторов, благодаря которым мы можем:

  • Работать с элементами String-строки как с массивами: myString[2] = 'a';
  • Сравнивать String-строки между собой: if (myString1 == myString2)
  • Сравнивать String-строки с массивами символов: if (myString1 == "kek")
  • Инициализировать String-строки любым численным типом данных, символом, массивом символов и массивом символов внутри макроса F(): String myString = 10.0;
  • Прибавлять к строке любой численный тип данных, символ или массив символов: myString += 12345;

Во всех перечисленных случаях можно считать, что данные “сами превратятся в String” и будут взаимодействовать со String-строкой, стоящей слева от оператора. Это не совсем так, но для понимания достаточно.

Итак, методы для работы со строками. Как и все методы, они применяются к своим объектам (к строкам) через точку. В рассмотренных ниже примерах строка называется myString.

charAt()

myString.charAt(index);

Возвращает элемент строки myString под номером index. Аналог – myString[index];

setCharAt()

myString.setCharAt(index, val);

Записывает в строку myString символ val на позицию index. Аналог – myString[index] = val;

compareTo()
myString.compareTo(myString2);
  • Возвращает отрицательное число, если myString идёт до myString2
  • Возвращает положительное число, если myString идёт после myString2
  • Возвращает 0, если строки одинаковы
concat()

myString.concat(value);

Присоединяет value к строке (value может иметь любой численный тип данных). Возвращает true при успешном выполнении, false при ошибке. Аналог – сложение, myString + value;

endsWith()

myString.endsWith(myString2);

Проверяет, заканчивается ли myString символами из myString2. В случае совпадения возвращает true

startsWith()

myString.startsWith(myString2);

Проверяет, начинается ли myString символами из myString2. В случае совпадения возвращает true

equals()

myString.equals(myString2);

Возвращает true, если myString совпадает с myString2. Регистр букв важен

equalsIgnoreCase()

myString.equalsIgnoreCase(myString2);

Возвращает true, если myString совпадает с myString2. Регистр букв неважен

indexOf()

myString.indexOf(val);
myString.indexOf(val, from);

Ищет и возвращает номер (позицию) значения val в строке, ищет слева направо, возвращает номер первого символа в совпадении. val может быть char или String, то есть ищем в строке другую строку или символ. Можно искать, начиная с позиции from. В случае, когда не может найти val в строке, возвращает -1.

lastIndexOf()

myString.lastIndexOf(val);
myString.lastIndexOf(val, from);

Ищет и возвращает номер (позицию) значения val в строке, ищет справа налево, возвращает номер последнего символа в совпадении. val может быть char или String, то есть ищем в строке другую строку или символ. Можно искать, начиная с позиции from. В случае, когда не может найти val в строке, возвращает -1.

length()

myString.length();

Возвращает длину строки в количестве символов

remove()

myString.remove(index);
myString.remove(index, count);

Удаляет из строки символы, начиная с index и до конца, либо до указанного count

replace()

myString.replace(substring1, substring2);

В строке myString заменяет последовательность символов substring1 на substring2.

String myString = "lol kek 4eburek";

// заменить чебурек на пельмень
myString.replace("4eburek", "pelmen");
reserve()

myString.reserve(size);

Зарезервировать в памяти количество байт size для работы со строкой

c_str()

myString.c_str();

Преобразовывает строку в “СИ” формат (null-terminated string) и возвращает указатель на полученную строку

trim()

myString.trim();

Удаляет пробелы из начала и конца строки. Действует со строкой, к которой применяется

substring()

myString.substring(from);
myString.substring(from, to);

Возвращает кусок строки, содержащейся в myString начиная с позиции from и до конца, либо до позиции to

String myString = "lol kek 4eburek";
String chebur = myString.substring(8);
// строка chebur содержит в себе "4eburek"
toCharArray()

myString.toCharArray(buf, len);

Раскидывает строку в массив – буфер buf (типа char []) с начала и до длины len

getBytes()

myString.getBytes(buf, len);

Копирует указанное количество символов len (вплоть до unsigned int) в буфер buf (byte [])

toFloat()

myString.toFloat();

Возвращает содержимое строки в тип данных float

toDouble()

myString.toDouble();

Возвращает содержимое строки в тип данных double

toInt()

myString.toInt();

Возвращает содержимое строки в тип данных int

String myString = "10500";
int val = myString.toInt();
// val теперь 10500
toLowerCase()

myString.toLowerCase();

Переводит все символы в нижний регистр. Было ААААА – станет ааааа

toUpperCase()

myString.toUpperCase();

Переводит все символы в верхний регистр. Было ааааа – станет ААААА

Длина строки


Небольшой комментарий по поводу длины строки: в отличие от char array, узнать длину String строки можно только при помощи метода length() (потому что String-строка является динамическим массивом, а sizeof() выполняется на этапе компиляции):

String textString = "Hello";
sizeof(textString);   // вернёт 6 ПРИ ЛЮБОЙ ДЛИНЕ СТРОКИ
textString.length();  // вернёт 5

Меры предосторожности


Строка по сути является динамическим массивом, её размер может меняться по ходу программы. Это очень удобно, но и очень опасно, потому что память может фрагментироваться, закончиться и т.д. Рассмотрим несколько базовых принципов безопасной работы со строками:

  • Если вам нужно передать String-строку в функцию – делайте это по ссылке (подробнее в этом уроке). Это избавит программу от дублирования куска данных, ведь из-за достаточно большой строки оперативная память может закончиться и программа зависнет! Пример: void someFunc(String &str); – функция принимает ссылку на строку. На использовании функции это никак не скажется, но при её вызове не будет создаваться копия строки!
  • Не вызывайте лишний раз преобразование в String, библиотека сделает это за вас! Например не нужно писать myString += String(value);, достаточно просто myString += value;. При создании длинной строки путём прибавления новых данных “по кусочку” это спасёт от фрагментации памяти.
  • Оборачивайте участки кода с объёмной работой со строками в {фигурные скобки}: локально созданные String-строки будут удалены из памяти сразу после закрывающей }, что может предотвратить фрагментацию и переполнение памяти.

Массивы символов


Массивы символов, они же “char array”, являются ещё одним способом работы с текстовыми данными. Этот вариант имеет гораздо меньше возможностей по работе с текстом, но зато занимает меньше места в памяти (не используется библиотека String) и работает значительно быстрее. К массиву символов применяются те же правила, какие работают для обычных массивов. Рассмотрим пример, в котором объявим массивы символов разными способами:

// объявить массив и задать текст символами
// размер будет посчитан компилятором
char helloArray[] = {'H', 'e', 'l', 'l', 'o'};

// но строковые массивы можно объявлять и вот так:
char helloArray2[] = "Hello!";

// можно объявить массив большего размера, чем начальный текст.
// Будет свободное место под другой текст в будущем
char helloArray3[100] = "Hello!";

// вот так можно объявить длинную строку
char longArray[] = "The quick brown fox "
                   "jumps over the lazy dog";

Можно работать с элементами строк как с массивами:

helloArray2[0] = 'L';         // заменим элемент
// теперь helloArray2 == "Lello!"

В отличие от строк, массивы символов нельзя:

helloArray3 += textArray;  // складывать
textArray = "new text";    // присваивать СТРОКУ после инициализации
if (helloArray == helloArray2);  // сравнивать

Для этого существуют специальные функции, о которых мы поговорим ниже.

Важный момент: любой “текст в кавычках” в коде программы является массивом символов, а точнее const char* : указатель, так как это массив, и константа – потому что текст введён до компиляции и во время работы программы измениться не может. Массив символов может быть преобразован в String-строку (как в начале урока), но сам по себе отношения к String не имеет!

При инициализации массива символов “текстом в кавычках” создаётся массив с размером на 1 больше, чем количество символов в тексте: компилятор дописывает в конец строки нулевой символ NULL, благодаря которому различные инструменты по работе со строками будут видеть длину строки: от первого символа и до NULL.

Длина строки char array


Для определения длины текста можно использовать оператор strlen(), который возвращает количество символов в массиве. Сравним его работу с оператором sizeof():

char textArray[100] = "World";
sizeof(textArray);  // вернёт 100
strlen(textArray);  // вернёт 5

Здесь оператор sizeof() вернул количество байт, занимаемое массивом. Массив я специально объявил с размером бОльшим, чем содержащийся в нём текст. А вот оператор strlen() посчитал и вернул количество символов, которые идут с начала массива и до нулевого символа в конце текста (без его учёта). А вот такой будет результат при инициализации без указания размера массива:

char text[] = "Hello";
strlen(text);   // вернёт 5 ("читаемых" символов)
sizeof(text);   // вернёт 6 (байт)

Массив строк


Очень мощной фишкой массивов символов является возможность создать один массив с несколькими строками, и обращаться к ним по номеру. Выглядит это следующим образом:

// объявляем массив строк
const char *names[]  = {
  "Period",   // 0
  "Work",     // 1
  "Stop",     // 2
};

// выводим третий элемент
Serial.println(names[2]); // выведет Stop

Данный способ работы со строками хорош тем, что строки хранятся под номерами, и это крайне удобно при работе с дисплеями и в частности создании текстового меню: практически все библиотеки дисплеев умеют выводить массив символов одной командой.

F() macro


Строки (массивы символов) являются очень тяжёлым грузом, ведь текст в строке хранится в оперативной памяти микроконтроллера, а её не так уж и много. Есть готовый инструмент, позволяющий удобно хранить текстовые данные во Flash памяти микроконтроллера. Этот способ хорош для вывода фиксированных текстовых данных, например, в монитор порта или на дисплей:

Serial.println(F("Hello, World!"));

Строка “Hello, World!” будет записана во Flash память и не займёт 14 байт (13 + нулевой) в оперативной.

Экономия памяти


“Строки” в массиве строк тоже хранятся в оперативной памяти, а рассмотренный выше F() macro к ним нельзя. То есть вот такой код приведёт к ошибке:

const char *names[]  = {
  F("Period"),   // 0
  F("Work"),     // 1
  F("Stop"),     // 2
};

Как же быть? Массив строк можно сохранить в PROGMEM, программной памяти микроконтроллера, Flash. Вот такую конструкцию можно использовать как шаблон:

// объявляем наши "строки"
const char array_1[] PROGMEM = "Period";
const char array_2[] PROGMEM = "Work";
const char array_3[] PROGMEM = "Stop";

// объявляем таблицу ссылок
const char* const names[] PROGMEM = {
  array_1, array_2, array_3,
};

void setup() {
  Serial.begin(9600);

  char arrayBuf[10];  // создаём буфер

  // копируем в arrayBuf при помощи встроенного strcpy_P
  strcpy_P(arrayBuf, (char*)pgm_read_word(&(names[1])));

  Serial.println(arrayBuf); // выведет Work
}

Да, сложно и громоздко, но при большом объёме текстовых данных это может спасти проект! Например при создании устройства с текстовым меню на дисплее. Более подробно о хранении строк в PROGMEM читайте в уроке про PROGMEM.

Инструменты для char array


Есть готовые функции, позволяющие конвертировать различные типы данных в строки:

  • itoa(int_data, str, base) – записывает переменную типа int int_data в строку str с базисом* base.
  • ltoa (long_data, str, base) – записывает переменную типа long long_data в строку str с базисом* base.
  • ultoa (unsigned_long_data, str, base) – записывает переменную типа unsigned long unsigned_long_data в строку str с базисом* base.
  • dtostrf(float_data, width, dec, str) – записывает переменную типа float float_data в строку str с количеством символов width и знаков после запятой dec.

* Примечание: base – основание системы исчисления, тут всё как при выводе в Serial:

  • DEC – десятичная
  • BIN – двоичная
  • OCT – восьмеричная
  • HEX – шестнадцатеричная
float x = 12.123;
char str[10] = "";
dtostrf(x, 4, 2, str);
// тут str == "12.12"

int y = 123;
itoa(y, str, DEC);
// тут str == "123"

И наоборот, можно преобразовывать строки в численные данные (функция вернёт результат):

  • atoi(str) – преобразование str в int
  • atol(str) – преобразование str в long
  • atof(str) – преобразование str в float
float x;
char str[10] = "12.345";
x = atof(str);
// тут x == "12.345"
Внимание! Функции конвертации, работающие с типом float, являются очень тяжёлыми: их “подключение” занимает ~2 кБ Flash памяти!! Максимально избегайте их применения в крупном проекте. Для преобразования можно сделать свою функцию, практически готовые варианты для всех типов данных можно найти в стандартной ардуиновской Print.cpp (ссылка на файл на гитхабе Arduino).

Массивы символов не так просты, как кажутся: их возможности сильно расширяет стандартная библиотека cstring. Использование всех доступных фишек по работе с массивами символов позволяет полностью избавить свой код от тяжёлых String-строк и сделать его легче, быстрее и оптимальнее. Подробно обо всех инструментах можно почитать в официальной документации. Очень интересный пример с манипуляцией этими инструментами можно посмотреть здесь. А мы вкратце рассмотрим самые важные.

Важный момент: библиотека работает со строками как с указателями, и многие функции возвращают как результат именно указатель. Как это понимать, если вы не читали урок про указатели и/или тема слишком сложная? Указатель – первый символ в строке, работа со строкой начнётся с него. Последним символом является нулевой NULL символ, и для программы строка существует именно в этом диапазоне. Если какая-то функция возвращает указатель на конкретный символ в строке – по сути она возвращает кусок строки начиная с этого символа и до конца строки. Например, мы искали символ , в строке "Hello, world!". Программа вернёт нам указатель на эту запятую, по сути это будет кусочек той же самой строки, содержащий ", world!". Просто “начало” строки сместится.

strcpy(str1, str2)
Копирует str2 в str1, включая NULL. Так как мы передаём указатель, цель и место назначения можно “подвинуть”:
char str1[] = "hello world";
char str2[] = "goodbye";
// вставим bye после hello
strcpy(str1 + 6, str2 + 4); 
// тут str1 == hello bye
strncpy(str1, str2, num)
Копирует num символов из начала str2 в начало str1
char str1[] = "hello world";
char str2[] = "goodbye";
// вставим good после hello
strncpy(str1 + 6, str2, 4); 
// тут str1 == hello goodd
// вторая d осталась после "world"
strcat(str1, str2)
Прибавляет str2 к str1, при этом str1 должна иметь достаточный для этого размер. NULL первой строки заменяется на первый символ из str2
strncat(str1, str2, num)
Добавляет num символов из начала str2 к концу str1
strcmp(str1, str2)
Сравнивает str1 и str2. Возвращает 0, если строки одинаковы. Больше нуля, если str1 > str2. Меньше нуля, если str1 < str2.
strncmp(str1, str2, num)
Сравнивает первые num символов из строк str1 и str2. Возвращает 0, если эти участки одинаковы.
strchr(str, symb)
Ищет символ symb в строке str и возвращает указатель на первое совпадение.
strrchr(str, symb)
Ищет символ symb в строке str и возвращает указатель на последнее совпадение.
strcspn(str1, str2)
Выполняет поиск первого вхождения в строку str1 любого из символов строки str2 и возвращает количество символов до найденного первого вхождения.
strpbrk(str1, str2)
Выполняет поиск первого вхождения в строку str1 любого из символов строки str2 и возвращает указатель на найденный символ.
strspn(str1, str2)
Поиск символов строки str2 в строке str1. Возвращает длину начального участка строки str1, который состоит только из символов строки str2.
strstr(str1, str2)
Функция ищет первое вхождение подстроки str2 в строке str1.
strtok(str, delim)
Ищет символы-разделители delim в строке str, возвращает указатель на последний найденный. Как использовать – смотри тут.
strlen(str)
Возвращает длину строки str без учёта нулевого символа.

Важные страницы