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

Си-строки


В прошлом уроке мы разобрали динамические String-строки в реализации Arduino, а сейчас настало время стандартных статических строк языка C/C++. Такая строка представляет собой массив символов типа char (char array) и для неё работает такой же синтаксис, как и для остальных массивов (урок про массивы). Конец строки определяется нулевым символом \0 (или целым число 0), за это такой тип строк называют null-terminated string: ноль на конце позволяет программе определять конец строки и её длину. Также это стандартные строки языка Си и поэтому называются cstring.

Текст в кавычках


Любой написанный в двойных кавычках текст "some text":

  • Является строковой константой – string constant
  • Имеет тип данных const char* – то есть указывает на свой первый символ в памяти
  • Хранится и в программной, и в оперативной памяти микроконтроллера
  • Компилятор автоматически добавляет нулевой символ в конец строки '\0' – то есть реальный размер строки всегда на 1 символ больше
  • Оптимизируется компилятором – об этом ниже

Объявление как массив


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

char str[] = {'h', 'e', 'l', 'l', 'o', 0};  // с нулевым символом на конце

Такой вариант записи не очень удобный, поэтому строки в C/C++ можно задавать просто текстом в двойных кавычках – компилятор сам посчитает размер массива:

char str[] = "hello";

Полученный выше массив содержит 6 символов: 5 на слово hello и 1 на завершающий символ. Текст в данном массиве можно изменять в процессе работы программы, потому что с точки зрения программы мы создали обычный массив и заполнили его буквами. Изменим первую букву на прописную: str[0] = 'H';. Выведем в монитор порта:

Serial.println(str);

Serial умеет работать с такими данными и с радостью их выведет.

Объявление как указатель


Также строку можно объявить как указатель на const char* – то есть сам текст в кавычках хранится где то в программе, а мы получаем на него “ссылку”:

const char* str = "hello";

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

Serial.println(str);

Оптимизация


Компилятор оптимизирует строковые константы, но не во всех случаях.

Если создать несколько строк как массивы (которые можно изменять) и присвоить им одинаковые строки, то они займут место в памяти как разные строки, т.е. столько, сколько в них суммарно символов:

char s1[] = "hello";
char s2[] = "hello";

Если создать несколько одинаковых строк как указатели – то компилятор их оптимизирует и они займут место в памяти как одна строка!

const char* s1 = "hello";
const char* s2 = "hello";

Если при выводе в Serial или передаче в другие функции мы используем одинаковые строки, то они также будут оптимизироваться и занимать место как одна строка:

Serial.println("hello");
lcd.print("hello");
String s("hello");

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

Serial.println(F("hello"));
lcd.print(F("hello"));

Сложение компилятором


Си-строки можно складывать из нескольких строк через пробелы:

char str[] = "Hello"   ", "   "World!";

И переносить сложение на новую строку:

char str[] = "Hello"
", "
"World!";

Сложение происходит на этапе компиляции, то есть скомпилированной программе это будет одна большая строка.

Отличия от String


В отличие от String-строк, Си-строки:

char str[] = "hello";
char str2[] = "world";

str += str2;      // НЕЛЬЗЯ складывать 
str = "text";     // НЕЛЬЗЯ присваивать после инициализации 
if (str == str2); // НЕЛЬЗЯ сравнивать

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

Длина строки


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

char str[100] = "World";
sizeof(str);  // вернёт 100
strlen(str);  // вернёт 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

Таким образом удобно паковать строки для создания текстовых меню и прочего. Единственный большой минус – весь этот текст висит в оперативной памяти мёртвым грузом. Можно сохранить его во 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 – шестнадцатеричная
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-строк и сделать его легче, быстрее и оптимальнее. Подробно обо всех инструментах можно почитать в официальной документации. Очень интересный пример с манипуляцией этими инструментами можно посмотреть здесь. А мы вкратце рассмотрим самые полезные.

Важный момент: библиотека работает со строками как с указателями, и многие функции возвращают как результат именно указатель. Как это понимать, если вы не читали урок про указатели и/или тема слишком сложная? Указатель – первый символ в строке, работа со строкой начнётся с него. Последним символом является нулевой символ, и для программы строка существует именно в этом диапазоне. Если какая-то функция возвращает указатель на конкретный символ в строке – по сути она возвращает часть строки, начиная с этого символа и до конца строки. Например, мы искали символ , в строке "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
char str1[15] = "hello ";
char str2[] = "world";
strcat(str1, str2);
// здесь str1 - "hello world"
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 без учёта нулевого символа.

Библиотека


У меня есть библиотека для работы с Си-строками: их преобразования и парсинга на блоки данных. Некоторые инструменты реализованы гораздо эффективнее, чем в стандартной строковой библиотеке. Библиотека называется GParser, документацию и примеры смотрите на GitHub.

Видео


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


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