String-строки

String-строки


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

  • Статические строки – они же массивы символов char, являются стандартными для языка C/C++ и работают одинаково на любой платформе. О них поговорим в следующем уроке.
  • Динамические String-строки, в Arduino за них отвечает отдельная библиотека, которая входит в состав “ядра”. Эти строки просты и удобны в использовании, поэтому сначала разберём работу с ними.

Базовый синтаксис

Создание String


Строка создаётся как обычная переменная:

String s;

При создании также можно присвоить строке значение. String позволяет автоматически преобразовывать любой стандартный тип данных в строку:

String s1 = 12345;    // из целого числа
String s2 = 3.14;     // из float
String s3 = "text";   // из строковой константы
String s4 = F("text");// из строковой константы с F
String s5 = 's';      // из символа
int val = 1234;
String s6 = val;      // из переменной
String s7 = s6;       // из другой строки

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

String s1(1234);      // из целого числа, в десятичном виде
String s2(1234, BIN); // с указанием разрядности (BIN, DEC, HEX)
String s3(3,14, 1);   // из float, с указанием кол-ва знаков после запятой
String s4("text");    // из строковой константы
String s5(F("text")); // из строковой константы с F
String s6(s4);        // из другой строки

Более того, данный способ работает быстрее присваивания, так как выделяет память под строку один раз. В варианте с присваиванием сначала выделяется пустая строка, а затем увеличивается под новые данные.

Сложение String


К строке можно прибавить любой тип данных, так же как при создании, по одному действию в строке кода:

String s;
s += 12345;    // из целого числа
s += 3.14;     // из float
s += "text";   // из строковой константы
s += F("text");// из строковой константы с F
s += 's';      // из символа
int val = 1234;
s += val;      // из переменной
String s2(1234);
s += s2;       // из другой строки

Также String позволяет складывать строки между собой при помощи оператора +. В тексте ниже данные имеют любой тип, с которым String поддерживает сложение (см. выше):

  • Одним из слагаемых должна быть строка, как выше: стринг + данные или данные + стринг
  • Операция сложения возвращает строку обратно, что позволяет сделать “каскад” из таких сложений и собрать строку “одной строкой кода”, сборка происходит слева направо: стринг + данные1 + данные2 или данные1 + стринг + данные2 + данные3
  • Результат всей суммы можно:
    • Приравнять к String: стринг = стринг + данные1 + данные2
    • Отправить в функцию, которая принимает String: f(стринг + данные1 + данные2)
    • И так далее
String s;
s = String("Hello") + ',' + "World" + 1234 + 3.14;
Serial.println(s + "text" + 1234);

Для сборки строки данный вариант менее предпочтительный, чем предыдущий с +=. Ниже разберёмся, почему.

Доступ к символам


К строке можно обратиться как к массиву и прочитать или изменить символ по порядку:

String s("hello!");
s[0] = 'H';
// Здесь s == Hello!
char c = s[1];
// здесь c == e

Сравнение String


Стринги можно сравнивать между собой и с обычными строками (const char*):

String s1 = "text1";
String s2 = "text2";

if (s1 == s2);       // false
if (s1 == "text1");  // true
if (s2 != "text2");  // false

Остальные методы


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

Примечание: большинство функций будут некорректно работать со строками, содержащими символы не из таблицы ASCII (английские буквы + цифры + символы). То есть строки с кириллицей, иероглифами и прочим будут обрабатываться некорректно.
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() – переводит все символы в верхний регистр. Было ааааа – станет ААААА

Проблемы и оптимизация String


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

  • String – тяжёлый. Несомненно – использование String-строк сразу добавляет пару килобайт Flash памяти к весу программы, так как для работы с ними используется менеджер памяти (встроенная библиотека). В то же время, если в программе уже используется динамическое выделение памяти – добавление String будет заметно не так сильно. На этом данная проблема заканчивается, потому что если открыть реализацию библиотеки String, то можно увидеть, что все действия со строками выполняются при помощи стандартных строковых функций языка Си (подробнее – в следующем уроке).
  • String – медленный. Да, когда строка меняет свою длину – она начинает менять свой размер и даже место в оперативной памяти микроконтроллера. Переписывание и перераспределение памяти происходит отнюдь не мгновенно, поэтому операции со String выполняются относительно долго: сотни микросекунд. Если собирать строку посимвольно – каждая прибавка будет выполняться дольше, чем хотелось бы! Этого можно избежать, используя метод reserve(), который зарезервирует память, чтобы увеличение строки происходило без выделения памяти (подробнее об этом ниже). Если место под строку зарезервировано – операции со строкой будут выполняться с такой же скоростью, как и с обычными строками, потому что для них используются те же стандартные строковые функции.
  • String – опасный. Всё верно, неаккуратная работа со String может привести к сильной фрагментации памяти, неправильной работе программы и даже полному её зависанию. В то же время, если понимать как работают стринги и использовать эффективные и безопасные конструкции в работе с ними – можно избежать абсолютно всех проблем!

Использование памяти


Несмотря на то, что строка – это динамический инструмент, в реализации Arduino она может только увеличиваться. Это означает, что если у нас была длинная строка, а затем мы её обнулили – места в памяти она не стала занимать меньше! То есть

String s = "0123456789";
// занято 11 байт
s = "";
// всё ещё занято 11 байт

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

  • Если строка создана глобально – никак, в библиотеке не предусмотрено публичного инструмента для очистки строки.
  • Если строка создана локально – она автоматически выгрузится из памяти, когда код дойдёт до закрывающей фигурной скобки, за которой она уже не существует.
String s1 = "123";

void f() {
  {
    String s2 = "123";
    // здесь существует и s1, и s2
  }
  // здесь s2 уже удалена
}
Совет: избегайте глобально объявленных строк. Если без глобальной строки не обойтись – постарайтесь не позволять ей увеличиваться в процессе работы программы: скорее всего это приведёт к фрагментации. Если и без этого никак – нужно зарезервировать reserve() “максимальную” длину при запуске программы!

Сложение строк


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

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

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

Для сборки строки из нескольких частей рекомендуется использовать исключительно вариант с построчным прибавлением +=. Пусть он не такой визуально компактный, как сложение “в одной строке кода”, но он позволяет избежать создания лишних экземпляров String, лишних перераспределений памяти и самое главное – не приводит к её фрагментации и выполняется гораздо быстрее:

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

Фрагментация памяти


Тут есть ещё один важный момент: если в процессе такой сборки строки создать ещё одну строку – это приведёт к сильной фрагментации памяти! Например:

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

В конце выполнения этого кода собранная строка состоит условно из 25 символов и должна занимать в памяти 25 байт. Но с начала выполнения этого кода свободная память уменьшилась на 50+5 байт! Как и почему это произошло:

  • Перед созданием второй строки у нас уже есть строка, представим её как блок памяти [-----str-----]
  • Мы создаём ещё одну строку, она располагается в памяти сразу за предыдущей строкой (так работает менеджер памяти) [-----str-----][--str2--]
  • Теперь мы прибавляем к первой строке вторую: вторая строка остаётся в памяти, она никуда не пропадает, а первой нужно больше места. Поэтому менеджер памяти переносит первую строку на место сразу после второй и в памяти остаётся “дырка”! [       дырка        ][--str2--][-----str-str2-----]
  • В итоге “край” свободной памяти смещается на длину первой строки плюс длину второй строки. Беда!

С небольшими строками и кучей свободной памяти данная ситуация нам ничем не страшна, но если вы неправильно собираете например веб-страницу или другой ответ серверу – строка может начать занимать в несколько раз больше места, чем должна, и свободная оперативная память просто закончится!

Аналогичная ситуация произойдёт в том числе при вот такой записи, здесь тоже будет создана новая стринга в процессе сборки первой:

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

Что делать?

  • Избегать создания новых строк в процессе работы с уже имеющимися
  • Резервировать место под “прибавку”, прибавлять, а затем удалять вторую строку из памяти. Рассмотрим этот вариант

Сценарий первый, строка создаётся как переменная

String str;
str += "Hello";
str += value;
str += 1234;
str += ", ";
// "расширим" строку на +5 символов, навскидку
str.reserve(str.lenfth() + 5);
// новую строку создаём в новом блоке кода
{
  String str2(3.14);
  str += str2;  // прибавили на пустое место
}
// здесь str2 уже выгружена из памяти и не мешает

str += "World!"

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

Сценарий второй, преобразование

String str;
str += "Hello";
str += value;
str += 1234;
str += ", ";
// расширили место под строку
str.reserve(str.length() + 5);
// прибавили
str += String(3.14);
// тут "временная" строка уже удалилась из памяти
str += "World!"

Как это работает:

  • Была строка [-----str-----]
  • Мы её расширили [-----str-----      ]
  • Создали вторую строку [-----str-----      ][--str2--]
  • Переписали [-----str------str2--][--str2--]
  • Удалили временную строку [-----str------str2--]
  • Осталась сумма и никаких препятствий в памяти

Резервирование памяти


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

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

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

Оптимизация памяти


Забегая немного вперёд – текст в двойных кавычках хранится и в программной памяти программы (которой много), и в оперативной (которой мало). В уроке про PROGMEM мы рассмотрим несколько способов оптимизации памяти, но уже сейчас можно начать применять макрос F() – данный макрос позволяет хранить строку только в программной памяти и доставать её оттуда только для сложения со стрингой. Например после выполнения вот такого безобидного кода

String s = "Hello!";

Текст "Hello" окажется продублирован в памяти микроконтроллера целых 3 раза!

  • Текст всегда хранится в памяти программы
  • При запуске МК текст переписывается в оперативную память, чтобы можно было иметь к нему доступ. Находится там на всём протяжении работы программы
  • Мы создали стринг-строку, в которую скопировали этот текст. Копия будет находиться в памяти, пока строка не будет удалена из памяти

Если обернуть текст в макрос F() – он будет загружаться из программной памяти напрямую в строку, и удалится из неё вместе со строкой:

String s = F("Hello!");

Таким образом этот макрос крайне рекомендуется использовать для сборки строки с участием строковых констант. Вернёмся к нашему примеру:

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

Именно так и рекомендуется собирать строки.

Передача в функции


Функции очень многих библиотек для Arduino принимают String-строки. Это может быть вывод на дисплей, отправка в веб и многое другое. Если посмотреть реализацию этих функций – они принимают тип данных String& или const String&. Это – ссылка на строку, подробнее читайте в уроке про указатели и ссылки.

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

void prStr(String s) {
  Serial.println(s);
}

Проблема в том, что когда мы передадим в эту функцию строку

String s = "123";
prStr(s);

она будет продублирована в памяти, то есть внутри нашей функции будет копия переданной строки. Если строка большая, а свободной памяти мало – быть беде. Если передать строку по ссылке – внутри функции окажется именно наша строка, лишней копии не будет.

void prStr(String& s) {
  Serial.println(s);
}

Рекомендуется делать именно так.

Далее, если вы захотите помимо String-строк отправлять в свою функцию строковые константы (текст в кавычках) или текст внутри макроса F() для экономии памяти, то нужно будет добавить слово const:

void prStr(const String& s) {
  Serial.println(s);
}

Такая конструкция сможет эффективно принимать любые строковые данные для дальнейшей работы:

prStr("text");
prStr(F("flash text"));

String s(123);
prStr(s);

Сборка строки в отдельной функции


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

  // ...
  send(getString());
  // ...

String getString() {
  String s;
  s += 12345;
  return s;
}

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

  // ...
  String s;
  makeString(s);
  send(s);
  // ...

void makeString(String &s) {
  s += 12345;
}

Такой вариант работает чуточку быстрее (на пару десятков мкс на AVR) и использует чуть меньше оперативной памяти в рамках всей конструкции (около 10 байт на AVR по моим тестам).

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


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

Видео


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


5/5 - (10 голосов)
Подписаться
Уведомить о
guest

17 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии