String Object Constructors
The String object allows you to manipulate strings of text in a variety of useful ways. You can append characters to Strings, combine Strings through concatenation, get the length of a String, search and replace substrings, and more. This tutorial shows you how to initialize String objects.
All of these methods are valid ways to declare a String object. They all result in an object containing a string of characters that can be manipulated using any of the String methods. To see them in action, upload the code below onto an Arduino board and open the Arduino IDE serial monitor. You’ll see the results of each declaration. Compare what’s printed by each println() to the declaration above it.
Hardware Required
Circuit
There is no circuit for this example, though your board must be connected to your computer via USB and the serial monitor window of the Arduino Software (IDE) should be open.
See Also
String object — Your Reference for String objects
CharacterAnalysis — We use the operators that allow us to recognise the type of character we are dealing with.
StringAdditionOperator — Add strings together in a variety of ways.
StringAppendOperator — Use the += operator and the concat() method to append things to Strings
StringCaseChanges — Change the case of a string.
StringCharacters — Get/set the value of a specific character in a string.
StringComparisonOperators — Get/set the value of a specific character in a string.
StringIndexOf — Look for the first/last instance of a character in a string.
StringLength — Get the length of a string.
StringLengthTrim — Get and trim the length of a string.
StringReplace — Replace individual characters in a string.
StringStartsWithEndsWith — Check which characters/substrings a given string starts or ends with.
StringSubstring — Look for «phrases» within a given string.
StringToInt — Allows you to convert a String to an integer number.
String Appending Operators
Just as you can concatenate Strings with other data objects using the StringAdditionOperator, you can also use the += operator and the concat() method to append things to Strings. The += operator and the concat() method work the same way, it’s just a matter of which style you prefer. The two examples below illustrate both, and result in the same String:
In both cases, stringOne equals «A long integer: 123456789». Like the + operator, these operators are handy for assembling longer strings from a combination of data objects.
Hardware Required
Circuit
There is no circuit for this example, though your board must be connected to your computer via USB and the serial monitor window of the Arduino Software (IDE) should be open.
See Also
String object — Your Reference for String objects
CharacterAnalysis — We use the operators that allow us to recognise the type of character we are dealing with.
StringAdditionOperator — Add strings together in a variety of ways.
StringCaseChanges — Change the case of a string.
StringCharacters — Get/set the value of a specific character in a string.
StringComparisonOperators — Get/set the value of a specific character in a string.
StringConstructors — Initialize string objects.
StringIndexOf — Look for the first/last instance of a character in a string.
StringLength — Get the length of a string.
StringLengthTrim — Get and trim the length of a string.
StringReplace — Replace individual characters in a string.
StringStartsWithEndsWith — Check which characters/substrings a given string starts or ends with.
StringSubstring — Look for «phrases» within a given string.
StringToInt — Allows you to convert a String to an integer number.
Си-строки (массивы символов)
Си-строки
В прошлом уроке мы разобрали динамические String-строки в реализации Arduino, а сейчас настало время стандартных статических строк языка C/C++. Такая строка представляет собой массив символов типа char (char array) и для неё работает такой же синтаксис, как и для остальных массивов (урок про массивы). Конец строки определяется нулевым символом \0 (или целым число 0 ), за это такой тип строк называют null-terminated string: ноль на конце позволяет программе определять конец строки и её длину. Также это стандартные строки языка Си и поэтому называются cstring.
Текст в кавычках
Любой написанный в двойных кавычках текст «some text» :
- Является строковой константой – string constant
- Имеет тип данных const char* – то есть указывает на свой первый символ в памяти
- Хранится и в программной, и в оперативной памяти микроконтроллера
- Компилятор автоматически добавляет нулевой символ в конец строки ‘\0’ – то есть реальный размер строки всегда на 1 символ больше
- Оптимизируется компилятором – об этом ниже
Объявление как массив
Основное отличие таких строк от String -строк: это обычный массив, размер которого известен заранее и не меняется в процессе работы. Можно объявить строку как массив и посимвольно задать текст:
Такой вариант записи не очень удобный, поэтому строки в C/C++ можно задавать просто текстом в двойных кавычках – компилятор сам посчитает размер массива:
Полученный выше массив содержит 6 символов: 5 на слово hello и 1 на завершающий символ. Текст в данном массиве можно изменять в процессе работы программы, потому что с точки зрения программы мы создали обычный массив и заполнили его буквами. Изменим первую букву на прописную: str[0] = ‘H’; . Выведем в монитор порта:
Serial умеет работать с такими данными и с радостью их выведет.
Объявление как указатель
Также строку можно объявить как указатель на const char* – то есть сам текст в кавычках хранится где то в программе, а мы получаем на него “ссылку”:
Текст в такой строке менять уже нельзя, но можно использовать дальше в программе для сложения или вывода:
Оптимизация
Компилятор оптимизирует строковые константы, но не во всех случаях.
Если создать несколько строк как массивы (которые можно изменять) и присвоить им одинаковые строки, то они займут место в памяти как разные строки, т.е. столько, сколько в них суммарно символов:
Если создать несколько одинаковых строк как указатели – то компилятор их оптимизирует и они займут место в памяти как одна строка!
Если при выводе в Serial или передаче в другие функции мы используем одинаковые строки, то они также будут оптимизироваться и занимать место как одна строка:
В то же время F() – строки (подробнее в уроке про PROGMEM) не оптимизируются компилятором и занимают в программной памяти каждая своё место:
Сложение компилятором
Си-строки можно складывать из нескольких строк через пробелы:
И переносить сложение на новую строку:
Сложение происходит на этапе компиляции, то есть скомпилированной программе это будет одна большая строка.
Отличия от String
В отличие от String-строк, Си-строки:
Для этого существуют специальные функции, о которых мы поговорим ниже.
Длина строки
Для определения длины текста можно использовать оператор strlen() , который возвращает количество символов в строке. Сравним его работу с оператором sizeof() :
Здесь оператор sizeof() вернул количество байт, занимаемое массивом. Массив я специально объявил с размером бОльшим, чем содержащийся в нём текст. А вот оператор strlen() посчитал и вернул количество символов, которые идут с начала массива и до нулевого символа в конце текста без его учёта. А вот такой будет результат при инициализации без указания размера массива:
Массив строк
Можно создать один массив с несколькими строками и обращаться к ним по индексу, фактически это будет двухмерный массив. Выглядит следующим образом:
Таким образом удобно паковать строки для создания текстовых меню и прочего. Единственный большой минус – весь этот текст висит в оперативной памяти мёртвым грузом. Можно сохранить его во Flash – программной памяти (PROGMEM), об этом читайте в отдельном уроке.
Оптимизация памяти
Как я писал выше – “текст в кавычках” хранится и в памяти программы, и в оперативной памяти, то есть после запуска микроконтроллера строка загружается в оперативную память, и уже там мы имеем к ней доступ. Как правило, объём программной памяти микроконтроллера в несколько раз больше, чем оперативной. Есть несколько возможностей хранения строк только в программной памяти, об этом очень подробно поговорим в уроке про PROGMEM.
Инструменты для Си-строк
Есть готовые функции, позволяющие конвертировать различные типы данных в строки:
- 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 – шестнадцатеричная
И наоборот, можно преобразовывать строки в численные данные, функция вернёт результат:
- atoi(str) – преобразование str в int
- atol(str) – преобразование str в long
- atof(str) – преобразование str в float
2 кБ Flash памяти!! Максимально избегайте их применения в крупном проекте. Для преобразования можно сделать свою функцию, практически готовые варианты для всех типов данных можно найти в стандартной ардуиновской Print.cpp (ссылка на файл на гитхабе Arduino).
Массивы символов не так просты, как кажутся: их возможности сильно расширяет стандартная библиотека cstring. Использование всех доступных фишек по работе с массивами символов позволяет полностью избавить свой код от тяжёлых String-строк и сделать его легче, быстрее и оптимальнее. Подробно обо всех инструментах можно почитать в официальной документации. Очень интересный пример с манипуляцией этими инструментами можно посмотреть здесь. А мы вкратце рассмотрим самые полезные.
Важный момент: библиотека работает со строками как с указателями, и многие функции возвращают как результат именно указатель. Как это понимать, если вы не читали урок про указатели и/или тема слишком сложная? Указатель – первый символ в строке, работа со строкой начнётся с него. Последним символом является нулевой символ, и для программы строка существует именно в этом диапазоне. Если какая-то функция возвращает указатель на конкретный символ в строке – по сути она возвращает часть строки, начиная с этого символа и до конца строки. Например, мы искали символ , в строке «Hello, world!» . Программа вернёт нам указатель на эту запятую, по сути это будет кусочек той же самой строки, содержащий «, world!» . Просто “начало” строки сместится.
Копирует str2 в str1, включая NULL . Так как мы передаём указатель, цель и место назначения можно “подвинуть”:
Копирует num символов из начала str2 в начало str1
Библиотека
У меня есть библиотека для работы с Си-строками: их преобразования и парсинга на блоки данных. Некоторые инструменты реализованы гораздо эффективнее, чем в стандартной строковой библиотеке. Библиотека называется GParser, документацию и примеры смотрите на GitHub.
Видео
String-строки
String-строки
Мы с вами уже познакомились с символами в уроке про типы данных. Как в обычной жизни, одиночные символы соединяются в слова и строки – это текст, заключённый в двойные кавычки: «Hello, World!» . У нас есть два набора инструментов для работы с ними:
- Статические строки – они же массивы символов char , являются стандартными для языка C/C++ и работают одинаково на любой платформе. О них поговорим в следующем уроке.
- Динамические String -строки, в Arduino за них отвечает отдельная библиотека, которая входит в состав “ядра”. Эти строки просты и удобны в использовании, поэтому сначала разберём работу с ними.
Базовый синтаксис
Создание String
Строка создаётся как обычная переменная:
При создании также можно присвоить строке значение. String позволяет автоматически преобразовывать любой стандартный тип данных в строку:
Помимо такой инициализации, есть ещё вариант с конструктором, он более гибкий: в скобках можно указать значение, а для некоторых типов данных доступны дополнительные настройки:
Более того, данный способ работает быстрее присваивания, так как выделяет память под строку один раз. В варианте с присваиванием сначала выделяется пустая строка, а затем увеличивается под новые данные.
Сложение String
К строке можно прибавить любой тип данных, так же как при создании, по одному действию в строке кода:
Также String позволяет складывать строки между собой при помощи оператора + . В тексте ниже данные имеют любой тип, с которым String поддерживает сложение (см. выше):
- Одним из слагаемых должна быть строка, как выше: стринг + данные или данные + стринг
- Операция сложения возвращает строку обратно, что позволяет сделать “каскад” из таких сложений и собрать строку “одной строкой кода”, сборка происходит слева направо: стринг + данные1 + данные2 или данные1 + стринг + данные2 + данные3
- Результат всей суммы можно:
- Приравнять к String: стринг = стринг + данные1 + данные2
- Отправить в функцию, которая принимает String: f(стринг + данные1 + данные2)
- И так далее
Для сборки строки данный вариант менее предпочтительный, чем предыдущий с += . Ниже разберёмся, почему.
Доступ к символам
К строке можно обратиться как к массиву и прочитать или изменить символ по порядку:
Сравнение String
Стринги можно сравнивать между собой и с обычными строками ( const char* ):
Остальные методы
Рассмотрим все библиотечные методы для работы со строками, они применяются к строке через точку. В рассмотренных ниже примерах “тестовая” строка называется myString. Также оставлю некоторые комментарии по оптимизации.
myString.compareTo(myString2)
- Возвращает отрицательное число, если myString идёт до myString2
- Возвращает положительное число, если myString идёт после myString2
- Возвращает 0, если строки одинаковы
myString.replace(substring1, substring2) – в строке myString заменяет последовательность символов substring1 на substring2.
myString.substring(from) и myString.substring(from, to) – возвращает подстроку, содержащуюся в myString с позиции from и до конца, либо до позиции to
myString.toInt() – конвертирует и возвращает содержимое строки в тип данных int
Проблемы и оптимизация String
Преимущество стрингов заключается в том, что с ними очень легко и удобно работать: собирать из других строк и переменных любых типов, складывать между собой, делить на подстроки и так далее. За удобство приходится платить: String является динамическим объектом (читай урок про динамическую память), что влечёт за собой некоторые проблемы. Также на форумах часто критикуют String и предлагают использовать вместо них обычные си-строки, давайте рассмотрим всё вместе:
- String – тяжёлый. Несомненно – использование String-строк сразу добавляет пару килобайт Flash памяти к весу программы, так как для работы с ними используется менеджер памяти (встроенная библиотека). В то же время, если в программе уже используется динамическое выделение памяти – добавление String будет заметно не так сильно. На этом данная проблема заканчивается, потому что если открыть реализацию библиотеки String, то можно увидеть, что все действия со строками выполняются при помощи стандартных строковых функций языка Си (подробнее – в следующем уроке).
- String – медленный. Да, когда строка меняет свою длину – она начинает менять свой размер и даже место в оперативной памяти микроконтроллера. Переписывание и перераспределение памяти происходит отнюдь не мгновенно, поэтому операции со String выполняются относительно долго: сотни микросекунд. Если собирать строку посимвольно – каждая прибавка будет выполняться дольше, чем хотелось бы! Этого можно избежать, используя метод reserve() , который зарезервирует память, чтобы увеличение строки происходило без выделения памяти (подробнее об этом ниже). Если место под строку зарезервировано – операции со строкой будут выполняться с такой же скоростью, как и с обычными строками, потому что для них используются те же стандартные строковые функции.
- String – опасный. Всё верно, неаккуратная работа со String может привести к сильной фрагментации памяти, неправильной работе программы и даже полному её зависанию. В то же время, если понимать как работают стринги и использовать эффективные и безопасные конструкции в работе с ними – можно избежать абсолютно всех проблем!
Использование памяти
Несмотря на то, что строка – это динамический инструмент, в реализации Arduino она может только увеличиваться. Это означает, что если у нас была длинная строка, а затем мы её обнулили – места в памяти она не стала занимать меньше! То есть
Следующая созданная стринга будет размещена в памяти сразу за предыдущей. Возникает вопрос: а как тогда удалить строку и освободить память? Очень просто:
- Если строка создана глобально – никак, в библиотеке не предусмотрено публичного инструмента для очистки строки.
- Если строка создана локально – она автоматически выгрузится из памяти, когда код дойдёт до закрывающей фигурной скобки, за которой она уже не существует.
Сложение строк
Самый плохой вариант, который только можно себе представить, выглядит вот так:
Так делать нельзя, но тем не менее, на форумах очень часто можно встретить этот вариант. Здесь плохо всё: создаётся несколько “временных” экземпляров строки, под каждый выделяется память, тратится время, в результате каждый кусок начинает смещаться по памяти вперёд на новое место и изначальная строка прыгает в самый конец этого “паравозика”, образуя “дырку” в памяти (фрагментацию)!
Для сборки строки из нескольких частей рекомендуется использовать исключительно вариант с построчным прибавлением += . Пусть он не такой визуально компактный, как сложение “в одной строке кода”, но он позволяет избежать создания лишних экземпляров String, лишних перераспределений памяти и самое главное – не приводит к её фрагментации и выполняется гораздо быстрее:
Фрагментация памяти
Тут есть ещё один важный момент: если в процессе такой сборки строки создать ещё одну строку – это приведёт к сильной фрагментации памяти! Например:
В конце выполнения этого кода собранная строка состоит условно из 25 символов и должна занимать в памяти 25 байт. Но с начала выполнения этого кода свободная память уменьшилась на 50+5 байт! Как и почему это произошло:
- Перед созданием второй строки у нас уже есть строка, представим её как блок памяти [——str——]
- Мы создаём ещё одну строку, она располагается в памяти сразу за предыдущей строкой (так работает менеджер памяти) [——str——][—str2—]
- Теперь мы прибавляем к первой строке вторую: вторая строка остаётся в памяти, она никуда не пропадает, а первой нужно больше места. Поэтому менеджер памяти переносит первую строку на место сразу после второй и в памяти остаётся “дырка”! [ дырка ][—str2—][——str-str2——]
- В итоге “край” свободной памяти смещается на длину первой строки плюс длину второй строки. Беда!
С небольшими строками и кучей свободной памяти данная ситуация нам ничем не страшна, но если вы неправильно собираете например веб-страницу или другой ответ серверу – строка может начать занимать в несколько раз больше места, чем должна, и свободная оперативная память просто закончится!
Аналогичная ситуация произойдёт в том числе при вот такой записи, здесь тоже будет создана новая стринга в процессе сборки первой:
- Избегать создания новых строк в процессе работы с уже имеющимися
- Резервировать место под “прибавку”, прибавлять, а затем удалять вторую строку из памяти. Рассмотрим этот вариант
Сценарий первый, строка создаётся как переменная
Мы поместили создание второй строки в отдельный блок кода, чтобы она автоматически удалилась из памяти после выхода из этого блока.
Сценарий второй, преобразование
Как это работает:
- Была строка [——str——]
- Мы её расширили [——str—— ]
- Создали вторую строку [——str—— ][—str2—]
- Переписали [——str——str2—][—str2—]
- Удалили временную строку [——str——str2—]
- Осталась сумма и никаких препятствий в памяти
Резервирование памяти
Вернёмся к первому примеру: можно ещё сильнее оптимизировать сборку строки, если сразу зарезервировать место. Мы не знаем, какой длины она будет, но можем прикинуть. Можно даже зарезервировать чуть больше, это не так страшно:
Теперь каждое прибавление не будет приводить к перераспределению памяти и код выполнится быстрее. Также создание промежуточных строк не будет приводить к фрагментации памяти, потому что строке есть куда расти!
Оптимизация памяти
Забегая немного вперёд – текст в двойных кавычках хранится и в программной памяти программы (которой много), и в оперативной (которой мало). В уроке про PROGMEM мы рассмотрим несколько способов оптимизации памяти, но уже сейчас можно начать применять макрос F() – данный макрос позволяет хранить строку только в программной памяти и доставать её оттуда только для сложения со стрингой. Например после выполнения вот такого безобидного кода
Текст «Hello» окажется продублирован в памяти микроконтроллера целых 3 раза!
- Текст всегда хранится в памяти программы
- При запуске МК текст переписывается в оперативную память, чтобы можно было иметь к нему доступ. Находится там на всём протяжении работы программы
- Мы создали стринг-строку, в которую скопировали этот текст. Копия будет находиться в памяти, пока строка не будет удалена из памяти
Если обернуть текст в макрос F() – он будет загружаться из программной памяти напрямую в строку, и удалится из неё вместе со строкой:
Таким образом этот макрос крайне рекомендуется использовать для сборки строки с участием строковых констант. Вернёмся к нашему примеру:
Именно так и рекомендуется собирать строки.
Передача в функции
Функции очень многих библиотек для Arduino принимают String-строки. Это может быть вывод на дисплей, отправка в веб и многое другое. Если посмотреть реализацию этих функций – они принимают тип данных String& или const String& . Это – ссылка на строку, подробнее читайте в уроке про указатели и ссылки.
Как это работает и зачем: допустим вам нужна функция, которая принимает строку и что то с ней делает, например выводит в монитор порта. Это будет выглядеть примерно так:
Проблема в том, что когда мы передадим в эту функцию строку
она будет продублирована в памяти, то есть внутри нашей функции будет копия переданной строки. Если строка большая, а свободной памяти мало – быть беде. Если передать строку по ссылке – внутри функции окажется именно наша строка, лишней копии не будет.
Рекомендуется делать именно так.
Далее, если вы захотите помимо String-строк отправлять в свою функцию строковые константы (текст в кавычках) или текст внутри макроса F() для экономии памяти, то нужно будет добавить слово const :
Такая конструкция сможет эффективно принимать любые строковые данные для дальнейшей работы:
Другие библиотеки
Ради интереса я написал свою версию String, но без использования динамической памяти: максимальный размер строки задаётся при её создании, это позволяет сэкономить в сумме около 2 кБ Flash на одних и тех же операциях со строкой. Библиотека имеет такой же набор методов и возможностей, как у String, что позволяет легко заменить стандартные стринги на мои, а также там есть несколько дополнительных фишек. Библиотека называется mString, документацию и примеры смотрите на GitHub.