String-строки

Мы с вами уже познакомились с символами в уроке про типы данных. Как в обычной жизни, одиночные символы соединяются в слова и строки – это текст, заключённый в двойные кавычки: "Hello, World!". У нас есть два набора инструментов по работе с ними: статические строки (массивы символов) и динамические String-строки. В этом уроке мы рассмотрим String-строки как более простые для понимания.

String-строки


Преимущество таких строк заключается в том, что их очень удобно собирать из других строк и даже переменных любых типов, складывать между собой, делить на подстроки и так далее. String является динамическим объектом и при изменении длины  строки начинает менять своё место в оперативной памяти микроконтроллера, что требует относительно большого времени выполнения и может приводить к фрагментации памяти:

  • Использование String сразу добавляет пару килобайт к весу программы, так как для её работы используется менеджер памяти.
  • Переписывание и перераспределение памяти происходит отнюдь не мгновенно, поэтому операции со String выполняются относительно долго (сотни микросекунд).
  • Неаккуратная работа со String может привести к сильной фрагментации памяти, неправильной работе программы и даже полному её зависанию.
  • Работа с такими строками обеспечивается встроенной библиотекой WString, и каждая строка представляет собой объект этой библиотеки.

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

  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

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

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


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

  • Работать со String-строкой как с массивом символов: обращаться по индексу для чтения и записи: myString[2] = 'a';
  • Сравнивать String-строки между собой: if (myString1 == myString2)
  • Сравнивать String-строки с массивами символов: if (myString1 == "kek")
  • Инициализировать String-строки любым числом, численным типом данных, символом, обычной строкой: String myString = 10.0;
  • Прибавлять к строке любой численный тип данных, символ или массив символов без преобразования к String: myString += 12345;
  • “Собирать” строки сложением из любых типов данных. Если первое (левое) слагаемое не является String – нужно преобразовать к (String). Остальные “подтянутся” сами: String str = (String)10 + " value" + var + ',' + 3.14;

Рассмотрим все методы для работы со строками, они применяются к строке через точку. В рассмотренных ниже примерах “тестовая” строка называется myString. Также оставлю некоторые комментарии по оптимизации.

charAt()
myString.charAt(index) – возвращает элемент строки myString под номером index. Аналог – myString[index];лучше использовать его вместо charAt()!
setCharAt()
myString.setCharAt(index, val) – записывает в строку myString символ val на позицию index. Аналог – myString[index] = val;лучше использовать его вместо setCharAt()!
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() – возвращает указатель char* на строку
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. В большинстве случаев лучше воспользоваться вариантом c_str()
getBytes()
myString.getBytes(buf, len) – копирует указанное количество символов len в буфер 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() – переводит все символы в верхний регистр. Было ааааа – станет ААААА

Оптимизация


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

Пример сложения строки


Самое большое преимущество String – удобная сборка строки из данных и переменных любого типа. Во многих примерах из Интернета можно увидеть следующую запись:

String str = String("Hello") + String(value) + String(1234) + String(", ") + String(3.14);
// результат: Hello 1234, 3.14

Так делать нельзя – каждый вызов String() создаёт новый экземпляр строки, что занимает кучу времени и приводит к дублированию и фрагментации памяти. В библиотеке String, которой мы пользуемся для работы с этими строками, предусмотрено более элегантное использование: достаточно, чтобы только первое слагаемое имело тип String, всё остальное библиотека преобразует сама. Перепишем:

String str = String("Hello") + value + 1234 + ", " + 3.14;

Запись стала короче и работает более оптимально.

Можно сделать ещё лучше – прибавлять к строке по одному “слагаемому” за раз. Запись станет длиннее, но выполняться будет гораздо быстрее. Используйте там, где время критично:

String str;
str += "Hello";
str += value;
str += 1234;
str += ", ";
str += 3.14;

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

String str;
str.reserve(30);
str += "Hello";
// .....

Осталась последняя проблема – текст в двойных кавычках. Такой текст глобально хранится в оперативной памяти, которой у нас не так уж и много. При прибавлении его к строке мы фактически дублируем его в той же оперативной памяти! Можно обернуть текст в макрос F(), который поместит строку в программную (Flash) память. Таким образом при сложении программа будет копировать текст из Flash памяти в оперативную и после работы функции память освободится. Супер!

String str;
str.reserve(30);
str += F("Hello");
str += value;
str += 1234;
str += F(", ");
str += 3.14;

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

void send(String s) {
  // какие-то действия со строкой
}

Применим её:

String str;
// ...
str += 3.14;
send(str);

Здесь происходит следующее: внутри функции send() мы получим новую строку, которая будет являться копией строки str и займёт столько же памяти. Этого можно избежать, передав строку по адресу: добавив символ &:

void send(String& s) {
  // какие-то действия со строкой 
}

Теперь внутри функции send() у нас будет та же самая строка str, просто под другим именем! Таким образом можно абсолютно безопасно передавать строку внутри программы, например для добавления к ней дополнительных данных и прочего.

Если какая-то функция (из какой-то библиотеки) принимает в качестве аргумента Си-строку char*, а нам ОЧЕНЬ хочется использовать String для создания строки, то можно очень просто передать в эту функцию нашу стрингу:

void send(char* s) {
  // какие-то действия со строкой
}

Передаём при помощи метода c_str() (см. документацию выше):

String str;
// ...
str += 3.14;
send(str.c_str());

Таким образом можно пользоваться преимуществами String-строк для сборки обычных си-строк, подробнее о них – в следующем уроке.

Другие библиотеки


Ради интереса я написал свою версию String, но без использования динамической памяти: максимальный размер строки задаётся при её создании, это позволяет сэкономить в сумме около 2 кБ Flash на одних и тех же операциях со строкой. Библиотека имеет такой же набор методов и возможностей, как у String, что позволяет легко заменить стандартные стринги на мои, а также там есть несколько дополнительных фишек. Библиотека называется mString, документацию и примеры смотрите на GitHub.

Видео


Полезные страницы


  • Набор GyverKIT – большой стартовый набор Arduino моей разработки, продаётся в России
  • Каталог ссылок на дешёвые Ардуины, датчики, модули и прочие железки с AliExpress у проверенных продавцов
  • Подборка библиотек для Arduino, самых интересных и полезных, официальных и не очень
  • Полная документация по языку Ардуино, все встроенные функции и макросы, все доступные типы данных
  • Сборник полезных алгоритмов для написания скетчей: структура кода, таймеры, фильтры, парсинг данных
  • Видео уроки по программированию Arduino с канала “Заметки Ардуинщика” – одни из самых подробных в рунете
  • Поддержать автора за работу над уроками
  • Обратная связь – сообщить об ошибке в уроке или предложить дополнение по тексту (alex@alexgyver.ru)
5/5 - (9 голосов)
Назад Циклы
Вперёд Си-строки (массивы символов)
Подписаться
Уведомить о
guest
5 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии