Коллеги, периодически приходится видеть Ваши вопросы типа «почему не отправляется смс», «почему не печатается в монитор порта», «почему не преобразуется число в строку» и прилагаемые коды, переполненные сложными манипуляциями с классом String (конкатенации по одному символу, передача по значению и т.д. и т.п.). Вот как раз на днях попалась такая тема, и это переполнило чашу терпения, решил написать для Вас маленький код, который, надеюсь, поможет Вам диагностировать самую частую проблему, возникающую при использовании класса String – нехватку памяти.
Но прежде, чем переходить к коду, три совета.
Первый совет: обязательно прочитайте вот это. Там много про String и про передачу его в функции, и про операции с ним, и вообще про то почему он является «убийцей памяти».
Второй совет: постарайтесь воздержаться от использования класса String, а там где не можете – постарайтесь локализовать его использование (фигурными скобками или ещё как).
Если же Вы по каким-то причинам не можете или не хотите отказываться от использования String, то, блин, ну хотя бы проверяйте выделилась ли объекту память! Ну что сложного написать
Если с памятью всё нормально – будет ИСТИНА, а если получается false, то приплыли! Вы же никогда этого не делаете!
Теперь переходим собственно к теме.
Итак, я предполагаю, что Вы наплевали на мой совет проверять память (или взяли чужой код, где это не делается) и Ваш код начал как-то необъяснимо глючить.
Первое, что Вы должны сделать,
увидев необъяснимые глюки кода,
в котором используется класс String –
убедиться, что ему везде хватает памяти!
Я покажу очень простой способ постоянно контролировать память без внесения изменений в Ваш код. Для это Вам потребуется выделить под контроль памяти один пин и повесить на него светодиод. Можно тот пин на котором светодиод и так есть, но если он занят (например, под SPI), то можно любой другой. Просто выделите пин и повесьте на него светодиод (если его там нет).
Далее делаем так:
1. находим файл WString.cpp (он находится в папке <Ардуино IDE>\hardware\arduino\avr\cores\arduino )
2. В самое начало файла вставляем 4 строки
1 | #define SAFE_STRING_LED LED_BUILTIN // Закомментировать эту строку, если контроль больше не нужен! |
4 | #endif // SAFE_STRING_LED |
Здесь LED_BUILTIN - это номер пина встроенного светодиода (на большинстве Ардуино - 13). Если Вы выделили другой пин - поставьте туда его номер вместо LED_BUILTIN
3. в этом же файле найдите функцию String::changeBuffer. Она выглядит вот так:
01 | unsigned char String::changeBuffer(unsigned int maxStrLen) |
03 | char *newbuffer = ( char *)realloc(buffer, maxStrLen + 1); |
и добавьте в неё четыре строки, чтобы она выглядела вот так:
01 | unsigned char String::changeBuffer(unsigned int maxStrLen) |
03 | char *newbuffer = ( char *)realloc(buffer, maxStrLen + 1); |
10 | pinMode(SAFE_STRING_LED, OUTPUT); |
11 | digitalWrite(SAFE_STRING_LED, HIGH); |
12 | #endif // SAFE_STRING_LED |
ВСЁ!
Теперь спокойно компилируйте и запускайте свой скетч. В случае нехватки памяти будет загораться светодиод. Однажды загоревшись, он будет светиться постоянно и не погаснет до перезагрузки. Таким образом, Вы всегда увидите, что в процессе работы возникла проблема с памятью.
Теперь у Вас есть инструмент, с помощью которого Вы сможете установить (или исключить) нехватку памяти для работы с классом String как причину глюков программы. Пользуйтесь им прежде, чем постить одни и те же вопросы. Работайте немножко и сами.
Только имейте в виду, данный инструмент работает только в выделением памяти в классе String. На нехватку памяти в другом месте он не реагирует. Но, как показывает анализ огромной кучи Ваших проблем - неаккуратная работа с классом String - это основная причина нехватки памяти - самая модная.
Если программа полностью оттестирована и проверку хочется убрать, достаточно закомментировать самую первую строку (она отмечена комментарием выше) и перезалить скетч. Всё остальное можно оставить до следующего раза – оно никак и ничему не мешает.
Вот пример, в котором при каждом прохождении loop к строке добавляется один символ. Примерно через 10 секунд после загрузки (для Uno) память переполнится и загорится светодиод.
63 | static String kaka = "kaka" ; |
Спасибо, полезная информация.
Спасибо, утащу в записную книжку.
Я, не могу отказаться от String, по многим причинам, в основном отсутствие опыта :-( Так что эта методика мне пригодится.
Ну не любит Евгений стринги :) Давно он их ругает, не без оснований, конечно, но краски сгущает :) Может имеет смысл, например, для всех стрингов, объявленых глобально, изначально задавать максимальный размер ?
по проверке понятно, но должен жеж быть механизм как использовать стринг и не плодить сущности
Что Вы имеете в виду?
Что Вы имеете в виду?
Вы хотите сказать, что если используем просто оператор присвоения, новая память не выделяется?
Не пойму я ничего с вашим С++
Я использую стринг. но у меня длина строки фиксирована, считал, что оператором присвоения просто затираю старое значение, как понял из объяснения, каждый раз создается новый объект данных
Наверное не правильно понял
ua6em,
в ту же самую функцию, куда мы вставляли включение светодиода, тольк в её начало вставьте печать в сериал. Например, вот так:
01
unsigned
char
String::changeBuffer(unsigned
int
maxStrLen)
02
{
03
04
Serial
.print(
"String mremory request: "
);
05
Serial
.println(maxStrLen + 1);
06
07
char
*newbuffer = (
char
*)realloc(buffer, maxStrLen + 1);
08
if
(newbuffer) {
09
buffer = newbuffer;
10
capacity = maxStrLen;
11
return
1;
12
}
13
return
0;
14
}
и Вы будете видеть все операции запроса памяти (String только здесь её запрашивает).
Разумеется, где-нибудь в сетапе вызовите Serial.begin.
Попробуйте, очень полезно для понимания внутренних механизмов, когда всё видно.
Можно также добавить печать в функцию освобождения памяти.
Если новая длинна стринга больше старой, то происходит выделение памяти, если меньше, то изменения размеров выделеной памяти не происходит. Таким образом стринг освобождает память только в момент дестроинга. То есть, если вы в начале програмы загрузите в стринговую переменную строку байт в 100, то в последствии, если вы измените содержимое этого стринга на десятибайтовое, занятая им память все равно будет такая же как для сто байтовой строки.
Если новая длинна стринга больше старой, то происходит выделение памяти, если меньше, то изменения размеров выделеной памяти не происходит. Таким образом стринг освобождает память только в момент дестроинга. То есть, если вы в начале програмы загрузите в стринговую переменную строку байт в 100, то в последствии, если вы измените содержимое этого стринга на десятибайтовое, занятая им память все равно будет такая же как для сто байтовой строки.
А если не секрет, где Вы это прочитали? Мне попадалась другая информация. А именно: при любой операции с объектом string создается новый объект и в него записывается результат операции.
Не при любой. При передаче по значению - да, но не при любой.
А прочитать надёжнее всего в исходном тексте, благо - он доступен.
как только обьявили строку, буквально следующим же оператором расширьте ее до максимального для ваших нужд значения
String s; s.reserve(int num);
пока длина строки после всех ваших манипуляций не превысит num, память перераспределяться не будет.
Ну и да, объявляйте ее локальной в блоке.
Я вот так делал экранный буфер для LCD экранчика. Lines - это String *[]
1
for
(
byte
i = 0; i < FLinesCount; i++)
2
{
3
Lines[i] =
new
String(getEmptyLine());
4
Lines[i]->reserve(FCharsCount);
5
}
О чем я и говорил. В принципе при таком подходе работа со String, мало чем отличается от работы с символьными массивами, разве только появляется минус - нужно для себя определить максимальный размер того или иного стринга, но этот косяк имеется и у массивов. За то в остальном одни плюсы :). Так что, Евгений, Вы не совсем правы :) Или знаете еще минусы ?
=======
+++
Вы не совсем правы :) Или знаете еще минусы ?
Да, Вы знаете, не нужно меня понимать прямолинейно. Я вовсе не против этого класса и сам иногда им пользуюсь. Заметьте, практически в любом моём этюде в том или ином виде присутствует фраза о том, что пользоваться можно и нужно всеми инструментами, например:
Моё мнение таково: всякий инструмент хорош, когда его используют умело и по назначению. Нельзя отказывать от инструмента потому, что в какой-то ситуации он неприменим. Нужно просто не применять его в той ситуации, а применять там, где от него наибольшая польза.
А почему не советую использовать String новичкам, так Вы знаете, String - очень сложный инструмент. Вот смотрите, у лётчиков есть погодные допуски, у электриков тоже есть допуски на те или иные работы в зависимости от квалификации, у сварщиков - одному можно варить газопроводы, а другому только калитку у бабушки, ну и т.д. В программировании никаких допусков нет, вот и начинается: человек, который не может использовать массивы char потому, что не понимает что такое указатель, почему-то считает, что он сумеет эффективно использовать класс с перегруженными операциями и динамической работой с памятью. Это не так. Не понимая механизмов работы такого класса, эффективно использовать его просто невозможно. Без культуры контроля памяти (хотя бы проверять выделилась ли память после запроса!) использование таких сложных механизмов как String превращается в русскую рулетку - авось пронесёт. Вот если человек ручками потрогал массивы char и динамическую память, тогда он хоть понимает что в этом String делается и как, и тогда - пожалуйста, пользуйтесь на здоровье. Понимаете о чём я?
Это ясно. Тут, я так понимаю, нужно выработать рекомендации как правильно пользоваться, согласен с вами. Но имхо, этот класс и создан для облегчения работы со строками, потому молодеж к нему и тянется. Хотя, если человек не понимает что такое указатель, но он кто угодно но не програмер.
Я, если позволите, выскажусь немного жестче:
на 328 и подобных, с 2К ОЗУ использовать динамическое распределение - нельзя вообще и никогда.
Если не понимаешь - то просто нельзя, а если понимаешь работу памяти и указателей, то не станешь.
String - прекрасен на Меге и, конечно, на АРМ.
Я, если позволите, выскажусь немного жестче:
на 328 и подобных, с 2К ОЗУ использовать динамическое распределение - нельзя вообще и никогда.
Если не понимаешь - то просто нельзя, а если понимаешь работу памяти и указателей, то не станешь.
String - прекрасен на Меге и, конечно, на АРМ.
даже 64 байта нельзя?
даже 64 байта нельзя?
Как обычно, в вопросе содержится ответ:
Если ты понимаешь, что 64 - можно, то это тебе просто не нужно.
Ради того, чтобы один раз написать "стринг1 + стринг2", вместо strcat()? Не смешите мои тапочки!
-------
Ясно же, что String пользуют те, кто не в состоянии воспользоваться strcat() и пр.
Это невежество, в свою очередь, имеет следствием то, что эти персонажи не ограничиваются 64-мя байтами, они просто не в курсе, что это такое.
даже 64 байта нельзя?
Как обычно, в вопросе содержится ответ:
Если ты понимаешь, что 64 - можно, то это тебе просто не нужно.
Ради того, чтобы один раз написать "стринг1 + стринг2", вместо strcat()? Не смешите мои тапочки!
-------
Ясно же, что String пользуют те, кто не в состоянии воспользоваться strcat() и пр.
Это невежество, в свою очередь, имеет следствием то, что эти персонажи не ограничиваются 64-мя байтами, они просто не в курсе, что это такое.
Ясень пень, что невежество - ведь клапауций жеж не разрешил
Увидел функцию, а в голове удивительные слова завертелись - конкатеация строк, не поленился, полез в википедию, чё за зверь
Предупреждать жеж надо, что можно использовать все функции С не зависимо от разрешений клапауция, мы жеж нубы откуда знаем