Этюды для начинающих: Память 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

Не можем :(