Этюды для начинающих. Контроль памяти при использовании String

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

Коллеги, периодически приходится видеть Ваши вопросы типа «почему не отправляется смс», «почему не печатается в монитор порта», «почему не преобразуется число в строку» и прилагаемые коды, переполненные сложными манипуляциями с классом String (конкатенации по одному символу, передача по значению и т.д. и т.п.). Вот как раз на днях попалась такая тема, и это переполнило чашу терпения, решил написать для Вас маленький код, который, надеюсь, поможет Вам диагностировать самую частую проблему, возникающую при использовании класса String – нехватку памяти.

Но прежде, чем переходить к коду, три совета.

Первый совет: обязательно прочитайте вот это. Там много про String и про передачу его в функции, и про операции с ним, и вообще про то почему он является «убийцей памяти».

Второй совет: постарайтесь воздержаться от использования класса String, а там где не можете – постарайтесь локализовать его использование (фигурными скобками или ещё как).

Если же Вы по каким-то причинам не можете или не хотите отказываться от использования String, то, блин, ну хотя бы проверяйте выделилась ли объекту память! Ну что сложного написать

if (myString) …

Если с памятью всё нормально – будет ИСТИНА, а если получается false, то приплыли! Вы же никогда этого не делаете!

Теперь переходим собственно к теме.

Итак, я предполагаю, что Вы наплевали на мой совет проверять память (или взяли чужой код, где это не делается) и Ваш код начал как-то необъяснимо глючить.

 


Первое, что Вы должны сделать,
увидев необъяснимые глюки кода,
в котором используется класс String –
убедиться, что ему везде хватает памяти!

 

Я покажу очень простой способ постоянно контролировать память без внесения изменений в Ваш код. Для это Вам потребуется выделить под контроль памяти один пин и повесить на него светодиод. Можно тот пин на котором светодиод  и так есть, но если он занят (например, под SPI), то можно любой другой. Просто выделите пин и повесьте на него светодиод (если его там нет).

Далее делаем так:

1. находим файл WString.cpp (он находится в папке <Ардуино IDE>\hardware\arduino\avr\cores\arduino )

2. В самое начало файла вставляем 4 строки

#define SAFE_STRING_LED LED_BUILTIN  // Закомментировать эту строку, если контроль больше не нужен!
#ifdef SAFE_STRING_LED
    #include <arduino.h>
#endif  // SAFE_STRING_LED

Здесь LED_BUILTIN - это номер пина встроенного светодиода (на большинстве Ардуино - 13). Если Вы выделили другой пин - поставьте туда его номер вместо LED_BUILTIN

3. в этом же файле найдите функцию String::changeBuffer. Она выглядит вот так:

unsigned char String::changeBuffer(unsigned int maxStrLen)
{
	char *newbuffer = (char *)realloc(buffer, maxStrLen + 1);
	if (newbuffer) {
		buffer = newbuffer;
		capacity = maxStrLen;
		return 1;
	}
	return 0;
}

и добавьте в неё четыре строки, чтобы она выглядела вот так:

unsigned char String::changeBuffer(unsigned int maxStrLen)
{
	char *newbuffer = (char *)realloc(buffer, maxStrLen + 1);
	if (newbuffer) {
		buffer = newbuffer;
		capacity = maxStrLen;
		return 1;
	}
#ifdef SAFE_STRING_LED
	pinMode(SAFE_STRING_LED, OUTPUT);
	digitalWrite(SAFE_STRING_LED, HIGH);
#endif // SAFE_STRING_LED
	return 0;
}

ВСЁ!

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

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

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

Если программа полностью оттестирована и проверку хочется убрать, достаточно закомментировать самую первую строку (она отмечена комментарием выше) и перезалить скетч. Всё остальное можно оставить до следующего раза – оно никак и ничему не мешает.

Вот пример, в котором при каждом прохождении loop к строке добавляется один символ. Примерно через 10 секунд после загрузки (для Uno) память переполнится и загорится светодиод.

/*
 Для постоянного контроля за памятью при активной работе с классом String нужно найти
 файл WString.cpp (он находится в папке <Ардуино IDE>\hardware\arduino\avr\cores\arduino )

 В самое начало файла вставить 4 строки

	#define SAFE_STRING_LED LED_BUILTIN  // Закомментировать эту строку, если контроль не нужен!
	#ifdef SAFE_STRING_LED
		#include <arduino.h>
	#endif	// SAFE_STRING_LED

 LED_BUILTIN - это номер пина встроенного светодиода (на большинстве Ардуино - 13)
 Если у Вас другой пин - поставьте туда его номер вместо LED_BUILTIN
 
 Затем нужно найти в этом же файле функцию String::changeBuffer. Она выглядит вот так:
 
		unsigned char String::changeBuffer(unsigned int maxStrLen)
		{
			char *newbuffer = (char *)realloc(buffer, maxStrLen + 1);
			if (newbuffer) {
				buffer = newbuffer;
				capacity = maxStrLen;
				return 1;
			}
			return 0;
		}
		
 и добавить в неё 4 строки, чтобы она выглядела вот так:
 
 		unsigned char String::changeBuffer(unsigned int maxStrLen)
		{
			char *newbuffer = (char *)realloc(buffer, maxStrLen + 1);
			if (newbuffer) {
				buffer = newbuffer;
				capacity = maxStrLen;
				return 1;
			}
		#ifdef SAFE_STRING_LED
			pinMode(SAFE_STRING_LED, OUTPUT);
			digitalWrite(SAFE_STRING_LED, HIGH);
		#endif // SAFE_STRING_LED
			return 0;
		}

  Теперь при ошибке выделения памяти будет загораться светодиод. Однажды загоревшись
  он будет светиться постоянно и не погаснет до перезагрузки.

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

  Если программа полностью оттестирована и проверку хочется убрать, достаточно
  закомментировать самую первую строку (она отмечена комментарием выше). Всё остальное
  можно оставить до следующего раза - оно никак и ничему не мешает.

  В примере ниже светодиод загорается примерно через 10 секунд после загрузки (для Uno)
*/


void setup(void) {
	Serial.begin(115200);
}

void loop() {
	static String kaka = "kaka";
	kaka += "a";
	delay(5);
}

 

Aleks_neofit
Aleks_neofit аватар
Offline
Зарегистрирован: 28.12.2016

Спасибо, полезная информация.

Andrey12
Andrey12 аватар
Offline
Зарегистрирован: 26.12.2014

Спасибо, утащу в записную книжку.

Я, не могу отказаться от String, по многим причинам, в основном отсутствие опыта :-( Так что эта методика мне пригодится.

brokly
brokly аватар
Offline
Зарегистрирован: 08.02.2014

Ну не любит Евгений стринги :) Давно он их ругает, не без оснований, конечно, но краски сгущает :) Может имеет смысл, например, для всех стрингов, объявленых глобально, изначально задавать максимальный размер ? 

ua6em
ua6em аватар
Offline
Зарегистрирован: 17.08.2016

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

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

Что Вы имеете в виду?

ua6em
ua6em аватар
Offline
Зарегистрирован: 17.08.2016

ЕвгенийП пишет:

Что Вы имеете в виду?

Вы хотите сказать, что если используем просто оператор присвоения, новая память не выделяется?
Не пойму я ничего с вашим С++
Я использую стринг. но у меня длина строки фиксирована, считал, что оператором присвоения просто затираю старое значение, как понял из объяснения, каждый раз создается новый объект данных
Наверное не правильно понял

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

ua6em,

в ту же самую функцию, куда мы вставляли включение светодиода, тольк в её начало вставьте печать в сериал. Например, вот так:

unsigned char String::changeBuffer(unsigned int maxStrLen)
{

        Serial.print("String mremory request: ");
        Serial.println(maxStrLen + 1);

	char *newbuffer = (char *)realloc(buffer, maxStrLen + 1);
 	if (newbuffer) {
		buffer = newbuffer;
		capacity = maxStrLen;
		return 1;
	}
	return 0;
}

и Вы будете видеть все операции запроса памяти (String только здесь её запрашивает).

Разумеется, где-нибудь в сетапе вызовите Serial.begin.

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

Можно также добавить печать в функцию освобождения памяти.

brokly
brokly аватар
Offline
Зарегистрирован: 08.02.2014

Если новая длинна стринга больше старой, то происходит выделение памяти, если меньше, то  изменения размеров выделеной памяти не происходит. Таким образом стринг освобождает память только в момент дестроинга. То есть, если вы в начале програмы загрузите в стринговую переменную строку байт в 100, то в последствии, если вы измените содержимое этого стринга на десятибайтовое, занятая им память все равно будет такая же как для сто байтовой строки.

AlexeySh
Offline
Зарегистрирован: 16.01.2017

brokly пишет:

Если новая длинна стринга больше старой, то происходит выделение памяти, если меньше, то  изменения размеров выделеной памяти не происходит. Таким образом стринг освобождает память только в момент дестроинга. То есть, если вы в начале програмы загрузите в стринговую переменную строку байт в 100, то в последствии, если вы измените содержимое этого стринга на десятибайтовое, занятая им память все равно будет такая же как для сто байтовой строки.

А если не секрет, где Вы это прочитали? Мне попадалась другая информация. А именно: при любой операции с объектом string создается новый объект и в него записывается результат операции.

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

Не при любой. При передаче по значению - да, но не при любой.

А прочитать надёжнее всего в исходном тексте, благо - он доступен.

DetSimen
DetSimen аватар
Offline
Зарегистрирован: 25.01.2017

как только обьявили строку, буквально следующим же оператором расширьте ее до максимального для ваших нужд значения

String s;  s.reserve(int num); 

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

Ну и да, объявляйте ее локальной в блоке.  

Я вот так делал экранный буфер для LCD экранчика. Lines - это String *[]

    for (byte i = 0; i < FLinesCount; i++)
    {
        Lines[i] = new String(getEmptyLine());
        Lines[i]->reserve(FCharsCount);
    }

 

brokly
brokly аватар
Offline
Зарегистрирован: 08.02.2014

О чем я и говорил. В принципе при таком подходе работа со String, мало чем отличается от работы с символьными массивами, разве только появляется минус - нужно для себя определить максимальный размер того или иного стринга, но этот косяк имеется и у массивов. За то в остальном одни плюсы :). Так что, Евгений, Вы не совсем правы :) Или знаете еще минусы ?

=======

ЕвгенийП пишет:
А прочитать надёжнее всего в исходном тексте, благо - он доступен.

+++

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

brokly пишет:

Вы не совсем правы :) Или знаете еще минусы ?

Да, Вы знаете, не нужно меня понимать прямолинейно. Я вовсе не против этого класса и сам иногда им пользуюсь. Заметьте, практически в любом моём этюде в том или ином виде присутствует фраза о том, что пользоваться можно и нужно всеми инструментами, например:

ЕвгенийП пишет:

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

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

brokly
brokly аватар
Offline
Зарегистрирован: 08.02.2014

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

wdrakula
wdrakula аватар
Offline
Зарегистрирован: 15.03.2016

Я, если позволите, выскажусь немного жестче:

на 328 и подобных, с 2К ОЗУ использовать динамическое распределение - нельзя вообще и никогда.

Если не понимаешь - то просто нельзя, а если понимаешь работу памяти и указателей, то не станешь.

String -  прекрасен на Меге и, конечно, на АРМ.

 

ua6em
ua6em аватар
Offline
Зарегистрирован: 17.08.2016

wdrakula пишет:

Я, если позволите, выскажусь немного жестче:

на 328 и подобных, с 2К ОЗУ использовать динамическое распределение - нельзя вообще и никогда.

Если не понимаешь - то просто нельзя, а если понимаешь работу памяти и указателей, то не станешь.

String -  прекрасен на Меге и, конечно, на АРМ.

 

даже 64 байта нельзя?

wdrakula
wdrakula аватар
Offline
Зарегистрирован: 15.03.2016

ua6em пишет:

даже 64 байта нельзя?

Как обычно, в вопросе содержится ответ:

Если ты понимаешь, что 64 - можно, то это тебе просто не нужно.

Ради того, чтобы один раз написать "стринг1 + стринг2", вместо strcat()? Не смешите мои тапочки!

-------

Ясно же, что String пользуют те, кто не в состоянии воспользоваться strcat() и пр.

Это невежество, в свою очередь, имеет следствием то, что эти персонажи не ограничиваются 64-мя байтами, они просто не в курсе, что это такое.

ua6em
ua6em аватар
Offline
Зарегистрирован: 17.08.2016

wdrakula пишет:

ua6em пишет:

даже 64 байта нельзя?

Как обычно, в вопросе содержится ответ:

Если ты понимаешь, что 64 - можно, то это тебе просто не нужно.

Ради того, чтобы один раз написать "стринг1 + стринг2", вместо strcat()? Не смешите мои тапочки!

-------

Ясно же, что String пользуют те, кто не в состоянии воспользоваться strcat() и пр.

Это невежество, в свою очередь, имеет следствием то, что эти персонажи не ограничиваются 64-мя байтами, они просто не в курсе, что это такое.

Ясень пень, что невежество - ведь клапауций жеж не разрешил

Увидел функцию, а в голове удивительные слова завертелись - конкатеация строк, не поленился, полез в википедию, чё за зверь

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