Этюды для начинающих: Память 3. Динамические и автоматические переменные
- Войдите на сайт для отправки комментариев
В прошлый раз мы с Вами говорили о статической памяти, т.е. о памяти, которая выделяется один раз перед началом выполнения программы и которую потом невозможно освободить. Сегодня поговорим о памяти динамической, т.е. о той, которой Вы можете управлять: выделить, освободить, выделить под другие нужды, снова освободить и т.д.
Для нормального понимания материала, следует обновить в памяти раздел «Правила области видимости» из прошлого этюда. Это важно, а повторять его здесь я не буду.
Для начала определимся с терминологией. Существует как минимум два вида такой «управляемой» памяти. Собственно-динамическая, которую Вы запрашиваете и освобождаете явно, и автоматическая, т.е. та, которая запрашивается и освобождается автоматически, без Вашего участия.
Управление динамической памятью сводится к запросам и освобожденьям нужных Вам областей. Вы сами из запрашиваете и освобождаете тогда, когда Вам нужно. Управление автоматической памятью непрямое. Поскольку она запрашивается и освобождается без Вашего явного участия, управлять этим процессом можно, если знать правила по которым она управляется и и их использовать.
Пример использования динамической памяти.
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"); }
Пожалуйста, не отвечайте на этот пост, чтобы я мог его редактировать.
Кстати, я там обещал рассказать про «странноватую функцию alloca» и забыл.
Функция пришла из тех времён, когда в языке не было динамических массивов и позволяет динамически запросить память на стеке. Отличие от обычного динамического запроса в том, что память, запрошенную alloca не нужно освобождать – она сама освободится при выходе из функции, в которой запрошена.
Честно говоря, сейчас, когда есть динамические массивы, я не вижу особой нужды в её использовании, но она есть и работает.
Заметьте, запрос памяти происходит в момент вызова alloca, а освобождение не в конце блока, а в конце функции! В примере это хорошо видно. Если проследить за «Stack pointer» - в точке 2 память ещё не была запрошена. А в точке 4 – всё ещё была.
Дождались :)
Товарищи модераторы, надо бы темку закрепить среди остальных этюдов. А то так затеряется.
Так как этой биб-кой узнавать сколько памяти я могу запросить, в смысле сколько мне могут выделить?
Вопрос к ЕвгениюП:
Если мы определяем в функции массив, то при закрытии функции массив должен удалиться. А если ссылку на этот массив возвращает оператор return, то значит он вернёт адрес на массив в свободной памяти.
И где изначально создастся массив (а он может быть большим): в куче или в стеке?
в куче или в стеке?
По идее на стеке, но стандарт языка оставляет физическое размещение на усмотрение реализации, только требует выполнения чётких условий (например, никакой фрагментации, своевременное освобождение и т.п.).
Как в этой реализации, я не знал, но это легко посмотреть всё той же библиотечкой MemoryExplorer. Смотрите:
Сотрите на значение указателя стека (строки 25, 32 и 40), а также не адрес массива (строка 35). Если при этом помнить, что стек растёт в сторону уменьшения адресов, то вывод однозначный - на стеке.
Кстати, куча при этом вообще "не шелохнётся" - строки 24, 31, 39
Так как этой биб-кой узнавать сколько памяти я могу запросить, в смысле сколько мне могут выделить?
Это не так просто. Память Вам может быть выделена как из нераспределённой области кучи (границы кучи в строке "HEAP", а адрес начала нераспределённой области в строке "Unallocated from"). Но также, память Вам могут выделить из любого свободного фрагментированного куска, если там хватает места. Библиотека печатает адреса и размеры фрагментированных кусков в отчёте, но доступа к ним программного не даёт. Можете добавить.
Ну вот, неплохое описание работы с памятью и без разного рода дурацких поучений "так можно, а так только дураки пишут" .. можно. По-всякому можно, когда знаешь что и как делаешь. В качестве развития понимания - вполне пойдет. Спасибо.
Вопрос к ЕвгениюП:
Действительно ли оператор return возвращает адрес на массив в освобождённой памяти?
Стеки бывают разные. Если стек процессора это одно, а если стек для вычисления, то это совсем другое. Компилятор подключает библиотеку для работы с числовыми выражениями. Вот у них есть свой стек. И разумеется его компилятор использует для создания временых переменных, если "посчитает нужным".
Действительно ли оператор return возвращает адрес на массив в освобождённой памяти?
Ну, конечно. А что, есть сомнения? Так легко же проверить. Сделаем две функции и посмотрим, что они возвращают.
Как видите в двух разных функциях массив буффер расположился на одном и том же месте в памяти. Значит, когда работала вторая функция, буфер первой уже числился "свободным участком".
Итак, в посте 6 мы выяснили, что массив функция расположит в стеке. А в посте 11 узнали, что после выполнения функции оператор return возвращает указатель на массив, находящийся в освобождённой памяти.
Значит, если после возврата из функции запустится другая функция (например функция прерывания), то массив затрётся другими данными и использовать его не получится. "Шеф, всё пропало!" :(
Итак, в посте 6 мы выяснили, что массив функция расположит в стеке. А в посте 11 узнали, что после выполнения функции оператор return возвращает указатель на массив, находящийся в освобождённой памяти.
В освобождённой части стека.
Да, всё пропало, не возвращайте из функции указатель на автоматическую переменную - классическая ошибка.
Итак, в посте 6 мы выяснили, что массив функция расположит в стеке. А в посте 11 узнали, что после выполнения функции оператор return возвращает указатель на массив, находящийся в освобождённой памяти.
В освобождённой части стека.
Да, всё пропало, не возвращайте из функции указатель на автоматическую переменную - классическая ошибка.
В этюде про это не было сказано, а мы как дети малые не знаем и мучаемся :)
Как это "не было сказано"? П.2, использование удаленной автоматической переменной .. вполне себе классика ошибок начинающего, особенно после перехода с разного рода "вумных" недоязыков типа PHP с автоматическим управлением кучами .. привычка - вторая натура, однако. :)
Да действительно было сказано в п.2, но про стек там не упоминалось - вот и не сложились в голове нужные ассоциации :(
Да ну как жеж не було сказано? В самом верху, апосля заголовка прямо так и прописано: "автоматическая память - выделяемая на СТЕКЕ" .. :)
.. на редкость полный и нейтральный этюд, свободный от каких-либо "догм".
можете подсказать как эту переменную ,,р'' вывести как изображение? как дополнить скетч чтоб на компе сохранилось изображение.?
спаибо!!
Не можем :(
Просто домножить на "29.9/1023.0" не судьба?
Доброго времени суток,сильно непинайте, на форуме первый раз,может конечно не в той теме пишу,и чесноговоря обычно разбераля сам но вот понадобилось мне значение с ацп преобразовать в число с точкой и тут индформации по данному вопросу толком ненашол.Раньше с целыми числами делал так:
Ну если информации не нашли, а своей фантазии не хватает, то делайте как привыкли, через map:
float val = map(analogRead(pin), 0, 1023, 0, 299) / 10.0;
Благодарю,да чтото за деление я както и неподумал :-) спасибо большое
То, что предложил Ворота, короче, быстрее и точнее, чем map + деление.
Поправляю, тема не та.
позвольте поинтересоваться глупым вопросом: допустим мы имеем некий алгоритм с десятком функций. Каждая из функций дергает светодиод. Например: первая функция вширь, вторая - вбок, третья - вглубь и так далее. Каждая из функицй действует буквально пару секунд и исполняются они подряд. Никаких обработчиков внешних событий не предусмотрено. Как вшили, так оно и дергает.
Мы дописали функцию вывода свободной памяти в сериал и пол дня непрерывно сидели и наблюдали за количеством свободной памяти (за дурной головой и глазам покоя нет). За эти пол дня мы видели лишь небольшие флуктуации размера свободной памяти, но не видели ни пиков резких, ни выхода за некие разумные рамки.
Означает ли это, что в следующие дни не произойдет никакого переполнения или резких скачков? Или же вероятность ошибок памяти все же отлична от нуля даже после длительного периода стабильной работы?
Ивиняюсь за столь глупый вопрос и за формулировку. Спасибо!
Или же вероятность ошибок памяти все же отлична от нуля даже после длительного периода стабильной работы?
Отладка и тестирования статистически приводят к уменьшению вероятности появления сбоев, но при этом:
1. Не приводят к уменьшению этой вероятности до 0.
2. Не гарантируют, что в даном конкретном случае эта вероятность вообще уменьшается.
Спасибо за труд!
А есть такой же анализ ошибок работы с прерываниями?
Думаю, что есть, искать надо. Народу в мире много. Кто-нить, чо-нить обязательно накропал.
Вопрос по памяти. Правильно ли я понимаю, что текущий размер стека в нашей Ардуине вычисляется нижеприведённым способом?
Да. Тока это расстояние от конца памяти до верхушки стека в данный, конкретный момент. Выйди из функции где вычисляется это и все изменится
Да, это я понимаю. Хочу посмотреть, насколько стек раздувается. Пока без конкретного применения, но думаю, что в отладке поможет как-нить.
Уважаемый Евгений!!! Огромная Вам моя благодарност за этюды Ваши и стремление поделиться знаниями и опытом.
Внезапно за конструкцию
поскольку я именно так формирую HTML я, и фигурные скобки изменили всё.
моему агрегатору сразу капитально полегчало с оперативой:)
практически, как после начала правильного применения PROGMEM .
минус 12%, и полагаю это еще не предел, если осмыслить все остальное.
несмотря на прикупленную DUE, я пока так и не перешёл на неё, практически спортивный интерес : а что еще я смогу впихнуть в МЕГУ ?
Прошу не глумиться, но многое в программировании на C для меня вдруг оказывается откровением ( все-таки тридцать лет других занятий не способствовали моему пргрессу в качестве программиста, со времён TurboPascal 5..)
Каждый раз читаю с истинным наслаждением этюды Ваши, каюсь, не всегда успеваю обкатать, но стремлюсь.
Вот было бы славно!!! Если бы мудрые модераторы разместили этюды уважаемого Евгения отдельным топом, наравне с общие, песочница, ЭТЮДЫ (все этюды), аппаратное, программное и прочее, оставив при этом обсуждение в программном, но дав права Автору на исправление релиза публикаций этюдов в топе.
... и тогда взалкавшим знаний не пришлось бы рыскать по всему форуму в поисках оных.
Вот было бы славно!!! Если бы мудрые модераторы разместили этюды уважаемого Евгения отдельным топом, наравне с общие, песочница, ЭТЮДЫ (все этюды), аппаратное, программное и прочее, оставив при этом обсуждение в программном, но дав права Автору на исправление релиза публикаций этюдов в топе.
... и тогда взалкавшим знаний не пришлось бы рыскать по всему форуму в поисках оных.
Все хотелки не умею, но Евгений дал ссылки на все свои этюды и темы в "Песочнице". Когда выйду из творческого криза, они будут рассортированы в третьем топике "Песочницы". С остальным, как решат те, у кого больше прав.)))
Здравствуйте Уважемый ЕвгенийП.
Насколько с точки зрения выделения и высвобождения памяти равнозначны конструкции :
и
или надо добавлять еще пару {}
?
не надо. s перед } сам отойдет
Нет, здесь нет разницы и дополнительная пара скобок лишняя.
Другое дело, что код сильно неидеален. С памятью то у него всё нормально в плане освобождения, но запросов слишком много. Он будет выделять и освобождать память очень много раз. Да, к тому же, он будет 1000 раз создавать совершенно не нужные объекты типа String в строках №№ 4 и 6 (по первому коду).
Давайте посмотрим что с ним не так, и что можно сделать. Итак, я включил печать оповещений о запросах/освобождениях памяти и запускаю Ваш код вот в таком виде (1000 заменил на 10, чтобы не утонуть в печати).
Выдаётся вот такой результат:
На самом деле здесь 10 (по количеству прохождений цикла) одинаковых кусков. Первый кусок - строки №№ 2-19, второй - строки №№ 20-37, и т.д.
Давайте сомтреть, что мы здесь видим.
Строки №№ 3 и 4 листинга показывают два запроса памяти. Первый под пустую строку, а второй - под строку в 9 байтов. Это лишнее. Если бы вместо
Вы написали просто
то был бы один запрос на 9 байтов. Запроса на 1 байт не было бы.
Далее. Строки листинга №№ 5,6,8 и 9 - это создание нового объекта типа String из-за того, что в строке программы №8 Вы написали String(n). Если бы Вы написали там просто n, то этих операций бы не было. Тоже самое касается строк листинга №№ 11,12,14 и 15 - они порождение совершенно ненужной конструкции String(n*1000) в строке программы №10. Там было достаточно просто n*1000.
Подведём промежуточный итог, а потом пойдём дальше Сейчас у Вас на каждый проход цикла выполняется восемь операций запроса памяти (посчитайте сами). Если исправить те две мелкие погрешности, что мы обсудили, то запросов будет всего пять на проход цикла - уже прогресс! Убедимся в этом:
Поехали дальше.
А, собственно, зачем при каждом проходе цикла перезапрашивать память? String устроена так, что она не будет запрашивать память заново, если новая строка короче существующей. Поэтому, если мы не будем удалять объект каждый раз, то она будет запрашивать только тогда, когда реально не хватает, а всё остальное время "переиспользовать" уже запрошенный кусок! Для того, чтобы объект не удалялся при каждом проходе цикла, вынесем его за цикл. А для того, чтобы он удалился после всего, заключим его вместе с циклом в фигурные скобки.
Имеем:
Результат:
Это полный результат - не сокращённый. Смотрите - она запрашивала память ТОЛЬКО при первом проходе цикла и ещё разок при втором - и всё. Все остальные проходы цикла пролетели без запросов памяти вовсе!
Т.е. мы выиграли практически во столько раз, сколько у нас проходв цикла!
Разобрались?
---------------
Ну и на десерт, маленький офф-топик.
Чего Вам всем так мёдом эта String намазана? Вот Ваш пример без String:
Нормально работает. И давайте сравним. Пример, который я назвал "промежуточным" выше занимал 3782 байта программной памяти. Этот пример занимает 1768. Т.е. отказавшись от String я сэкономил более 2000 байтов! И, кстати, выиграл по быстродействию, можете запустить и убедиться.
Ага, понятно. Благодарю Вас.
Уважаемый Евгений! Не раскрыта тема указателей! Может быть расскажете?
Там много тем не раскрыто на самом деле.
Недавно обнаружил:
Хотелось бы услышать коментарии от Гуров =)
Ключевая фраза "казалось бы". Чего вы ожидаете присваивая указателю литерал?
Хотелось бы услышать коментарии
Ну, если хотелось, так чего не задали никаких вопросов? Вам что-то непонятно или так - потрындеть захотелось?
А вообще, прежде, чем показывать такие вещи, неплохо бы запустить самому.
1. В строке №24 (в комментарии) написан бред. Там получается совсем не это;
2. Компилятор, бедняга, аж изматерился весь, предупреждая, что так делать нельзя, но нам-то пофиг, плевали мы на него! Вспоминается стишок про "не влезай - убьёт".
Ок. Вопрос. Литералы это фиксированные значения в коде программы? А так же Литералы записанные в коде программы не должны изменяться. Однако в приведенном коде, который я запускал, и знаю об предупреждениях компилятора, что там не все по фэншую. Литералы должны оставаться не изменными, но этого не происходит? Или я чего то не понимаю?
stepan_sotnikov - сравните строчки 14 и 21 в своем коде. Ничего не замечаете?
Ок. Вопрос.
Ок. Ответ.
Литералы это фиксированные значения в коде программы?
Зависит от языка программирования. Здесь строковые литералы - нет не фиксированные. Если быть более строгим, они здесь и не литералы вовсе. Кстати, в Фортране IV такие фокусы проходили и с числовыми литералами - можно было спокойно изменить литерал 2 на 28, а потом всем показывать, как 2/7 получается 4.
Литералы записанные в коде программы не должны изменяться. ... Литералы должны оставаться не изменными
Кому это они должны или не должны? Это Ваша фантазия. Мир устроен по-другому.
но этого не происходит
Так ото ж. Происходит всё так, как должно происходить в этом языке программирования, я не так, как Вам видится в Вашей картине мира.
Или я чего то не понимаю?
Таки да. Вы просто недостаточно хорошо знаете этот язык программирования.
Он пытался сказать, Вам, что мол "не стоит трахать точилку для карандашей, но ты большой мальчик и свободен в своих поступках, надеюсь, ты знаешь, что делаешь". А Вы не знали. Но, делали.
Если Вам хочется обмануть компилятор, это надо делать тоньше. Я тут на форуме выкладывал пример, где также меняется текстовая константа (причём, явно описанная как const и ни в какие функции ни разу не передаваемая) и при этом компилятор даже рта не раскрывает. Можете полюбопытствовать :)
14 строка указатель на строковый литерал, то есть по сути переменная s хранит адрес на начало массива строковой пеерменной. А вот что происходит в 21 мне не понятно. По сути я по адрессу на первую ячейку ссылки на строковую переменную запихал строковую переменную... и теперь она куда то ссылаеться...
Вот именно - "куда-то". И показывает на "что-то". У меня на одно, у вас на другое, а ЕвгенийП вообще не будет заливать это в МК. Вот вы пытались объяснить, почему пьяного мотает из стороны в сторону именно так, а не иначе?
Да, я плохо знаю этот язык и пытаюсь его понять. Спасибо за ответ "Здесь строковые литералы - нет не фиксированные. Если быть более строгим, они здесь и не литералы вовсе." Принято к сведенью.
Сбивало с толку неожиданное выполнение котроллером этого бреда. И отсутсвия отладчика.
Основная головоломка была, не в том, что адресу присваивалась литера.
Основное недопонимание у меня было именно, в том как этот язык работает с литералами.
Даже такая програма, будет каждый раз его менять. То есть char * s = "ffff*" выполняется единожды.
и в s записываеться адрес начала массива. При повторном выполнении loop, в переменную записываеться адресс массива, но уже без присвоения. Но функция prs уже его поковыряла. Если запустить подобный код на большенстве языков, то "ffff*" станит константой, и при каждом цикле s[]="ffff*" в любом случаии спасибо за помошь.