Этюды для начинающих: Память 3. Динамические и автоматические переменные

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

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

Для нормального понимания материала, следует обновить в памяти раздел «Правила области видимости» из прошлого этюда. Это важно, а повторять его здесь я не буду.

Для начала определимся с терминологией. Существует как минимум два вида такой «управляемой» памяти. Собственно-динамическая, которую Вы запрашиваете и освобождаете явно, и автоматическая, т.е. та, которая запрашивается и освобождается автоматически, без Вашего участия.

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

Пример использования динамической памяти.

char *s = new char [23]; // запрашиваем кусок, достаточный для размещения 23 переменных типа char
int *n = new int [16]; // запрашиваем кусок, достаточный для размещения 16 переменных типа int
int *m = (int *) malloc(16 * sizeof(int)); // тоже, что и строчкой выше.
byte *p = (byte *) malloc(321); // запрашиваем 321 байт

// освобождаем всё в обратном порядке
free(p);
free(m);
delete [] n;
delete [] s;

В нашем примере мы использовали для запроса памяти оператор new и функцию malloc. Для освобождения, соответственно оператор delete и функцию free.

Пример использования автоматической памяти,

Автоматически память выделяется для переменных, которые мы описываем без слова static внутри любого программного блока (внутри любой пары фигурных скобок { } ).

void loop() {
	int a = 321;
	char *s = "Hello, world!\n";
	{
		long n = 100500;
	}
	// В этой точке переменной n уже нет, а переменные a и s всё ещё доступны
}

Такая переменная живёт до фигурной скобки, закрывающей блок, в котором она объявлена. Как только программа добирается до закрывающей скобки, память (или любые другие ресурсы) выделенные под переменную освобождаются для использования под другие нужды.

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

Динамическое выделение памяти

Динамически память выделяется в т.н. куче. Куча расположена в начале свободной области памяти и растёт в сторону увеличения адресов.

Для выделения памяти стандартная библиотека предоставляет три функции: malloc, calloc и realloc.

Функция malloc

void * malloc(size_t __size);

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

Функция calloc

void * calloc(size_t __nele, size_t __size);

получает два аргумента – количество элементов под которые нужно выделить память и длину одного элемента. Функция сначала вызывает malloc с параметром, равным произведению своих аргументов, а затем, если память успешно выделилась, инициализирует выделенную память нулями. Возвращает тоже, что и malloc.

Вот собственно её текст:

void * calloc(size_t nele, size_t size) {
	void *p;
	if ((p = malloc(nele * size)) == 0) return 0;
	memset(p, 0, nele * size);
	return p;
}

Функция realloc

void * realloc(void * __ptr, size_t __size);

Эта функция эта функция может быть использована и для простого выделения памяти, но основное её назначение – изменить размер ранее выделенного участка. Первый параметр – указатель на ранее выделенный участок (он может быть равен 0), второй – новый размер этого участка в байтах.

Логика работы этой такова:

1.       Если параметр __ptrравен 0, то просто вызывается malloc, которой передаётся второй параметр и возвращается то, что вернул malloc.

2.       Если параметр __ptrуказывает на блок памяти размер которого больше, чем значение параметра __size, то размер блока урезается до __size, а освободившееся место объявляется свободным. При этом, если непосредственно за ним следует также свободная память, то эти свободные участки объединяются в один. Возвращается значение параметра __ptr.

3.       Если параметр __ptrуказывает на блок памяти размер которого равен значению параметра __size, то просто возвращается значение параметра __ptr.

4.       Если параметр __ptrуказывает на блок памяти размер которого меньше значения параметра __size, то

4.1.    Если за блоком идёт свободный участок памяти, которого хватает для нового размера блока, то блок просто расширяется за счёт того свободного участка до нужного размера и возвращается значение параметра __ptr.

4.2.    Если же за блоком нет достаточного свободного места, то запрашивается новый блок памяти нужного размера (при помощи malloc). В случае успеха, содержимое блока по адресу __ptrкопируется на новое место, блок по адресу __ptrосвобождается. Возвращается то, что вернула malloc.

Для освобождения ранее запрошенной памяти служит функция free. Её единственный параметр – адрес освобождаемого блока. Эта функция объявляет блок, адрес которого ей передан и, если перед и/или после этого блока идут участки свободной памяти, объединяет соседние свободные участки в один.

Также, для запроса и освобождения памяти можно использовать операторы new и delete. До тех пор, пока мы просто запрашиваем память под простые переменные, а не создаём экземпляры классов, разницы нет никакой. Чтобы сразу и в корне пресечь разговоры о возможной разнице и о том, что лучше, приведу тексты операторов new и delete для этого случая из файлов среды Ардуино

void *operator new(size_t size) {
  return malloc(size);
}

void *operator new[](size_t size) {
  return malloc(size);
}

void operator delete(void * ptr) {
  free(ptr);
}

void operator delete[](void * ptr) {
  free(ptr);
}

Как видите, разницы никакой нет. Ну, если не быть параноиком и не начинать кричать, что при вызове new теряются несколько лишних тактов на вызов «посредника», а если вызывать malloc прямо, то этих потерь нет.

В общем, в дальнейшем я буду говорить только об malloc и не буду больше оговариваться про другие способы запроса памяти, т.к. они все так или иначе к malloc сводятся.

Важное замечание

Отмечу еще, что в реальности malloc запрашивает на 2 байта больше, чем Вы его попросили. В этих двух байтах он хранит размер выделенного блока, чтобы free знала сколько освобождать. А при оформлении свободного участка, от него отъедается четыре байта, которые нужны, чтобы хранить размер участка и адрес следующего свободного участка в списке.

Конец важного замечания

Итак, как же мы работаем с динамической памятью? Тут надо  выделить следующие правила:

1.       Если Вы запросили память, то Вы обязаны её освободить. Никто и ни при каких обстоятельствах не освободит её за Вас. Не думайте, что она освободится сама, например, при выходе их функции, в которой Вы её запросили – не освободится (исключение – странноватая функция alloca, но о ней отдельно поговорим). Запросили – освобождаем и никак иначе.

2.       Стараемся те куски памяти, которые будут нужны в течение длительного времени, запрашивать раньше тех, которые нужны ненадолго. Это позволит снизить фрагментацию или даже избежать её полностью. В примере выше, я не зря вставил комментарий «освобождаем в обратном порядке». Стараемся так всегда и делать.

3.       Если Вам нужно много мелких блоков памяти, лучше запросить один большой. Например, если Вам надо хранить 200 переменные размером в 1 байт и Вы запросите память под массив из 200 байтов одним куском, Вы потратите 202 байта, а если Вы будете 200 раз запрашивать по одному байту, Вы потратите все 600 байтов.

О фрагментации памяти

Фрагментация – бич динамической памяти. В высокоуровневых языках, где управление памятью более продвинутое, такого явления либо не возникает, либо там есть эффективные способы с ним бороться. Здесь же … беда. Бороться можно только при помощи здравого смысла и аккуратного программирования. Итак, что это такое.

Представьте себе ситуацию, когда мы запросили блок в 200 байтов, затем блок в 1 байт, затем ещё блок в 200 байтов, затем ещё 1 байт и так далее, пока память не переполнилась. Тогда мы освободили все 200-байтовые блоки и, после этого, попытались запросить один единственный блок размером в 250 байтов. И что? Запускаем пример и смотрим:

template <typename T> inline Print & operator << (Print &s, T n) { s.print(n); return s; }

//
//	Запрашивает 2 куска памяти 200 байтов и 1 байт
//	Результаты помещает в параметры
// Возвращает true если память успешно выделилась.
//
static bool Get2MemoryBlocks(byte **block200, byte **block1) {
	*block200 = (byte *) malloc(200);
	if (*block200) {
		*block1 = (byte *) malloc(1);
		if (!*block1) free(*block200);
	}
	return *block200 && *block1;
}

void setup() {
	Serial.begin(115200);
	Serial << "*** Memory fragmentation test ***\n";
	byte *p[20], *ps[20];
	int8_t counter = 0;
	//
	// Запрашиваем пары блоков по 200 байтов и по 1 байту пока зватает памяти
	//
	for (; counter < 20 && Get2MemoryBlocks(& p[counter], & ps[counter]); counter ++);
	Serial << "counter = " << counter << "\n";
	//
	//	Успешно запросили counter пар блоков. Больше уже не лезет.
	// Теперь освобождаем все запрошенные блоки по 200 байтов
	//
	for (int8_t i=0; i < counter; i++) free(p[i]);
	//
	//	пытаемся запросить 250 байтов
	//
	byte *p250 = (byte *) malloc(250);
	if (p250) Serial << "250 bytes - succsess\n";
	else  Serial << "250 bytes - failure\n";
}

void loop() {}
//
// РЕЗУЛЬТАТ
//	*** Memory fragmentation test ***
//	counter = 6
//	250 bytes - failure

Как говаривал г-н Матроскин: «Фигвам!». Т.е. мы только что освободили ШЕСТЬ(!!!) кусков по 200 байтов, но тем не менее на один единственный кусок в 250 байтов у нас памяти не хватает! 

 

Ну, причина понятна - между нашими свободными блоками по 200 байтов встряли занятые кусочки по 1 байту и в реальности сплошного куска памяти в 250 байтов у нас таки нет :((((

 

Специально, чтобы Вы могли самостоятельно исследовать что у Вас творится с памятью, я написал небольшую библиотечку MemoryExplorer (текст в конце поста). Давайте с её помощью посмотрим, что у нас там творится. Добавляем в скетч вызов функции memoryReport и смотрим, что она нам выдаст.

 

template <typename T> inline Print & operator << (Print &s, T n) { s.print(n); return s; }
#include "MemoryExplorer.h"

//
//	Запрашивает 2 куска памяти 200 байтов и 1 байт
//	Результаты помещает в параметры
// Возвращает true если память успешно выделилась.
//
static bool Get2MemoryBlocks(byte **block200, byte **block1) {
	*block200 = (byte *) malloc(200);
	if (*block200) {
		*block1 = (byte *) malloc(1);
		if (!*block1) free(*block200);
	}
	return *block200 && *block1;
}

void setup() {
	Serial.begin(115200);
	Serial << "*** Memory fragmentation test ***\n";
	byte *p[20], *ps[20];
	int8_t counter = 0;
	//
	// Запрашиваем пары блоков по 200 байтов и по 1 байту пока зватает памяти
	//
	for (; counter < 20 && Get2MemoryBlocks(& p[counter], & ps[counter]); counter ++);
	Serial << "counter = " << counter << "\n";
	//
	//	Успешно запросили counter пар блоков. Больше уже не лезет.
	// Теперь освобождаем все запрошенные блоки по 200 байтов
	//
	for (int8_t i=0; i < counter; i++) free(p[i]);
	//
	//	пытаемся запросить 250 байтов
	//
	byte *p250 = (byte *) malloc(250);
	if (p250) Serial << "250 bytes - succsess\n";
	else  Serial << "250 bytes - failure\n";

	memoryReport("Fragmentation");
}

void loop() {}
//
// РЕЗУЛЬТАТ
//	*** Memory fragmentation test ***
//	counter = 6
//	250 bytes - failure
//	---- Memory report: Fragmentation
//	HEAP:@02BA(698)-@0871(2161);
//	Unallocated from:@078E(1934);
//	Stack pointer: @08F1(2289)
//	Free List:
//		Block at:@02BA(698); Size:200
//		Block at:@0388(904); Size:200
//		Block at:@0456(1110); Size:200
//		Block at:@0524(1316); Size:200
//		Block at:@05F2(1522); Size:200
//		Block at:@06C0(1728); Size:200
//	-----

Ну, вот они наши шесть свободных кусков по 200 байтов. Вверху можно видеть границы кучи. А свободного кусочка в 250 байтов нет и взять негде.

 

Вот такой зверь «фрагментация».

 

 

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

Автоматическое выделение памяти

Автоматически память выделяется на стеке. Стек начинается в конце свободной области памяти и растёт в сторону уменьшения адресов. Т.е. стек растёт навстречу куче – это нам пригодится!

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

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

Поэтому я не буду больше на этом останавливаться и буду говорить о «стеке», хотя часто в реальности речь идёт о каких-то других ресурсах.

Итак, как уже говорилось выше, динамическая переменная живёт только до закрывающей скобки того блока, в котором она объявлена. Это даёт нам возможность управлять динамической памятью.

Рассмотрим пример. Довольно часто на данном форуме приходится видать, как новички выделяют буферы для чтения данных (например, с веб-сервера) и забывают их освобождать, когда они уже не нужны. В результате, без памяти остаются «на раз». Вот смотрите:

template <typename T> inline Print & operator << (Print &s, T n) { s.print(n); return s; }

//	Предположим. что эта функция делает что-то нужное и важное
//	У нас же она будет просто обнулять переданный её массив.
void SomeRoutine(char *s, uint8_t len) {
	memset(s, 0, len);
}

void setup(void) {
	Serial.begin(115200);
	Serial << "Fun begins!\n";

	char buf1[500];
	SomeRoutine(buf1, sizeof(buf1));
	Serial << "Point #1\n";

	char buf2[500];
	SomeRoutine(buf2, sizeof(buf2));
	Serial << "Point #2\n";

	char buf3[500];
	SomeRoutine(buf3, sizeof(buf3));
	Serial << "Point #3\n";

	char buf4[500];
	SomeRoutine(buf4, sizeof(buf4));
	Serial << "Point #4\n";

	char buf5[500];
	SomeRoutine(buf5, sizeof(buf5));
	Serial << "Point #5\n";
}

void loop(void) {}

// РЕЗУЛЬТАТ
//	Fun Ђ

Мы наплодили массивов на 2500 байтов, а у бедной «Нанки» памяти всего два кило. Надеюсь, Вас не сильно удивило, что она послала нас на, и отказалась работать?

Но ведь наши буферы на самом деле ни для чего не нужны после того, как с ними поработала функция SomeFunction!!! Так почему бы нам не освобождать буфер каждый раз. Когда он больше не нужен? Ничего ведь сложного – фигурные скобки поставить – делов-то! Смотрите:

template <typename T> inline Print & operator << (Print &s, T n) { s.print(n); return s; }

//	Предположим. что эта функция делает что-то нужное и важное
//	У нас же она будет просто обнулять переданный её массив.
void SomeRoutine(char *s, uint8_t len) {
	memset(s, 0, len);
}

void setup(void) {
	Serial.begin(115200);
	Serial << "Fun begins!\n";
	{
	char buf1[500];
	SomeRoutine(buf1, sizeof(buf1));
	Serial << "Point #1\n";
	}
	{
	char buf2[500];
	SomeRoutine(buf2, sizeof(buf2));
	Serial << "Point #2\n";
	}
	{
	char buf3[500];
	SomeRoutine(buf3, sizeof(buf3));
	Serial << "Point #3\n";
	}
	{
	char buf4[500];
	SomeRoutine(buf4, sizeof(buf4));
	Serial << "Point #4\n";
	}
	{
	char buf5[500];
	SomeRoutine(buf5, sizeof(buf5));
	Serial << "Point #5\n";
	}
}

void loop(void) {}

// РЕЗУЛЬТАТ
//	Fun begins!
//	Point #1
//	Point #2
//	Point #3
//	Point #4
//	Point #5

Всё классно работает! Как доктор прописал! (на вопрос «а почему бы не использовать один и тот же буфер на все вызовы, ответ – моя задача сейчас показать, что они уничтожаются по достижении фигурной скобке, а не написать осмысленную программу)

Автоматическая переменная всегда уничтожается, когда программа выходит из области видимости переменной. Если эта переменная – экземпляр класса, для неё вызывается деструктор и, если деструктор нетривиален, а имеет какой-то побочный эффект, это нужно иметь в виду. Сейчас мы воспользуемся этим фактом (тем. что вызывается деструктор), чтобы более наглядно увидеть просецесс создания и уничтожения автоматической переменной.

Смотрите, переменная а создаётся в блоке, а печать контрольных точек позволяет чётко понять в какой момент она была создана и в каой уничтожена:

template <typename T> inline Print & operator << (Print &s, T n) { s.print(n); return s; }

struct Duck {
	Duck(void) { Serial << "Instance of class Duck constructed\n"; }
	~Duck(void) { Serial << "Instance of class Duck destroyed\n"; }
};

void setup(void) {
	Serial.begin(115200);
	Serial << "Fun begins!\n";

	Serial << "Point #1\n";
	{
		Serial << "Point #2\n";
		Duck a;
		Serial << "Point #3\n";
	}
	Serial << "Point #4\n";
}

void loop(void) {}

// РЕЗУЛЬТАТ
//		Fun begins!
//		Point #1
//		Point #2
//		Instance of class Duck constructed
//		Point #3
//		Instance of class Duck destroyed
//		Point #4

Обратите внимание, переменная создаётся не в момент входа в блок, а в момент объявления! А уничтожается в момент выхода из блока. Это чётко видно по контрольным токам.

Из определения стека «первым пришел – последним вышел», мы можем заключить, что никакая фрагментация с автоматическими переменными невозможна в принципе! (мы не рассматриваем лямбда-выражения и прочие подобные штучки, т.к. люди, которые владеют таким аппаратом в моих этюдах не нуждаются).

Казалось бы, вот оно, счастье! Выбросить динамические переменные на свалку и спать спокойно! Но, как известно, «если Вам кажется, что ситуация улучшается, значит Вы чего-то не заметили». В автоматических переменных есть свои засады и первая из них – неконтролируемость стека.

Вот как мы определим, что стек переполнился и налез собою на кучу, затирая там всё, что попадётся? Он ведь растёт в сторону кучи! Система этого никак не контролирует! В простейшем случае, конечно можно взять хоть мой MemoryExplorer и сравнить указатель стека с границей кучи, но это в простейшем случае. Есть миллион примеров, где это не проходит и о том, что стек переполнился мы можем  догадаться только по появлению необъяснимых глюков в программе (как у Азимова об аварии и разгерметизации космического корабля пассажиры могли «легко догадаться по отсутствию воздуха»).

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

Давайте посмотрим пример переполнения стека. В этой программе функция SomeRountine размещает на стеке 200-байтовый буфер и вызывает сама себя, попутно печатая глубину вызова. Как видите, после восьми вызовов стек налазит на кучу, всё там портит и МК перегружается. В прошлый раз, всё просто сломалось, теперь перегружается – реакция на переполнение стека может быть любой и глюки совершенно необъяснимы. Смотрите

template <typename T> inline Print & operator << (Print &s, T n) { s.print(n); return s; }

//	Предположим. что эта функция делает что-то нужное и важное
//	У нас же она будет просто обнулять переданный её массив.
void SomeRoutine(byte *s) {
	static long counter = 0;
	byte buffer[200];
	memcpy(buffer, s, 200);
	counter ++;
	Serial << "Level: " << counter << "\n";
	delay(100);
	SomeRoutine(buffer);
}

void setup(void) {
	Serial.begin(115200);
	Serial << "Fun begins!\n";

	byte buffer[200];
	SomeRoutine(buffer);
}

void loop(void) {}

// РЕЗУЛЬТАТ
//	Fun begins!
//	Level: 1
//	Level: 2
//	Level: 3
//	Level: 4
//	Level: 5
//	Level: 6
//	Level: 7
//	Level: 8Fun begins!
//	Level: 1
//	Level: 2
//	Level: 3
//	Level: 4
//	Level: 5
//	Level: 6
//	Level: 7
//	Level: 8Fun begins!
//	Level: 1
//	Level: 2

Совместное использование автоматической и динамической схем

Очень часто эти два типа памяти используются совместно, хотя это иногда бывает неочевидно.

Самый простой пример – указатель. Часто сам указатель является автоматической переменной, а память на которую он указывает, запрашивается динамически.

char *s = (char *) maclloc(321);

Здесь указатель s - автоматическая переменная, занимающая два байта. Она будет уничтожена при выходе из блока. Но это абсолютно не означает, что будет освобождена память. на которую этот указатель указывает. Память запрашивалась динамически и её нужно явно освободить. Пока Вы этого не сделаете, этого не сделает никто.

Менее очевидный случай - использование классов в которых используется динамическая память. например, столь любимый всеми начинающими класс String. В классах правда немного проще, там об освобождении памяти позаботится деструктор, но проблемы такое скрытое использование динамической памяти создаёт и ещё какие. дело в том, что при создании экземпляра класса String, создаётся собственно сама переменная типа String (возможно, автоматически) и происходит запрос памяти под хранение строки. Сама по себе переменная типа String занимает 6 байтов и это не зависит от размера хранимой строки. А вот под хранение строки память запрашивается динамически. если строке становится мало места. память перезапрашивается (функцией realloc), а то место, где строка была раньше идёт на подкорм фрагментации.

Специально, чтобы показать, что происходит внутри String я вставил трассировку в конструктор, деструктор и в функцию запроса памяти. Посмотрите, как изнутри выглядит довольно типичная для данного форума конструкция:

void setup(void) {
	Serial.begin (115200);
	int n = 12;
	String s;
	s = "Result: ";
	s += n;
	s += " kHz (";
	s += n*1000;
	s += " Hz)";
}

void loop(void) {}

// РЕЗУЛЬТАТ
//		Created: 0 (0)
//		GetMemorySuccess (realloc): 1 (0)
//		GetMemorySuccess (realloc): 9 (0)
//		GetMemorySuccess (realloc): 11 (0)
//		GetMemorySuccess (realloc): 17 (0)
//		GetMemorySuccess (realloc): 22 (0)
//		GetMemorySuccess (realloc): 26 (0)
//		Destroyed: 0 (0)

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

	String s;
	s = "Result: ";
	s += String(n);
	s += " kHz (";
	s += String(n*1000);
	s += " Hz)";

А это уже полный ужас! Хотите посмотреть, что скажет memoryExplorer?

#include "MemoryExplorer.h"

void setup(void) {
	Serial.begin (115200);
	int n = 12;
	memoryReport("BEFORE STRING MANIPULATIONS");
	String s;
	s = "Result: ";
	s += String(n);
	s += " kHz (";
	s += String(n*1000);
	s += " Hz)";
	memoryReport("AFTER STRING MANIPULATIONS");
}

void loop(void) {}

// РЕЗУЛЬТАТ
//		---- Memory report: BEFORE STRING MANIPULATIONS
//		HEAP:@02E8(744)-@085B(2139);
//		Unallocated from:@02E8(744);
//		Stack pointer: @08DB(2267)
//		Free List: EMPTY
//		-----
//		Created: 0 (0)
//		GetMemorySuccess (realloc): 1 (0)
//		GetMemorySuccess (realloc): 9 (0)
//		Created: 1 (1)
//		GetMemorySuccess (realloc): 3 (1)
//		GetMemorySuccess (realloc): 11 (0)
//		Destroyed: 1 (1)
//		GetMemorySuccess (realloc): 17 (0)
//		Created: 2 (2)
//		GetMemorySuccess (realloc): 6 (2)
//		GetMemorySuccess (realloc): 22 (0)
//		Destroyed: 2 (2)
//		GetMemorySuccess (realloc): 26 (0)
//		---- Memory report: AFTER STRING MANIPULATIONS
//		HEAP:@02E8(744)-@085B(2139);
//		Unallocated from:@0314(788);
//		Stack pointer: @08DB(2267)
//		Free List:
//			Block at:@02E8(744); Size:14
//		-----
//		Destroyed: 0 (0)

Пожалуйста - восемь запросов и повисший дефрагментированный кусок памяти. До наших манипуляций свободная память начиналась с адреса 744. а теперь начинается с 788 - не жирно ли? Приехали, в общем.

Фигурные скобки – великая вещь!

Как быть? Я понимаю, что для многих, кто вчера увидел программирование, этот класс - просто спасение. Написать такое вручную без ошибок – серьёзный проект. Понимаю!

Но, ребята, давайте просто работать чуть менее расхлябанно. Для чего нужна такая строка? Чтобы её, например, напечатать или там по смс выслать, так?. Так давайте мы эту печать расположим рядом формированием строки и всё вместе возьмём в фигурные скобки!

Переменная, поскольку она автоматическая, по достижению скобки удалится, а её деструктор освободит всю память, т.е. память полностью восстановится, как она была до объявления строки. Да, восемь запросов останутся при нас, но никакой фрагментации и никакого расхода памяти! А всего-то и делов - поставить пару скобок!

#include "MemoryExplorer.h"

void setup(void) {
	Serial.begin (115200);
	int n = 12;
	memoryReport("BEFORE STRING MANIPULATIONS");
	{
		String s;
		s = "Result: ";
		s += String(n);
		s += " kHz (";
		s += String(n*1000);
		s += " Hz)";
		Serial.println(s);
	}
	memoryReport("AFTER STRING MANIPULATIONS");
}

void loop(void) {}

// РЕЗУЛЬТАТ
//		---- Memory report: BEFORE STRING MANIPULATIONS
//		HEAP:@02E8(744)-@085B(2139);
//		Unallocated from:@02E8(744);
//		Stack pointer: @08DB(2267)
//		Free List: EMPTY
//		-----
//		Created: 0 (0)
//		GetMemorySuccess (realloc): 1 (0)
//		GetMemorySuccess (realloc): 9 (0)
//		Created: 1 (1)
//		GetMemorySuccess (realloc): 3 (1)
//		GetMemorySuccess (realloc): 11 (0)
//		Destroyed: 1 (1)
//		GetMemorySuccess (realloc): 17 (0)
//		Created: 2 (2)
//		GetMemorySuccess (realloc): 6 (2)
//		GetMemorySuccess (realloc): 22 (0)
//		Destroyed: 2 (2)
//		GetMemorySuccess (realloc): 26 (0)
//		Result: 12 kHz (12000 Hz)
//		Destroyed: 0 (0)
//		---- Memory report: AFTER STRING MANIPULATIONS
//		HEAP:@02E8(744)-@085B(2139);
//		Unallocated from:@02E8(744);
//		Stack pointer: @08DB(2267)
//		Free List: EMPTY
//		-----

Вот и всё. Никакой фрагментации. Нераспределённая память как начиналась с адрес 744, так и начинается. Памтяь полностью восстановлена, пользуйтесь этим!

Типичные ошибки при работе с памятью

Этот раздел будет дополняться ошибками. Которые мне будут встречаться на просторах форума. Я их буду здесь разбирать. Специально для этих дополнений, а также для исправления возможных ошибок в кодах данного топика, я сразу же после публикацию отвечу на топик, и править и дополнять буду там, чтобы исправления и дополнения висели в начале темы, а не были в ней затеряны.

Итак, поехали по ошибкам.

1. Неоправданная передача параметра функции по значению

Коллеги, представим себе, что у нас есть строка (пусть любимый String) и нам надо её напечатать и выслать по СМС. Печатью занимается отдельная функция. Как передать в неё строку? Часто здесь на форуме я вижу вот такую передачу:

void printString(const String s) {
	Serial.println(s);
}

void setup(void) {
	Serial.begin (115200);
	String s = 
		"Never Imagine yourself not to be otherwise than what it might "
		"appear to others that what you were or might have been was not "
		"otherwise than what you had been would have appeared to them "
		"to be otherwise";
	printString(s);
}

void loop(void) {}

Это называется передача по значению. При такой передаче (внимание!!!) создаётся новый экземпляр - копия передаваемой переменной и этот новый экземпляр передаётся в функцию! После выхода из функции созданный экземпляр уничтожается.

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

Created: 0 (0)
GetMemorySuccess (realloc): 202 (0)
Created: 1 (1)
GetMemorySuccess (realloc): 202 (1)
Never Imagine yourself not to be otherwise than what it might appear to others that what you were or might have been was not otherwise than what you had been would have appeared to them to be otherwise
Destroyed: 1 (1)
Destroyed: 0 (0)

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

А может, не будем создавать копию? Может, ну её нафиг эту передачу по значению? Давайте передавать по ссылке. Для этого и нужно-то одну (!!!!) буковку добавить в объявлении функции. Вот скетч, в который добавлен 1 (один!) символ.

void printString(const String & s) {
	Serial.println(s);
}

void setup(void) {
	Serial.begin (115200);
	String s = 
		"Never Imagine yourself not to be otherwise than what it might "
		"appear to others that what you were or might have been was not "
		"otherwise than what you had been would have appeared to them "
		"to be otherwise";
	printString(s);
}

void loop(void) {}

// РЕЗУЛЬТАТ
//	Created: 0 (0)
//	GetMemorySuccess (realloc): 202 (0)
//	Never Imagine yourself not to be otherwise than what it might appear to others that what you were or might have been was not otherwise than what you had been would have appeared to them to be otherwise
//	Destroyed: 0 (0)

Ну, это ж совсем другое дело! Никаких новых экземпляров – создаётся строка и спокойно печатается.

Ребята, без нужды большие объекты по значению не передаём!

2. Использование уничтоженной автоматической переменной

Допустим, стоит задача написать функцию, которая преобразовывает частоту к виду, пригодному для печати. При этом, если она меньше килогерца, то печатать в формате “NNN Hz”, а если больше, то округлять до ближайшей 1000 и печатать в формате “MMM kHz”.

Одно из возможных решений приведено в скетче ниже:

char * ferquencyToString(const long freq) {
	char buffer [20];
	long f;
	char * tail;
	if (freq > 1000) {
		f = (freq + 500) / 1000;
		tail = " kHz";
	} else {
		f = freq;
		tail = " Hz";
	}
	ltoa(f, buffer, 10);
	strcat(buffer, tail);
	return buffer;
}

void setup(void) {
	Serial.begin (115200);
	Serial.println(ferquencyToString(13000));
	Serial.println(ferquencyToString(490));
}

void loop(void) {}

// РЕЗУЛЬТАТ
//		13 kHz
//		490 Hz

Но здесь есть очень коварная ошибка. Дело в том, buffer - переменная автоматическая, а значит, после выхода из функции её память будет освобождена. Поэтому что мы увидим на печати зависит от того, успел кто-то в эту память что-то новое записать или не успел. если не успел. то нам повезло - напечатается нормально, а если успел - полезет грязь.

 

Более того, программа может сначала работать нормально, а через месяц, при малейшем изменении перестать работать. А уже месяц прошёл ... вот ищи что там случилось. очень коварная ошибка.

 

Как с этим бороться? Ну, можно передавать функции свой буфер или, если это не мешает ничему другому, просто добавить в объявление переменной buffer слово static.

3. Преобразование константных указателей к не константным типам

 

В другой теме, я как-то уже приводил программу, которая родилась в процессе подготовки этого этюда. Программа выглядит несколько экзотически – печатает вроде бы константу, а печатается совсем не то, что написано. Вот она:

 

 

void awfulShitCode(const char * s)  {
	*((long*)s+1)=1702125896l;
}

void setup(void) {
	Serial.begin(115200);
	awfulShitCode("Oh!");
	Serial.println("Love you!");
}

void loop(void) {}

// РЕЗУЛЬТАТ
// Hate you!

Ну, что здесь. Главная ошибка в том, в функции awfulShitCode мы преобразовали константный указатель - параметр к неконстантному long *. А дальше уже дело техники - мы просто испортили тектсовую константу "Love you!", заменив в ней первое слово.

Выводы: что же всё-таки использовать?

Часто на форуме приходится слышать «динамическую память использовать нельзя – она приводит к фрагментации».

При всём уважении к коллегам, я не могу разделить такого мнения. Ну, во-первых, я могу ответить: «Стек использовать нельзя – его переполнение не контролируемо», но я не буду так говорить потому, что было бы глупостью.

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

Из того, что молотком неудобно красить стены, ещё не значит,
что надо забивать гвозди малярной кистью

Автоматические переменные проще, динамические дают больший контроль над ситуацией. У этих областей памяти разные области видимости. Они просто разные.

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

Обещанная библиотека MemoryExplorer

Файл MemoryExplorer.h

#ifndef	MEMORY_EXPLRORER_H
#define	MEMORY_EXPLRORER_H

#include <arduino.h>

extern char *__brkval;

class Pointer : public Printable {
public:
	Pointer(const void * ptr = NULL) : m_ptr(ptr) {}

	size_t printTo(Print& p) const {
		char szBuffer[13];
		sprintf(szBuffer, "@%04X(%d)", (unsigned)m_ptr, (unsigned)m_ptr);
		return  p.print(szBuffer);
	}
	
private:
	const void * m_ptr;
};

//
//	Возвращает текущее значение указателя стека
// как беззнаковое целое и макрос для печати указателя стека
//
static inline unsigned getSP(void) { return (unsigned)(SP); }
#define SP_Pointer	(Pointer((void*)SP))

//
//	Возвращает текущее значение адрес "конца" кучи
// как беззнаковое целое и как Pointer для печати
//
static inline unsigned heapEnd(void) { return (__malloc_heap_end) ? (unsigned)__malloc_heap_end : SP - __malloc_margin; }
#define	HE_Pointer	(Pointer((__malloc_heap_end) ? __malloc_heap_end : (void *)(SP - __malloc_margin)))
//
//	Возвращает текущее значение адреса "начала" кучи
//
static inline unsigned heapStart(void) { return (unsigned)__malloc_heap_start; }
#define	HS_Pointer	(Pointer(__malloc_heap_start))
//
// Возвращает текущее значение адреса начала ещё нераспределённого куска памяти
//
static inline unsigned newSoil(void) { return (unsigned)(__brkval ? __brkval : __malloc_heap_start); }
#define	NS_Pointer	(Pointer(__brkval ? __brkval : __malloc_heap_start))


extern void memoryReport(const char *title);

#endif	//	MEMORY_EXPLRORER_H

Файл MemoryExplorer.cpp

#include <arduino.h>
#include "MemoryExplorer.h"


typedef struct __freelist {
	size_t _size;
	struct __freelist *_next;
} FreeList;

extern FreeList *__flp;
extern char *__brkval;

int getNumberOfBlocksInFreeList() {
	FreeList *fp;
	int i;
	for(i=0,fp=__flp;fp;fp=fp->_next,i++);
	return i;
}


void memoryReport(const char * title) {
	Serial.print("---- Memory report: ");
	Serial.println(title);
	Serial.print("HEAP:");
	Serial.print(HS_Pointer);
	Serial.print('-');
	Serial.print(HE_Pointer);
	Serial.print(";\nUnallocated from:");
	Serial.print(NS_Pointer);
	Serial.print(";\nStack pointer: ");
	Serial.println(SP_Pointer);
	Serial.print("Free List:");
	FreeList *ptr = __flp;
	if (!ptr) Serial.print(" EMPTY");
	while (ptr) {
		Serial.print("\n\tBlock at:");
		Serial.print(Pointer(ptr));
		Serial.print("; Size:");
		Serial.print(ptr->_size);
		ptr = ptr->_next;
	}
	Serial.print("\n-----\n");
}

 

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

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

Кстати, я там обещал рассказать про «странноватую функцию alloca» и забыл.

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

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

Заметьте, запрос памяти происходит в момент вызова alloca, а освобождение не в конце блока, а в конце функции! В примере это хорошо видно. Если проследить за «Stack pointer» - в точке 2 память ещё не была запрошена. А в точке 4 – всё ещё была.

#include <alloca.h>
#include "MemoryExplorer.h"

template <typename T> inline Print & operator << (Print &s, T n) { s.print(n); return s; }


void someFunction(void) {
	memoryReport("POINT #1");
	{
		memoryReport("POINT #2");
		char * s = alloca(100);
		memset(s, 0, 100);
		memoryReport("POINT #3");
	}	
	memoryReport("POINT #4");
}

void setup(void) {
	Serial.begin (115200);
	memoryReport("POINT #0");
	someFunction();
	memoryReport("POINT #5");
}

void loop(void) {}

// РЕЗУЛЬТАТ
//		---- Memory report: POINT #0
//		HEAP:@0278(632)-@084E(2126);
//		Unallocated from:@0278(632);
//		Stack pointer: @08CE(2254)
//		Free List: EMPTY
//		-----
//		---- Memory report: POINT #1
//		HEAP:@0278(632)-@0848(2120);
//		Unallocated from:@0278(632);
//		Stack pointer: @08C8(2248)
//		Free List: EMPTY
//		-----
//		---- Memory report: POINT #2
//		HEAP:@0278(632)-@0848(2120);
//		Unallocated from:@0278(632);
//		Stack pointer: @08C8(2248)
//		Free List: EMPTY
//		-----
//		---- Memory report: POINT #3
//		HEAP:@0278(632)-@07E4(2020);
//		Unallocated from:@0278(632);
//		Stack pointer: @0864(2148)
//		Free List: EMPTY
//		-----
//		---- Memory report: POINT #4
//		HEAP:@0278(632)-@07E4(2020);
//		Unallocated from:@0278(632);
//		Stack pointer: @0864(2148)
//		Free List: EMPTY
//		-----
//		---- Memory report: POINT #5
//		HEAP:@0278(632)-@084E(2126);
//		Unallocated from:@0278(632);
//		Stack pointer: @08CE(2254)
//		Free List: EMPTY
//		-----

 

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

Дождались :)

Jeka_M
Jeka_M аватар
Offline
Зарегистрирован: 06.07.2014

Товарищи модераторы, надо бы темку закрепить среди остальных этюдов. А то так затеряется.

Ворота
Ворота аватар
Offline
Зарегистрирован: 10.01.2016

Так как этой биб-кой узнавать сколько памяти я могу запросить, в смысле сколько мне могут выделить?

arduino328
Offline
Зарегистрирован: 01.09.2016

Вопрос к ЕвгениюП:

Если мы определяем в функции массив, то при закрытии функции массив должен удалиться. А если ссылку на этот массив возвращает оператор return, то значит он вернёт адрес на массив в свободной памяти.
И где изначально создастся массив (а он может быть большим): в куче или в стеке?

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

arduino328 пишет:

в куче или в стеке?

По идее на стеке, но стандарт языка оставляет физическое размещение на усмотрение реализации, только требует выполнения чётких условий (например, никакой фрагментации, своевременное освобождение и т.п.).

Как в этой реализации, я не знал, но это легко посмотреть всё той же библиотечкой MemoryExplorer. Смотрите:

#include "MemoryExplorer.h"

void SomeRoutine(void) {
	char buffer[200];
	memoryReport("INSIDE");
	Serial.print("address of buffer:");
	Serial.println(Pointer(buffer));
}

void setup(void) {
	Serial.begin(115200);
	memoryReport("BEFORE");
	Serial.println();
	SomeRoutine();
	Serial.println();
	memoryReport("AFTER");
}

void loop(void) {}

// РЕЗУЛЬТАТ
//		---- Memory report: BEFORE
//		HEAP:@027A(634)-@086F(2159);
//		Unallocated from:@027A(634);
//		Stack pointer: @08EF(2287)
//		Free List: EMPTY
//		-----
//		
//		---- Memory report: INSIDE
//		HEAP:@027A(634)-@079F(1951);
//		Unallocated from:@027A(634);
//		Stack pointer: @081F(2079)
//		Free List: EMPTY
//		-----
//		address of buffer:@082C(2092)
//		
//		---- Memory report: AFTER
//		HEAP:@027A(634)-@0871(2161);
//		Unallocated from:@027A(634);
//		Stack pointer: @08F1(2289)
//		Free List: EMPTY
//		-----

Сотрите на значение указателя стека (строки 25, 32 и 40), а также не адрес массива (строка 35). Если при этом помнить, что стек растёт в сторону уменьшения адресов, то вывод однозначный - на стеке.

Кстати, куча при этом вообще "не шелохнётся" - строки 24, 31, 39

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

Ворота пишет:

Так как этой биб-кой узнавать сколько памяти я могу запросить, в смысле сколько мне могут выделить?

Это не так просто. Память Вам может быть выделена как из нераспределённой области кучи (границы кучи в строке "HEAP", а адрес начала нераспределённой области в строке "Unallocated from"). Но также, память Вам могут выделить из любого свободного фрагментированного куска, если там хватает места. Библиотека печатает адреса и размеры фрагментированных кусков в отчёте, но доступа к ним программного не даёт. Можете добавить.

Arhat109-2
Offline
Зарегистрирован: 24.09.2015

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

arduino328
Offline
Зарегистрирован: 01.09.2016

Вопрос к ЕвгениюП:

arduino328 пишет:
Если мы определяем в функции массив, то при закрытии функции массив должен удалиться. А если ссылку на этот массив возвращает оператор return, то значит он вернёт адрес на массив в свободной памяти.

Действительно ли оператор return возвращает адрес на массив в освобождённой памяти?

qwone
qwone аватар
Offline
Зарегистрирован: 03.07.2016

Стеки бывают разные. Если стек процессора это одно, а если стек для вычисления, то это совсем другое. Компилятор подключает библиотеку для работы с числовыми выражениями. Вот у них есть свой стек. И разумеется его компилятор использует  для создания временых переменных, если "посчитает нужным".

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

arduino328 пишет:

Действительно ли оператор return возвращает адрес на массив в освобождённой памяти?

Ну, конечно. А что, есть сомнения? Так легко же проверить. Сделаем две функции и посмотрим, что они возвращают.

#include "MemoryExplorer.h"

char * ferquencyToString(void) {
	char buffer [20];
	memset(buffer, 0, sizeof(buffer));
	return buffer;
}

char * voltageToString(void) {
	char buffer [20];
	memset(buffer, 0, sizeof(buffer));
	return buffer;
}

void setup(void) {
	Serial.begin (115200);
	Serial.print("1st buffer address: ");
	Serial.println(Pointer(ferquencyToString()));
	Serial.print("2nd buffer address: ");
	Serial.println(Pointer(voltageToString()));
}

void loop(void) {}

// РЕЗУЛЬТАТ
//		1st buffer address: @08DC(2268)
//		2nd buffer address: @08DC(2268)

Как видите в двух разных функциях массив буффер расположился на одном и том же месте в памяти. Значит, когда работала вторая функция, буфер первой уже числился "свободным участком".

arduino328
Offline
Зарегистрирован: 01.09.2016

Итак, в посте 6 мы выяснили, что массив функция расположит в стеке. А в посте 11 узнали, что после выполнения функции оператор return возвращает указатель на массив, находящийся в освобождённой памяти.
Значит, если после возврата из функции запустится другая функция (например функция прерывания), то массив затрётся другими данными и использовать его не получится. "Шеф, всё пропало!" :(

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

arduino328 пишет:

Итак, в посте 6 мы выяснили, что массив функция расположит в стеке. А в посте 11 узнали, что после выполнения функции оператор return возвращает указатель на массив, находящийся в освобождённой памяти.

В освобождённой части стека.

Да, всё пропало, не возвращайте из функции указатель на автоматическую переменную - классическая ошибка.

arduino328
Offline
Зарегистрирован: 01.09.2016

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

arduino328 пишет:

Итак, в посте 6 мы выяснили, что массив функция расположит в стеке. А в посте 11 узнали, что после выполнения функции оператор return возвращает указатель на массив, находящийся в освобождённой памяти.

В освобождённой части стека.

Да, всё пропало, не возвращайте из функции указатель на автоматическую переменную - классическая ошибка.

В этюде про это не было сказано, а мы как дети малые не знаем и мучаемся :)

Arhat109-2
Offline
Зарегистрирован: 24.09.2015

Как это "не было сказано"? П.2, использование удаленной автоматической переменной .. вполне себе классика ошибок начинающего, особенно после перехода с разного рода "вумных" недоязыков типа PHP с автоматическим управлением кучами .. привычка - вторая натура, однако. :)

arduino328
Offline
Зарегистрирован: 01.09.2016

Да действительно было сказано в п.2, но про стек там не упоминалось - вот и не сложились в голове нужные ассоциации :(

Arhat109-2
Offline
Зарегистрирован: 24.09.2015

Да ну как жеж не було сказано? В самом верху, апосля заголовка прямо так и прописано: "автоматическая память - выделяемая на СТЕКЕ" .. :)

.. на редкость полный и нейтральный этюд, свободный от каких-либо "догм".

arprudnikov
Offline
Зарегистрирован: 13.03.2017


 p = finger.image2Tz(1);                Serial.print  ("Image converting: ");                                       // Конвертируем первое изображение и возвращаем результат выполнения данной операции в переменную p
  switch(p){                                                                                                         // Проверка ответа ...
    case FINGERPRINT_OK:                 Serial.println("Ok!");                                       break;         // Изображение сконвертировано
    case FINGERPRINT_IMAGEMESS:          Serial.println("Image too messy :(");                        return p;      // Изображение слишком нечеткое
    case FINGERPRINT_PACKETRECIEVEERR:   Serial.println("Communication error :(");                    return p;      // Ошибка соединения
    case FINGERPRINT_FEATUREFAIL:        Serial.println("No fingerprint on image :(");                return p;      // Ошибка конвертирования
    case FINGERPRINT_INVALIDIMAGE:       Serial.println("No fingerprint on image :(");                return p;      // Ошибка изображения
    default:                             Serial.println("Unknown error :(");                          return p;      // Неизвестная ошибка
  }
//Просим убрать палец от сканера

 

можете подсказать как эту переменную ,,р'' вывести как изображение? как дополнить скетч чтоб на компе сохранилось изображение.?

спаибо!!

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

Не можем :(

Ворота
Ворота аватар
Offline
Зарегистрирован: 10.01.2016

kskot пишет:
как преобразовать значение с ацп 0-1023 до 0.0 до 29.9

Просто домножить на "29.9/1023.0" не судьба?

vk007
Offline
Зарегистрирован: 16.06.2015

kskot пишет:

Доброго времени суток,сильно непинайте, на форуме первый раз,может конечно не в той теме пишу,и чесноговоря обычно разбераля сам но вот  понадобилось мне значение с ацп преобразовать в число с точкой и тут индформации по данному вопросу толком ненашол.Раньше с целыми числами делал так:

 #define pin 2
 int val;
  
  val = analogRead(pin);
  val = map(val, 0, 1023, 0, 100);    
  val = constrain(val, 0, 100) ;
 
А вот с числами с точкой зашол в тупик,может знающие гуру подскажут как преобразовать значение с ацп 0-1023 до 0.0 до 29.9
Заранее благодарен.

Ну если информации не нашли, а своей фантазии не хватает, то делайте как привыкли, через map:

float val = map(analogRead(pin), 0, 1023, 0, 299) / 10.0;

kskot
Offline
Зарегистрирован: 30.09.2018

Благодарю,да чтото за деление я както и неподумал :-) спасибо большое

andriano
andriano аватар
Offline
Зарегистрирован: 20.06.2015

То, что предложил Ворота, короче, быстрее и точнее, чем map + деление.

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

kskot пишет:
поправьте если ошибаюсь

Поправляю, тема не та. 

ku-ku
Offline
Зарегистрирован: 14.11.2018

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

Мы дописали функцию вывода свободной памяти в сериал и пол дня непрерывно сидели и наблюдали за количеством свободной памяти (за дурной головой и глазам покоя нет). За эти пол дня мы видели лишь небольшие флуктуации размера свободной памяти, но не видели ни пиков резких, ни выхода за некие разумные рамки.
Означает ли это, что в следующие дни не произойдет никакого переполнения или резких скачков? Или же вероятность ошибок памяти все же отлична от нуля даже после длительного периода стабильной работы?

Ивиняюсь за столь глупый вопрос и за формулировку. Спасибо!

andriano
andriano аватар
Offline
Зарегистрирован: 20.06.2015

ku-ku пишет:

Или же вероятность ошибок памяти все же отлична от нуля даже после длительного периода стабильной работы?

Совершенно верно. 

Отладка и тестирования статистически приводят к уменьшению вероятности появления сбоев, но при этом:

1. Не приводят к уменьшению этой вероятности до 0.

2. Не гарантируют, что в даном конкретном случае эта вероятность вообще уменьшается.

qwone
qwone аватар
Offline
Зарегистрирован: 03.07.2016

ku-ku пишет:
Означает ли это, что в следующие дни не произойдет никакого переполнения или резких скачков? Или же вероятность ошибок памяти все же отлична от нуля даже после длительного периода стабильной работы?
Допустим у вас есть автомобиль. И вы стараетесь не гробить свою машину и водите ее плавно, без рыков. Если ли вероятность того что в какой-то момент у вас поменяется стиль вождения и на ресурс машины вам будет нас**ть. Так и здесь . Оптимально написанная программа это программа ,где потребление памяти и мощности сбалансировано. Но если код кривой, то тут уже другие следствия и последствия. 

zaxa
Offline
Зарегистрирован: 23.02.2019

Спасибо за труд!

А есть такой же анализ ошибок работы с прерываниями?

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

Думаю, что есть, искать надо. Народу в мире много. Кто-нить, чо-нить обязательно накропал.

sadman41
Offline
Зарегистрирован: 19.10.2016

Вопрос по памяти. Правильно ли я понимаю, что текущий размер стека в нашей Ардуине вычисляется нижеприведённым способом?

  uint16_t stackSize = (uint16_t)RAMEND - (uint16_t)SP;

 

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

Да. Тока это расстояние от конца памяти до верхушки стека в данный, конкретный момент.  Выйди из функции где вычисляется это и все изменится

sadman41
Offline
Зарегистрирован: 19.10.2016

Да, это я понимаю. Хочу посмотреть, насколько стек раздувается. Пока без конкретного применения, но думаю, что в отладке поможет как-нить.

inspiritus
Offline
Зарегистрирован: 17.12.2012

Уважаемый Евгений!!! Огромная Вам моя благодарност за этюды Ваши и стремление поделиться знаниями и опытом.

Внезапно за конструкцию 

{
String s;
s= .........;
s+=..........;
........
cilent.println(s);
}

поскольку я именно так формирую HTML я, и фигурные скобки изменили всё.

       моему агрегатору сразу капитально полегчало с оперативой:)

             практически, как после начала правильного применения PROGMEM .

                   минус 12%, и полагаю это еще не предел, если осмыслить все остальное.

несмотря на прикупленную DUE, я пока так и не перешёл на неё, практически спортивный интерес : а что еще я смогу впихнуть в МЕГУ ?

Прошу не глумиться, но многое в программировании на C для меня вдруг оказывается откровением ( все-таки тридцать лет других занятий не способствовали моему пргрессу в качестве программиста, со времён TurboPascal 5..) 

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

Вот было бы славно!!! Если бы мудрые модераторы разместили этюды уважаемого Евгения отдельным топом, наравне с общие, песочница, ЭТЮДЫ (все этюды), аппаратное, программное и прочее, оставив при этом обсуждение в программном, но дав права Автору на исправление релиза публикаций этюдов в топе.

... и тогда взалкавшим знаний не пришлось бы рыскать по всему форуму в поисках оных.

bwn
Offline
Зарегистрирован: 25.08.2014

inspiritus пишет:

Вот было бы славно!!! Если бы мудрые модераторы разместили этюды уважаемого Евгения отдельным топом, наравне с общие, песочница, ЭТЮДЫ (все этюды), аппаратное, программное и прочее, оставив при этом обсуждение в программном, но дав права Автору на исправление релиза публикаций этюдов в топе.

... и тогда взалкавшим знаний не пришлось бы рыскать по всему форуму в поисках оных.

Все хотелки не умею, но Евгений дал ссылки на все свои этюды и темы в "Песочнице". Когда выйду из творческого криза, они будут рассортированы в третьем топике "Песочницы". С остальным, как решат те, у кого больше прав.)))

inspiritus
Offline
Зарегистрирован: 17.12.2012

Здравствуйте Уважемый ЕвгенийП.

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

 

	{
		String s;
		s = "Result: ";
		s += String(n);
		s += " kHz (";
		s += String(n*1000);
		s += " Hz)";
		Serial.println(s);
	}

и

	for (int i=0; i<1000; i++) 
{
		String s;
		s = "Result: ";
		s += String(n);
		s += " kHz (";
		s += String(n*1000);
		s += " Hz)";
		Serial.println(s);
	}

или надо добавлять еще пару {}

 


	for (int i=0; i<1000; i++) 
{
	     { String s;
		s = "Result: ";
		s += String(n);
		s += " kHz (";
		s += String(n*1000);
		s += " Hz)";
		Serial.println(s); }
	}

?

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

не надо.  s перед } сам отойдет

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

Нет, здесь нет разницы и дополнительная пара скобок лишняя.

Другое дело, что код сильно неидеален. С памятью то у него всё нормально в плане освобождения, но запросов слишком много. Он будет выделять и освобождать память очень много раз. Да, к тому же, он будет 1000 раз создавать совершенно не нужные объекты типа String в строках №№ 4 и 6 (по первому коду).

Давайте посмотрим что с ним не так, и что можно сделать. Итак, я включил печать оповещений о запросах/освобождениях памяти и запускаю Ваш код вот в таком виде (1000 заменил на 10, чтобы не утонуть в печати).

void setup(void) {
	Serial.begin(57600);
	Serial.println("Fun begins!");
	for (int i=0; i<10; i++) {
		const int n = i;
		String s;	
		s = "Result: ";
		s += String(n);
		s += " kHz (";
		s += String(n*1000);
		s += " Hz)";
		Serial.println(s);
	}
}

void loop(void) {}

Выдаётся вот такой результат:

Fun begins!
Constructor #1: 2294
Memory allcoation: object at 2294; size: 1 - Ok; address: 630
Memory allcoation: object at 2294; size: 9 - Ok; address: 630
Constructor #8: 2288
Memory allcoation: object at 2288; size: 2 - Ok; address: 641
Memory allcoation: object at 2294; size: 10 - Ok; address: 645
Destructor of object @: 2288
Freeing buffer of: 2288
Memory allcoation: object at 2294; size: 16 - Ok; address: 645
Constructor #8: 2288
Memory allcoation: object at 2288; size: 2 - Ok; address: 641
Memory allcoation: object at 2294; size: 17 - Ok; address: 645
Destructor of object @: 2288
Freeing buffer of: 2288
Memory allcoation: object at 2294; size: 21 - Ok; address: 645
Result: 0 kHz (0 Hz)
Destructor of object @: 2294
Freeing buffer of: 2294
Constructor #1: 2294
Memory allcoation: object at 2294; size: 1 - Ok; address: 630
Memory allcoation: object at 2294; size: 9 - Ok; address: 630
Constructor #8: 2288
Memory allcoation: object at 2288; size: 2 - Ok; address: 641
Memory allcoation: object at 2294; size: 10 - Ok; address: 645
Destructor of object @: 2288
Freeing buffer of: 2288
Memory allcoation: object at 2294; size: 16 - Ok; address: 645
Constructor #8: 2288
Memory allcoation: object at 2288; size: 5 - Ok; address: 638
Memory allcoation: object at 2294; size: 20 - Ok; address: 645
Destructor of object @: 2288
Freeing buffer of: 2288
Memory allcoation: object at 2294; size: 24 - Ok; address: 645
Result: 1 kHz (1000 Hz)
Destructor of object @: 2294
Freeing buffer of: 2294
Constructor #1: 2294
Memory allcoation: object at 2294; size: 1 - Ok; address: 630
Memory allcoation: object at 2294; size: 9 - Ok; address: 630
Constructor #8: 2288
Memory allcoation: object at 2288; size: 2 - Ok; address: 641
Memory allcoation: object at 2294; size: 10 - Ok; address: 645
Destructor of object @: 2288
Freeing buffer of: 2288
Memory allcoation: object at 2294; size: 16 - Ok; address: 645
Constructor #8: 2288
Memory allcoation: object at 2288; size: 5 - Ok; address: 638
Memory allcoation: object at 2294; size: 20 - Ok; address: 645
Destructor of object @: 2288
Freeing buffer of: 2288
Memory allcoation: object at 2294; size: 24 - Ok; address: 645
Result: 2 kHz (2000 Hz)
Destructor of object @: 2294
Freeing buffer of: 2294
Constructor #1: 2294
Memory allcoation: object at 2294; size: 1 - Ok; address: 630
Memory allcoation: object at 2294; size: 9 - Ok; address: 630
Constructor #8: 2288
Memory allcoation: object at 2288; size: 2 - Ok; address: 641
Memory allcoation: object at 2294; size: 10 - Ok; address: 645
Destructor of object @: 2288
Freeing buffer of: 2288
Memory allcoation: object at 2294; size: 16 - Ok; address: 645
Constructor #8: 2288
Memory allcoation: object at 2288; size: 5 - Ok; address: 638
Memory allcoation: object at 2294; size: 20 - Ok; address: 645
Destructor of object @: 2288
Freeing buffer of: 2288
Memory allcoation: object at 2294; size: 24 - Ok; address: 645
Result: 3 kHz (3000 Hz)
Destructor of object @: 2294
Freeing buffer of: 2294
Constructor #1: 2294
Memory allcoation: object at 2294; size: 1 - Ok; address: 630
Memory allcoation: object at 2294; size: 9 - Ok; address: 630
Constructor #8: 2288
Memory allcoation: object at 2288; size: 2 - Ok; address: 641
Memory allcoation: object at 2294; size: 10 - Ok; address: 645
Destructor of object @: 2288
Freeing buffer of: 2288
Memory allcoation: object at 2294; size: 16 - Ok; address: 645
Constructor #8: 2288
Memory allcoation: object at 2288; size: 5 - Ok; address: 638
Memory allcoation: object at 2294; size: 20 - Ok; address: 645
Destructor of object @: 2288
Freeing buffer of: 2288
Memory allcoation: object at 2294; size: 24 - Ok; address: 645
Result: 4 kHz (4000 Hz)
Destructor of object @: 2294
Freeing buffer of: 2294
Constructor #1: 2294
Memory allcoation: object at 2294; size: 1 - Ok; address: 630
Memory allcoation: object at 2294; size: 9 - Ok; address: 630
Constructor #8: 2288
Memory allcoation: object at 2288; size: 2 - Ok; address: 641
Memory allcoation: object at 2294; size: 10 - Ok; address: 645
Destructor of object @: 2288
Freeing buffer of: 2288
Memory allcoation: object at 2294; size: 16 - Ok; address: 645
Constructor #8: 2288
Memory allcoation: object at 2288; size: 5 - Ok; address: 638
Memory allcoation: object at 2294; size: 20 - Ok; address: 645
Destructor of object @: 2288
Freeing buffer of: 2288
Memory allcoation: object at 2294; size: 24 - Ok; address: 645
Result: 5 kHz (5000 Hz)
Destructor of object @: 2294
Freeing buffer of: 2294
Constructor #1: 2294
Memory allcoation: object at 2294; size: 1 - Ok; address: 630
Memory allcoation: object at 2294; size: 9 - Ok; address: 630
Constructor #8: 2288
Memory allcoation: object at 2288; size: 2 - Ok; address: 641
Memory allcoation: object at 2294; size: 10 - Ok; address: 645
Destructor of object @: 2288
Freeing buffer of: 2288
Memory allcoation: object at 2294; size: 16 - Ok; address: 645
Constructor #8: 2288
Memory allcoation: object at 2288; size: 5 - Ok; address: 638
Memory allcoation: object at 2294; size: 20 - Ok; address: 645
Destructor of object @: 2288
Freeing buffer of: 2288
Memory allcoation: object at 2294; size: 24 - Ok; address: 645
Result: 6 kHz (6000 Hz)
Destructor of object @: 2294
Freeing buffer of: 2294
Constructor #1: 2294
Memory allcoation: object at 2294; size: 1 - Ok; address: 630
Memory allcoation: object at 2294; size: 9 - Ok; address: 630
Constructor #8: 2288
Memory allcoation: object at 2288; size: 2 - Ok; address: 641
Memory allcoation: object at 2294; size: 10 - Ok; address: 645
Destructor of object @: 2288
Freeing buffer of: 2288
Memory allcoation: object at 2294; size: 16 - Ok; address: 645
Constructor #8: 2288
Memory allcoation: object at 2288; size: 5 - Ok; address: 638
Memory allcoation: object at 2294; size: 20 - Ok; address: 645
Destructor of object @: 2288
Freeing buffer of: 2288
Memory allcoation: object at 2294; size: 24 - Ok; address: 645
Result: 7 kHz (7000 Hz)
Destructor of object @: 2294
Freeing buffer of: 2294
Constructor #1: 2294
Memory allcoation: object at 2294; size: 1 - Ok; address: 630
Memory allcoation: object at 2294; size: 9 - Ok; address: 630
Constructor #8: 2288
Memory allcoation: object at 2288; size: 2 - Ok; address: 641
Memory allcoation: object at 2294; size: 10 - Ok; address: 645
Destructor of object @: 2288
Freeing buffer of: 2288
Memory allcoation: object at 2294; size: 16 - Ok; address: 645
Constructor #8: 2288
Memory allcoation: object at 2288; size: 5 - Ok; address: 638
Memory allcoation: object at 2294; size: 20 - Ok; address: 645
Destructor of object @: 2288
Freeing buffer of: 2288
Memory allcoation: object at 2294; size: 24 - Ok; address: 645
Result: 8 kHz (8000 Hz)
Destructor of object @: 2294
Freeing buffer of: 2294
Constructor #1: 2294
Memory allcoation: object at 2294; size: 1 - Ok; address: 630
Memory allcoation: object at 2294; size: 9 - Ok; address: 630
Constructor #8: 2288
Memory allcoation: object at 2288; size: 2 - Ok; address: 641
Memory allcoation: object at 2294; size: 10 - Ok; address: 645
Destructor of object @: 2288
Freeing buffer of: 2288
Memory allcoation: object at 2294; size: 16 - Ok; address: 645
Constructor #8: 2288
Memory allcoation: object at 2288; size: 5 - Ok; address: 638
Memory allcoation: object at 2294; size: 20 - Ok; address: 645
Destructor of object @: 2288
Freeing buffer of: 2288
Memory allcoation: object at 2294; size: 24 - Ok; address: 645
Result: 9 kHz (9000 Hz)
Destructor of object @: 2294
Freeing buffer of: 2294

На самом деле здесь 10 (по количеству прохождений цикла) одинаковых кусков. Первый кусок - строки №№ 2-19, второй - строки №№ 20-37, и т.д.

Давайте сомтреть, что мы здесь видим.

Строки №№ 3 и 4 листинга показывают два запроса памяти. Первый под пустую строку, а второй - под строку в 9 байтов. Это лишнее. Если бы вместо

String s;
s = "Result: ";

Вы написали просто 

String s("Result: ");

то был бы один запрос на 9 байтов. Запроса на 1 байт не было бы.

Далее. Строки листинга №№ 5,6,8 и 9 - это создание нового объекта типа String из-за того, что  в строке программы №8 Вы написали String(n). Если бы Вы написали там просто n, то этих операций бы не было. Тоже самое касается строк листинга №№ 11,12,14 и 15 - они порождение совершенно ненужной конструкции String(n*1000) в строке программы №10. Там было достаточно просто n*1000.

Подведём промежуточный итог, а потом пойдём дальше Сейчас у Вас на каждый проход цикла выполняется восемь операций запроса памяти (посчитайте сами). Если исправить те две мелкие погрешности, что мы обсудили, то запросов будет всего пять на проход цикла - уже прогресс! Убедимся в этом:

void setup(void) {
	Serial.begin(57600);
	Serial.println("Fun begins!");
	for (int i=0; i<10; i++) {
		const int n = i;
		String s("Result: ");
		s += n;
		s += " kHz (";
		s += n*1000;
		s += " Hz)";
		Serial.println(s);
	}
}

void loop(void) {}

//	РЕЗУЛЬТАТ
//
//	Fun begins!
//	Constructor #1: 2294
//	Memory allcoation: object at 2294; size: 9 - Ok; address: 614
//	Memory allcoation: object at 2294; size: 10 - Ok; address: 614
//	Memory allcoation: object at 2294; size: 16 - Ok; address: 614
//	Memory allcoation: object at 2294; size: 17 - Ok; address: 614
//	Memory allcoation: object at 2294; size: 21 - Ok; address: 614
//	Result: 0 kHz (0 Hz)
//	Destructor of object @: 2294
//	Freeing buffer of: 2294
//	Constructor #1: 2294
//	Memory allcoation: object at 2294; size: 9 - Ok; address: 614
//	...

Поехали дальше.

А, собственно, зачем при каждом проходе цикла перезапрашивать память? String устроена так, что она не будет запрашивать память заново, если новая строка короче существующей. Поэтому, если мы не будем удалять объект каждый раз, то она будет запрашивать только тогда, когда реально не хватает, а всё остальное время "переиспользовать" уже запрошенный кусок! Для того, чтобы объект не удалялся при каждом проходе цикла, вынесем его за цикл. А для того, чтобы он удалился после всего, заключим его вместе с циклом в фигурные скобки.

Имеем:

void setup(void) {
	Serial.begin(57600);
	Serial.println("Fun begins!");
	{
		const char * const title = "Result: ";
		String s(title);
		for (int i=0; i<10; i++) {
			const int n = i;
			s += n;
			s += " kHz (";
			s += n*1000;
			s += " Hz)";
			Serial.println(s);
			s = title;
		}
	}
}

void loop(void) {}

Результат:

Fun begins!
Constructor #1: 2294
Memory allcoation: object at 2294; size: 9 - Ok; address: 614
Memory allcoation: object at 2294; size: 10 - Ok; address: 614
Memory allcoation: object at 2294; size: 16 - Ok; address: 614
Memory allcoation: object at 2294; size: 17 - Ok; address: 614
Memory allcoation: object at 2294; size: 21 - Ok; address: 614
Result: 0 kHz (0 Hz)
Memory allcoation: object at 2294; size: 24 - Ok; address: 614
Result: 1 kHz (1000 Hz)
Result: 2 kHz (2000 Hz)
Result: 3 kHz (3000 Hz)
Result: 4 kHz (4000 Hz)
Result: 5 kHz (5000 Hz)
Result: 6 kHz (6000 Hz)
Result: 7 kHz (7000 Hz)
Result: 8 kHz (8000 Hz)
Result: 9 kHz (9000 Hz)
Destructor of object @: 2294
Freeing buffer of: 2294

Это полный результат - не сокращённый. Смотрите - она запрашивала память ТОЛЬКО при первом проходе цикла и ещё разок при втором - и всё. Все остальные проходы цикла пролетели без запросов памяти вовсе!

Т.е. мы выиграли практически во столько раз, сколько у нас проходв цикла!

Разобрались?

---------------

Ну и на десерт, маленький офф-топик.

Чего Вам всем так мёдом эта String намазана? Вот Ваш пример без String:

void setup(void) {
	Serial.begin(57600);
	Serial.println("Fun begins!");
	for (int i=0; i<10; i++) {
		const int n = i;
		Serial.print("Result: ");
		char buffer[12];
		itoa(n, buffer, sizeof(buffer));
		Serial.print(buffer);
		Serial.print(" kHz (");
		itoa(n*1000, buffer, sizeof(buffer));
		Serial.print(buffer);
		Serial.println(" Hz)");
	}
}

void loop(void) {}

Нормально работает. И давайте сравним. Пример, который я назвал "промежуточным" выше занимал 3782 байта программной памяти. Этот пример занимает 1768. Т.е. отказавшись от String я сэкономил более 2000 байтов! И, кстати, выиграл по быстродействию, можете запустить и убедиться.

inspiritus
Offline
Зарегистрирован: 17.12.2012

Ага, понятно. Благодарю Вас.

inspiritus
Offline
Зарегистрирован: 17.12.2012

Уважаемый Евгений! Не раскрыта тема указателей! Может быть расскажете?

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

Там много тем не раскрыто на самом деле.

stepan_sotnikov
Offline
Зарегистрирован: 06.04.2019

Недавно обнаружил:
Хотелось бы услышать коментарии от Гуров =)
 

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

char   prs (char *s)   //изменяем содержимое массива 
{
  int i = -1;
  while (s[++i] != '*')
  {
    s[i] -= 1;    //из содержимого вычитаем единицу
  }
}
char   *s = "ffff*";  //обявляем массив по адресу инициализируем "ffff*"
void loop()
{
  Serial.println (s);
  prs (s); //обрабатываем массив 
  Serial.println (s);        //выводим содрежание массива ожидаемо получаем "eeee*"

  s = "ffff*";             //казалось бы переменной присваиваем значение "ffff*"
  Serial.println (s);     //читаем содержимое.... "eeee*", а там ка было "eeee*" так и осталось
  prs (s);               //с упорством осетра идущего на нерест еще раз изменяем содержимое на -1
  Serial.println (s);   //и получаем из "eeee*" "bbbb*", хотя мы хотели из "ffff*" получить "eeee*"
                       //ПОЧЕМУ ТАК? Как говорила Кэти-Скарлетт О’Хара Гамильтон Кеннеди Батлер "Об этом я подумаю завтра"
  s = "hhhh*";        //ну и еще одно попытка, присваиваем массиву значение "hhhh*" 
  Serial.println (s);//о чудо получилось присвоить "hhhh*"
  prs (s);
  Serial.println (s);//получили "gggg*"
  
  s = "hhhh*";                  // ну и еще раз...
  Serial.println (s);          // но "hhhh*" уже не та
  prs (s);
  Serial.println (s);

s = "ffff*";             
  Serial.println (s);     
  prs (s);               //
  Serial.println (s); 
  delay (1000000); // при выполнении все 2 "ffff*" и "hhhh*" прересчитаются в "*****"
                  // именно две т.к. компилятор их видит как одинаковые константы. И повторное явное s = "ffff*";  
                 // не приводит к желаемому, т.к. по факту "ffff*" мы давно уже поменяли
}
/*ATmega328P
1.8.5
Скетч использует 1870 байт (5%) памяти устройства. Всего доступно 32256 байт.
Глобальные переменные используют 202 байт (9%) динамической памяти, оставляя 1846 байт для локальных переменных. Максимум: 2048 байт.
*/

 

sadman41
Offline
Зарегистрирован: 19.10.2016

Ключевая фраза "казалось бы". Чего вы ожидаете присваивая указателю литерал?

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

stepan_sotnikov пишет:

Хотелось бы услышать коментарии 

Ну, если хотелось, так чего не задали никаких вопросов? Вам что-то непонятно или так - потрындеть захотелось?

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

1. В строке №24 (в комментарии) написан бред. Там получается совсем не это;

2. Компилятор, бедняга, аж изматерился весь, предупреждая, что так делать нельзя, но нам-то пофиг, плевали мы на него! Вспоминается стишок про "не влезай - убьёт".

stepan_sotnikov
Offline
Зарегистрирован: 06.04.2019

Ок. Вопрос. Литералы это фиксированные значения в коде программы? А так же Литералы записанные в коде программы не должны изменяться. Однако в приведенном коде, который я запускал, и знаю об предупреждениях компилятора, что там не все по фэншую. Литералы должны оставаться не изменными, но этого не происходит? Или я чего то не понимаю? 

b707
Offline
Зарегистрирован: 26.05.2017

stepan_sotnikov - сравните строчки 14 и 21 в своем коде. Ничего не замечаете?

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

stepan_sotnikov пишет:

Ок. Вопрос.

Ок. Ответ.

stepan_sotnikov пишет:

Литералы это фиксированные значения в коде программы?

Зависит от языка программирования. Здесь строковые литералы - нет не фиксированные. Если быть более строгим, они здесь и не литералы вовсе. Кстати, в Фортране IV такие фокусы проходили и с числовыми литералами - можно было спокойно изменить литерал 2 на 28, а потом всем показывать, как 2/7 получается 4.

stepan_sotnikov пишет:

Литералы записанные в коде программы не должны изменяться. ... Литералы должны оставаться не изменными

Кому это они должны или не должны? Это Ваша фантазия. Мир устроен по-другому.

stepan_sotnikov пишет:

но этого не происходит

Так ото ж. Происходит всё так, как должно происходить в этом языке программирования, я не так, как Вам видится в Вашей картине мира.

stepan_sotnikov пишет:

Или я чего то не понимаю? 

Таки да. Вы просто недостаточно хорошо знаете этот язык программирования.

stepan_sotnikov пишет:
знаю об предупреждениях компилятора

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

Если Вам хочется обмануть компилятор, это надо делать тоньше. Я тут на форуме выкладывал пример, где также меняется текстовая константа (причём, явно описанная как const и ни в какие функции ни разу не передаваемая) и при этом компилятор даже рта не раскрывает. Можете полюбопытствовать :)

stepan_sotnikov
Offline
Зарегистрирован: 06.04.2019

14 строка указатель на строковый литерал, то есть по сути переменная s хранит адрес на начало массива строковой пеерменной. А вот что происходит в 21 мне не понятно. По сути я по адрессу на первую ячейку ссылки на строковую переменную запихал строковую переменную... и теперь она куда то ссылаеться...  

sadman41
Offline
Зарегистрирован: 19.10.2016

Вот именно - "куда-то". И показывает на "что-то". У меня на одно, у вас на другое, а ЕвгенийП вообще не будет заливать это в МК. Вот вы пытались объяснить, почему пьяного мотает из стороны в сторону именно так, а не иначе? 

stepan_sotnikov
Offline
Зарегистрирован: 06.04.2019

Да, я плохо знаю этот язык и пытаюсь его понять. Спасибо за ответ "Здесь строковые литералы - нет не фиксированные. Если быть более строгим, они здесь и не литералы вовсе." Принято к сведенью. 
Сбивало с толку неожиданное выполнение котроллером этого бреда. И отсутсвия отладчика. 

stepan_sotnikov
Offline
Зарегистрирован: 06.04.2019

Основная головоломка была, не в том, что адресу присваивалась литера.
Основное недопонимание у меня было именно, в том как этот язык работает с литералами.
Даже такая програма, будет каждый раз его менять. То есть char * s = "ffff*" выполняется единожды.
и в s записываеться адрес начала массива. При повторном выполнении loop, в переменную записываеться адресс массива, но уже без присвоения. Но функция prs уже его поковыряла. Если запустить подобный код на большенстве языков, то "ffff*" станит константой, и при каждом цикле s[]="ffff*" в любом случаии спасибо за помошь. 

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

char   prs (char *s)  
{
  int i = -1;
    while (s[++i] != '*')
      {
        s[i] -=  1;
      }
}

void loop()
{
  char * s = "ffff*";  
    Serial.println (s);
      prs (s); 

delay (1000); 
}