delete[] вылетает ESP32 (ошибка Bad tail)

Поскольку я хотел поддерживать платы на базе ESP32 и AVR, мне пришлось перейти от таких вещей, как std::string, к альтернативному методу. Я хотел избежать класса String, потому что я слышал, что это немного беспорядочно с точки зрения памяти. Я решил использовать массивы char и строки с нулевым окончанием и сам освободить их все.

Из моего приложения по Bluetooth я получаю данные в формате <id>:<data>, где id - это двузначное число (т.Е. 01). Как только я получаю данные, я передаю их этой функции, которая выполняет некоторую обработку:

void CDataUtils::process(char *data) {
    char *split = strtok(data, ":");
    m_id = static_cast<ActionID>(atoi(split));

    uint16_t len = strlen(data) - (strlen(split) + 1);
    char *d = new char[len + 1];

    strcpy(d, split + strlen(split) + 1);

    delete[] split;
    //......
    delete[] d;
}

Я разделяю строку с помощью strtokи использую обе стороны, поэтому храню ее в переменной split. Я беру первый раздел, который является идентификатором, и бросаю его в перечисление, но это не имеет значения для этого. Затем я вычисляю длину данных после двоеточия и создаю массив char такого размера (плюс 1).

Затем я копирую данные, которые идут после двоеточия, в d. Как только это будет сделано, поскольку мне больше не нужен сплит, я освобождаю его с помощью delete[]. Однако это приводит к сбою моего ESP

CORRUPT HEAP: Bad tail at 0x3ffdc795. Expected 0xbaad5678 got 0xbaad5600
abort() was called at PC 0x40081d91 on core 1

ELF file SHA256: 0000000000000000

Backtrace: 0x4008e254:0x3ffc71f0 0x4008e4d1:0x3ffc7210 0x40081d91:0x3ffc7230 0x40081ebd:0x3ffc7260 0x400dd653:0x3ffc7280 0x400d8c65:0x3ffc7540 0x400d8bf4:0x3ffc7590 0x400931b5:0x3ffc75c0 0x400817f6:0x3ffc75e0 0x40081cd5:0x3ffc7600 0x4000bec7:0x3ffc7620 0x40104059:0x3ffc7640 0x400d1fa7:0x3ffc7660 0x400d1e9c:0x3ffc7680 0x400d1b0e:0x3ffc76a0 0x400d32e5:0x3ffc76c0 0x4008ffda:0x3ffc76e0

Согласно первой ссылке при поиске ПОВРЕЖДЕННОЙ КУЧИ: Плохой хвост, это обычно признак записи данных за пределы буфера. Конечно, если бы это было так, то ошибка произошла бы в строке strcpy, а не в строке delete? Что меня смущает, так это то, что у меня есть эта маленькая служебная функция, которая удаляет и перераспределяет новый массив:

char *_realloc_and_null_array(char *arr, uint16_t size)
{
    delete[] arr;         //Свободный массив
    arr = new char[size]; //Создать новый символ заданной длины

    memset(arr, 0x00, size); // Заполнить нулем

    return arr;
}

Раньше я использовал это в функции process выше, и я не получил никаких ошибок с ней, даже если я освобождаю массив с помощью delete[].

Что я сделал не так?

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

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

В терминах функции _realloc_and_null_array это было чисто для удаления дублирующегося кода. Я получил фрагмент из сообщения SO, но не могу вспомнить его. Когда я создаю программу (на любом языке) Мне не нравится просто волей-неволей создавать переменные. В этом случае, если у меня есть массив d, я могу использовать его для чего-то, освободить его, создать новый и заполнить другими данными. Это избавляет от необходимости иметь еще один ненужный массив. Я часто повторно использую массивы, так что это позволяет мне просто создать новый массив другой длины и избавиться от старого (я не делаю этого глупо, когда программу трудно понять. Я буду использовать эту функцию максимум один раз в некоторых функциях).

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

, 👍1

Обсуждение

Строка плоха, потому что она использует динамическое распределение, как и вы. Это приводит к фрагментации памяти. А в тех случаях, когда этого не происходит, вы просто можете использовать статическое распределение, не теряя функциональности. Хотя мне немного непонятно, почему вы пытаетесь удалить "split" вместо "data" (поскольку "split" будет просто указателем на начало первого токена внутри "data", который является только началом "data). Или вы не собираетесь удалять данные`?, @chrisl

И какова именно цель функции _realloc_and_null_array()? Почему вы хотите сначала удалить массив, а затем воссоздать его снова, когда вы все равно заполняете память нулями? Какую проблему вы пытаетесь решить с помощью этого?, @chrisl

strtok возвращает указатель на первый токен в списке токенов, разделенных символом ':'. Он не создает новый массив. Нет необходимости удалять массив, на который указывает split. split" указывает только на массив данных, а не на его собственный массив. Но strtok разрушителен. Он изменяет содержимое строки данных, записывая \0 вместо ':' в вашем случае. Вы можете вызвать d = strtok(NULL, ":")` во второй раз, и вы получите следующий токен. Нет необходимости вычислять длину или d таким сложным способом., @Peter Paul Kiefer

https://majenko.co.uk/blog/splitting-text-c, @Majenko

Это не имеет ничего общего с самой проблемой, но вы можете new char[size](), а не new char [size], за которым следует ваш memset до `\ 0". Добавление круглой скобки позаботится об этом за вас. Тем не менее, большая часть того, почему "strtok" разрушителен для его ввода, так что вам не обязательно даже копировать данные, не говоря уже о динамическом выделении для их обработки; часто вы этого не делаете., @timemage

Я отредактировал свой пост, чтобы ответить на комментарии, потому что их довольно много., @DreamingInsanity

вы должны изучить некоторые основы памяти, указателей, стека и кучи, @Juraj

вы можете "исправить" входную строку strtok после ее запуска, если вы знаете исходный размер строки, просто измените все, кроме последнего '\ 0', обратно на разделитель. Вероятно, вам следует просто создать один глобальный массив, например: char buffer[99], где 99 больше, чем вам когда-либо понадобится. Вы можете повторно использовать его в течение нескольких месяцев, не пропуская ни байта., @dandavis

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

@dandavis Мне нравится идея постоянных массивов. Я буду использовать это для большинства частей, за исключением одного бита, где я действительно не знаю длину строки. Причина, по которой я не делал этого раньше, заключалась в том, что я стараюсь избегать жесткого кодирования значений в основном из-за "что, если". Теоретически "x" - это максимальная длина, но * что, если * это не так., @DreamingInsanity


1 ответ


1

Итак, у вас есть некоторые заблуждения:

  • strtok() не создаст новый массив c-string/character . Он возвращает указатель, который указывает на место в данных, где начинается ваш текущий токен. Так что dong delete[] on split не имеет особого смысла. Также обратите внимание, что strtok() изменит c-строку, которую вы ему дадите. Таким образом, данные будут изменены после выполнения функции. strtok() вставляет нулевой символ \0 на место разделителя (: в этом случае. Таким образом, функции c-string могут быть снабжены указателем, который возвращает strtok(), и будут считываться только до конца токена (поскольку они выглядят как нулевой символ как конец строки).

  • Класс String плох на Arduino (также на ESP, но не в той же срочности), потому что у них очень мало памяти для переменных. Класс String выделяет свою память динамически каждый раз, когда вы объединяете, назначаете или изменяете размер строки. Это означает, что он довольно часто создает и уничтожает массивы разного размера, что приведет к фрагментации памяти. Вы получите небольшие дыры в своей памяти, которые слишком малы для новых выделений. Со временем вы будете получать все больше и больше дыр, пока ваша память не заполнится. Затем Arduino выходит из строя. Поскольку ардуино часто имеют очень мало памяти (и не имеют ОС, которая управляет оперативной памятью), это происходит довольно быстро. У ESP больше памяти, так что это происходит не так быстро, но проблема все та же.

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

  • В этом случае, если у меня есть массив d, я могу использовать его для чего-то, освободить его, создать новый и заполнить другими данными. Это избавляет от необходимости иметь еще один ненужный массив.

    На самом деле: Нет, не совсем. Массив d создается где-то в начале функции process() и удаляется в конце ее. Точно так же, как локальная переменная (статически выделенная). Единственное преимущество здесь заключается в том, что d может иметь переменный размер (не знаю во время компиляции). Хотя это также может создать проблемы с динамическим распределением, как описано выше (зависит от того, выполняете ли вы динамическое распределение между выделением и удалением).

  • Функция _realloc_and_null_array() имеет смысл только тогда, когда размер отличается от размера предоставленного массива. Но опять же вы можете столкнуться с проблемой, описанной выше.

  • Мне не нравится просто волей-неволей создавать переменные.

    Что вы имеете в виду под "волей-неволей"? Вы создаете только те переменные, которые вам действительно нужны. Но это не значит, что эти переменные не могут быть статически распределены. Распространенное заблуждение, с которым часто сталкиваются программисты с более крупных платформ (например, ПК), заключается в том, что вы должны держать объем памяти как можно ниже, таким образом используя динамическое распределение свободных ресурсов, которые в настоящее время не нужны. На ПК освобожденная память может быть использована другими программами, запущенными на нем. Но на Arduino работает только одна программа. Память, которую вы не используете, будет просто ничего не делать. Таким образом, сохранение объема памяти важно, когда у вас действительно не хватает памяти для вашей программы, но не важно, когда у вас все еще достаточно памяти. Поскольку динамическое распределение памяти на этих платформах сопряжено с большими оговорками, оно того не стоит.

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

,

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

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

В сторону: "Строки не всегда вызывают самопроизвольное возгорание программы. Сами по себе несколько строковых имен файлов, SSID или веб-ответов ничему не повредят. В основном, только когда вы обрабатываете данные (split, concat, substring) с помощью строковых методов, вы сталкиваетесь с проблемами., @dandavis

Я мог бы определенно ограничить количество операций, которые мне нужно выполнять со строками, изменив данные, которые я отправляю из своего приложения, более удобным способом. Худший из них - градиенты, в которых я просто отправляю необработанную строку CSS, поэтому для этого требуется пара совпадений регулярных выражений, несколько расщеплений и так далее, поэтому я избегал "Строки" из-за этого., @DreamingInsanity