Строки и массивы символов

Мы с вами уже познакомились с символами в уроке про типы данных. Напомню: символ является переменной (или константой) типа 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). Остальные “подтянутся” сами: String str = (String)10 + " value" + var + ',' + 3.14;

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

Документация на String
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!". Просто “начало” строки сместится.

Инструменты cstring
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 без учёта нулевого символа.

Видео


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


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