Публичная переменная в классе не увеличивается

Я не уверен, что не так с моим кодом, но всякий раз, когда я вызываю addNewLine, переменная linesInserted обновляется неправильно. Когда я печатаю значение linesInserted внутри метода addNewLine, оно отображается как увеличивающееся, но когда я печатаю значение внутри метода draw, оно показывает, что оно равно 0.

Я пробовал разные операторы инкремента (linesInserted += 1 и linesInserted = linesInserted + 1). Как ни странно, когда я делаю linesInserted = 99 вместо linesInserted++, он устанавливает значение правильно. Мне интересно, не перепутал ли я где-нибудь ссылку?

#include <Adafruit_GFX.h>
#include <Adafruit_SSD1331.h>
#include <SPI.h>

class Character {
  public:
    char** lines; // using char for 8 bit integer
    int sizeOfLines;
    char symbol;
    Adafruit_SSD1331* display;
    int width;
    draw(int x, int y, int color);
    addNewLine(char startX, char startY, char endX, char endY);
    int linesInserted = 0;
};

Character::draw(int x, int y, int color) {
  if (linesInserted < sizeOfLines) {
    display->setCursor(0, 39);
    display->setTextColor(0xF800);
    display->setTextSize(1);
    char buf[30];
    sprintf(buf, "SYMBOL %c\nWASTED MEMORY", symbol);
    Serial.println(symbol);
    Serial.print(linesInserted);
    Serial.print(" ");
    Serial.println(sizeOfLines);
    display->print(buf);
  }
  for (int i = 0; i < sizeOfLines; i++) {
    char* line = lines[i];
    display->drawLine(line[0] + x, line[1] + y, line[2] + x, line[3] + y, color);
  }
};

Character::addNewLine(char startX, char startY, char endX, char endY)  {
  linesInserted++;
  if (linesInserted > sizeOfLines) {
    display->setCursor(0, 15);
    display->setTextColor(0xF800);
    display->setTextSize(1);
    char buf[30];
    sprintf(buf, "ERROR\nSYMBOL %c\nTOO MANY LINES", symbol);
    display->print(buf);
  } else {
    lines[linesInserted - 1] = new char[4]{startX, startY, endX, endY};
  }
};

EDIT: Git repo в проект добавлен - https://github.com/Pyrodron/charger-destination-board

, 👍1

Обсуждение

Является ли char ** lines; когда-либо инициализированным?, @timemage

@timemage - Да, есть метод, который устанавливает lines в (char**) malloc(sizeof(char*) * Char.sizeOfLines);. Я нахожусь в процессе преобразования этого из "структуры" в класс, поэтому до создания конструктора еще не дошло., @Pyro

Я подозреваю, что в конце концов вы обнаружите, что это вопрос искажения памяти. Без полного кода, вероятно, нет возможности его выполнить, и у меня нет особых шансов что-нибудь найти. Как правило, чтобы иметь больше шансов получить помощь в этом, я бы сказал, предоставьте полный код для урезанного примера, который все еще показывает проблему. Если возможно, полностью удалите код отображения из примера, поскольку эта зависимость сильно ограничит набор людей, которые могут воспроизвести вашу проблему. Тем не менее, вы, скорее всего, найдете ответ в процессе этого., @timemage

Вы должны переосмыслить всю свою стратегию. Похоже, вы создаете оболочку для некоторых графических символов, состоящих из отрезков прямой линии. Было бы гораздо лучше хранить данные статически во флэш-памяти и просто предоставлять указатель на них вашему классу. Затем используйте функции pgm_read_* для доступа к данным. Нет необходимости в каком-либо динамическом распределении оперативной памяти. Нет ограничений на количество строк в символе (это число может быть статическим и частью данных символа). Не связывайся с мэллоком., @Majenko

@timemage - Я не хотел делать этот вопрос длинным, публикуя весь код, но потом понял, что Git - это вещь. Я думаю, это то, что я получаю за публикацию этого в 11 вечера, лол. Я отредактировал вопрос с добавлением репо., @Pyro

@Majenko - Если быть до конца честным, я уже думал об этом. Просто пытаюсь сделать какой-то код в качестве доказательства концепции, прежде чем перенести все на flash., @Pyro


2 ответа


Лучший ответ:

3

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

Во - первых, просто записка: Код, с которым я тестирую (потому что он компилируется),-это код из вашего репозитория github,который использовал переменную типа char для linesInserted и имел закомментированный println(linesInserted);, так что кажется, что в какой-то момент вы пытались напечатать счетчик как символ. в частности, непечатаемые управляющие символы. Так что это могло бы сбить вас с толку. Я раскомментировал его и добавил к нему параметр (DEC) (вместо приведения), чтобы принудить println рассматривать его как int.

Я протестировал ваш код без библиотеки display в среде ПК, чтобы запустить на нем valgrind. Он ничего не нашел, да я и не ожидал этого. Но я грубо обработал ваш код в этой среде, чтобы увидеть, что вы вызываете malloc()/new 124 раза, в общей сложности 500 байт, но важно отметить, что это не включает в себя накладные расходы кучи структуры, что важно, потому что вы вводитекрошечные четырехсимвольные фрагменты.

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

Основной вывод заключается в том, что вы опасно близки к тому, чтобы "исчерпать" память:

000005e0| 01 00 02 04 00 01 00 02 - 00 04 00 03 01 03 05 04 | ........ ........
000005f0| 00 01 03 02 03 04 00 01 - 06 02 06 04 00 00 00 00 | ........ ........
00000600 | 00 04 00 02  00 02 00 04 - 00 01 01 01  01 04 00 00 | ........ ........
00000610 | 02 00 02 04  00 02 02 02 - 02 04 00 01  03 01 03 04 | ........ ........
00000620 | 00 00 04 00  04 04 00 02 - 04 02 04 04  00 01 05 01 | ........ ........
00000630 | 05 04 00 00  06 00 06 04 - 00 02 06 02  06 00 00 00 | ........ ........
                                                      ^^
                                                 [__брквал]

                          [СП]
                           ВВ               
000006b0| 00 00 00 00 00 00 в7 09 - 03 01 01 06 02 а5 02 00 | ........ ........
000006c0| 00 00 00 01 00 57 05 06 - cc 00 03 8f 0f 01 00 00|..... W.. ........
000006d0| 00 01 00 00 03 00 03 00 - 00 00 07 02 а4 00 01 00 | ........ ........
000006e0 | 03 02 a4 00 05 f8 0d 06 - f6 00 03 00 00 08 c3 08 | ........ ........
000006f0 | de 00 01 00 01 80 1a ed - 02 04 00 41 а4 02 04 00 | ........ ...А....

Расстояние между краем кучи и указателем стека составляет 120 байт. Реализация malloc, которую я запустил, проверяет, находится ли указатель стека в пределах 128 (__malloc_margin) байтов. Поскольку ваш код строился в моей среде, без встречной печати, это удалось не-не выделить. Однако добавления одного дополнительного вызова addNewLine было достаточно, чтобы вызвать сбой malloc() через new. Так что вы действительно находитесь на самом краю пропасти. И у вас нет проверок, чтобы увидеть, возвращает ли malloc или new nullptr.

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

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

lines[linesInserted - 1] = new char[4]{startX, startY, endX, endY};

Сгенерированный код выглядит следующим образом:

0x24c6 [addNewLine+244] ldi r25, 0x00 ; 0
0x24c8 [addNewLine+246] вызов 0x3830 ; 0x3830 [malloc]
0x24cc [addNewLine+250] movw r30, r24
0x24ce [addNewLine+252] st Z, r10
0x24d0 [addNewLine+254] std Z+1, r11 ; 0x01
0x24d2 [addNewLine+256] std Z+2, r17 ; 0x02
0x24d4 [addNewLine+258] std Z+3, r16 ; 0x03

Итак, если malloc() потерпит неудачу, очевидно, новый оператор просто предположит, что это не так, и пойдет дальше и все равно запишет в память 0x0000 (nullptr), тьфу. Я думаю, это потому, что технически вы вызываете обычный new, который должен вызывать исключение при сбое. Но нет никаких исключений из того, как avr-g++ настроен для Arduino. Поскольку он существует в ArduinoCore-avr версии 1.8.3, новый код просто возвращает то, что возвращает malloc, и компилятор считает, что в случае сбоя будет выполняться исключение. Обычно вы вызываете std::nothrow версию new [], которая возвращает nullptr при сбое, как в:

lines[linesInserted - 1] = new(std::nothrow) char[4]{startX, startY, endX, endY};
// check lines[linesInserted - 1] against nullptr here

Тем не менее, в 1.8.3 нет возможности сделать это. Мастер на момент написания статьи, похоже, решил этупроблему, но это еще не доставка.


Короткий ответ на этот вопрос в основном таков: не делай этого.

  • По возможности избегайте динамического распределения вообще.

  • Об этом упоминалось в комментариях, но стоит повторить, используйте flash там, где можете. Помимо получения ваших символьных данных в PROGMEM, у вас также есть некоторые строковые литералы, которые можно переместить во flash с помощью F().

  • В той мере, в какой вы выполняете динамические запросы на распределение:

    • Проверьте, действительно ли они преуспевают

    • Делайте как можно меньше запросов.

    • Сделайте это заранее в начале кода. "Авансовая" часть, которую вы более или менее уже делаете, так что это очень хорошо.

    • Похоже, что если вы собираетесь динамически выделять средства в основной версии 1.8.3 и вам вообще грозит сбой выделения, важно, чтобы вы не использовали new, так как это может привести к перезаписи памяти для части инициализации/построения после сбоя самого выделения. С другой стороны, серьезно подумайте о том, чтобы изменить ядро вашего Arduino. new.cpp файл, чтобы сделать что-то вроде вызова abort (), если malloc() возвращает nullptr, как минимум, если не что-то более разумное. В противном случае это может свести вас с ума.

,

Вероятно, мне следовало бы указать, что регистровый файл - это память, отображенная в начало пространства данных. Итак, когда malloc() терпит неудачу под new char[4]{...} более непосредственный эффект от того, что он продолжает пытаться инициализировать, заключается в том, что он портит регистры общего назначения, которые будут делать кто-знает-что оттуда; ничего хорошего или ужасно предсказуемого., @timemage

Я и не подозревал, что под ним "новый" называется мэллок. Честно говоря, это многое объяснило бы. Я всегда забываю, что у вас не так много памяти для работы на Arduino, поэтому я думаю, что просто перейду к использованию flash или SD-карты для хранения информации для символов на этом этапе. Похоже, в любом случае это будет меньше головной боли., @Pyro

Да, новый (за исключением версии "размещение") - это динамическое распределение в любом случае. То, что он использует "malloc", является деталью реализации; это не должно быть сделано строго таким образом. Однако обычно именно это и происходит. До сих пор не похоже, что вам нужна SD-карта; просто посмотрите на использование "PROGMEM" и остальной части pgmspace.h. Поначалу это может вас немного сбить с толку., @timemage

Перенос всех графических данных на flash очень помог! Я больше не сталкиваюсь с проблемами. Потребовалось немного привыкнуть к `ПРОГМЕМУ", но оно определенно того стоило., @Pyro


0

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

Читая его, я вижу 2 недостатка в управлении памятью. Этот блок использует размер указателя char вместо символа. Я не понимаю, что вы хотите, чтобы этот код делал.

(char**) malloc(sizeof(char*) * Char.sizeOfLines);

Попробуйте что-то более похожее:

(char**) malloc(sizeof(char) * <numberOfCharsPerLine> * Char.sizeOfLines);

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

lines[linesInserted - 1] = new char[4]{startX, startY, endX, endY};

Вместо этого попробуйте что-то вроде:

lines[linesInserted - 1][0] = startX;
lines[linesInserted - 1][1] = startY;
...
,

что плохого в присвоении массивов элементам в массиве указателей?, @Juraj

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

вы говорите, что это указатели на то, что выделяет OP, а затем показываете, как выделить его сразу. или я неправильно читаю?, @Juraj