Так, ну поехали, начнём с самой вкусной ошибки, потом поговорим о других, а на закуску, поговорим о том, что expand
вообще делается не так, его надо просто выбросить и написать по-другому. Я буду подводить постепенно, чтобы Вы сами догадались в чём беда.
Самая вкусная ошибка
Чтобы упростить код, я написал точно такой же (по сути) expand
как у Вас, только массив у меня int
и всё лишнее (не имеющее отношение к ошибке) выбросил. Все примеры полные, запускайте, любуйтесь.
В примере есть класс Kaka
, в нём массив arr
, который изначально запрашивается размером в INI_SIZE
и заполняется числами от 0 до INI_SIZE - 1
.
Также в классе есть метод expand
- идейно копия Вашего (убедитесь). Он увеличивает массив на 1 элемент и все старые элементы копирует в новый массив.
Наконец, в классе есть метод printArr
, который печатает первые INI_SIZE
элементов массива arr
в одну строку.
В основной программе создаётся экземпляр класса, печатается массив arr
, вызывается expand и снова печатается массив arr
для проверки как всё прошло.
Вот пример, убедитесь, что всё так и есть, потом запустите его и посмотрите на результат:
Код примера
struct Kaka {
int * arr;
int len;
static constexpr uint8_t INI_SIZE = 10;
// Изначально запросим массив размером INI_SIZE и
// заполним его разумными значениями
Kaka(void) {
len = INI_SIZE;
arr = new int[len];
for (int i = 0; i < len; arr[i] = i, i++);
}
//
// expand покороче, но идейно - точно как Ваш, сверьте
void expand(int * p) {
int * tmp = new int[len+1]; // запрос временного массива
for (int i = 0; i < len; tmp[i] = p[i], i++); // копирование во временный массив
p = (int *) realloc(p, sizeof(int) * (len + 1)); // перезапрос основного массива
for (int i = 0; i < len; p[i] = tmp[i], i++); // копирование из временного в новый основной
len ++;
delete [] tmp;
}
// Печатаем только первые INI_SIZE элементов arr
// т.к. в добавляемый элемент мы всё равно ничего не записывали, чего грязь печатать.
void printArr(void) {
for (int i = 0; i < INI_SIZE; i++) {
Serial.print(arr[i]);
Serial.print(i == (INI_SIZE - 1) ? "\r\n" : ", ");
}
}
};
void setup(void) {
Serial.begin(9600);
static Kaka kaka;
kaka.printArr();
kaka.expand(kaka.arr);
kaka.printArr();
}
void loop(void) {}
Результат выполнения
0, 1, 2, 3, 4, 5, 6, 7, 8, 9
0, 1, 2, 3, 4, 5, 6, 7, 8, 9
Ну, чё? Ура! Всё работает! Здорово!
Пишем дальше программу. Понадобилось нам тут строку завести и напечатать. Добавляем в наш пример между строками №40 и №41 создание текстовой строки и её печать:
Вставить после строки №40 предыдущего примера
String str = "Everything is fine!";
Serial.println(str);
Результат выполнения
0, 1, 2, 3, 4, 5, 6, 7, 8, 9
Everything is fine!
0, 1, 2, 3, 4, 5, 6, 7, 8, 9
Ну, ничего удивительного, всё работает как надо.
Тут нам приспичило изменить текст. Ну, что что надо, так изменим. В строке №41 (по последнему тексту заменяем
String str = "Everything is fine!";
на
String str = "I am a genius! My software is bugs free! No one can do it as fine as I do!";
запускаем и видим, что всё так и есть! Я - гений!
Результат выполнения
0, 1, 2, 3, 4, 5, 6, 7, 8, 9
I am genius! My software is bugs free! No one can do it as fine as I do!
0, 1, 2, 3, 4, 5, 6, 7, 8, 9
Ну, а собственно с чего бы её и не работать?
Прошло ещё какое-то время и мен опять приспичило поменять текст. На этот раз вместо строки №41 надо вписать
String str = "What the fucking glitches in there???";
(я приведу полный текст, чтобы впредь на него ссылаться, но изменеа в нём только строка №41)
Полный текст примера 'ГЛЮК'
struct Kaka {
int * arr;
int len;
static constexpr uint8_t INI_SIZE = 10;
// Изначально запросим массив размером INI_SIZE и
// заполним его разумными значениями
Kaka(void) {
len = INI_SIZE;
arr = new int[len];
for (int i = 0; i < len; arr[i] = i, i++);
}
//
// expand покороче, но идейно - точно как Ваш, сверьте
void expand(int * p) {
int * tmp = new int[len+1]; // запрос временного массива
for (int i = 0; i < len; tmp[i] = p[i], i++); // копирование во временный массив
p = (int *) realloc(p, sizeof(int) * (len + 1)); // перезапрос основного массива
for (int i = 0; i < len; p[i] = tmp[i], i++); // копирование из временного в новый основной
len ++;
delete [] tmp;
}
// Печатаем только первые INI_SIZE элементов arr
// т.к. в добавляемый элемент мы всё равно ничего не записывали, чего грязь печатать.
void printArr(void) {
for (int i = 0; i < INI_SIZE; i++) {
Serial.print(arr[i]);
Serial.print(i == (INI_SIZE - 1) ? "\r\n" : ", ");
}
}
};
void setup(void) {
Serial.begin(9600);
static Kaka kaka;
kaka.printArr();
kaka.expand(kaka.arr);
String str = "What the fucking glitches in there???";
Serial.println(str);
kaka.printArr();
}
void loop(void) {}
Запускаем и с удивлением видим:
Результат выполнения
0, 1, 2, 3, 4, 5, 6, 7, 8, 9
What the fucking glitches in there???
0, 1, 38, 26711, 29793, 29728, 25960, 26144, 25461, 26987
Это вообще, чё щас было? Каким образом эта грёбанная строка могла поменять массив?
Если бы я этот пример запустил после первой строки (“Everything is fine!”), то можно было бы предположить, что более длинная строка переполнила память, но ведь в предыдущем примере строка была ещё более длинной! И всё работало!
Более того, когда я случайно изменил изначальный размер массива (в примере “ГЛЮК” в строке 4 поменял 10 на 5, то … сами смотрите:
Результат выполнения
0, 1, 2, 3, 4
What the fucking glitches in there???
0, 1, 2, 3, 4
Вернул обратно 10 и …
Опять 25 :(
0, 1, 2, 3, 4, 5, 6, 7, 8, 9
What the fucking glitches in there???
0, 1, 38, 26711, 29793, 29728, 25960, 26144, 25461, 26987
Хрень какая-то! Получается, что эта грёбанная строка не просто изменяет постронний массив, да ещё и делает это избирательно, то меняет, то меняет в зависимости от длины строки и длины самого массива! Всё зависит от каких-то непонятных свойств строки!
Но ведь не может эта строка менять массив! Этого не может быть! Вернее, не должно быть! Похоже, объяснение одно (вернее, два): ардуина китайская, а компилятор опенсорсный. Кругом одно дерьмо, тока я посреди него весь в белом
Пойду на форум в жилетку плакаться.
Так, @Alexey_Rem если Вы уже догадались в чём задница, отлично. Если нет, почитайте тему про память, прежде, чем читать объяснения ниже. Возможно, после прочтения той темы, Вам уже никакие объяснения не понадобятся.
Wo ist der hund begraben?
Ну, что давайте начнём с того, что посмотрим вот на такой
Крохотный пример
void kaka(int n) {
n = n * 2;
Serial.print("Inside 'kaka': ");
Serial.println(n);
}
void setup(void) {
Serial.begin(9600);
int iN = 11;
Serial.print("Before 'kaka': ");
Serial.println(iN);
kaka(iN);
Serial.print("After 'kaka': ");
Serial.println(iN);
}
void loop(void) {}
и его
Результат выполнения
Before 'kaka': 11
Inside 'kaka': 22
After 'kaka': 11
Получается, переменная iN
в функции setup
не изменилась, хотя мы её передавали в функцию kaka
и там меняли. Более того, мы убедились в том, что там она изменилась! Вас это не смущает?
Видимо, нет. И правда, с чего бы её меняться? Мы же передали её по значению, т.е. в функции kaka
была совершенно другая переменная, изначально равная нашей iN
. И все изменения этой другой переменной внутри функции kaka
, там и останутся. На нашу iN
они никак не влияют!
Ну, тогда вернёмся к примеру “ГЛЮК”.
Что мы там делаем? Мы функции expand
передаём указатель. Этот указатель внутри той функции изменяем и … и ничего. Внутри функции expand
совершенно другой указатель, его можно хоть заменяться - на тот указатель, что мы передали это никак не отразится! Т.е. наши изменения никак не затронули внешнюю, по отношению к функции expand
, переменную arr
. Никак! arr
каким был, таким и остался. Меняли мы его локальную копию внутри функции expand
. А сама переменная arr
по-прежнему указывает на наш старый массив, а вовсе не на новый, который мы создали в expand
.
Но и это не всё! Как говаривал М.С. Горбачёв, «я вам больше скажу», мы ведь в expand
не только меняли локальную копию arr
! Мы ведь ещё и освободили ту память, на которую наш arr
указывал! Т.е. после вызова expand
наш arr
численно не изменился и указывает на кусок свободной памяти в которой когда-то, до вызова expand
, был массив! Но память эта уже свободна!
Вот теперь мы готовы понять почему “то меняется, то не меняется”! Дело в том, что пока этой свободной памятью никто не воспользовался в ней лежит то, что и лежало и нам кажется, что всё прекрасно! Но как только эту память хапнула под себя строка, мы тут же увидели, что там что-то поменялось!
Получается, что программа наша всегда работала неправильно, просто пока памятью никто не воспользовался, мы этого не замечали, т.к. всё выглядело прилично. А как только эта память кому-то понадобилась, сразу начали “глюки”.
Теперь понятно, что происходит и откуда глюки взялись? Еще раз, работало неправильно всегда, просто не всегда это было заметно!
Как с этим бороться?
Ну, я с ходу вижу три способа.
Способ №1 (идейно-правильный) - не менять пытаться параметр функции, а возвращать новое значение. Смотрите, здесь наша функция уже не void
, она возвращает локальную копию указателя, а там мы его присваиваем arr
.
Исправляем пример 'ГЛЮК'. Способ №1
struct Kaka {
int * arr;
int len;
static constexpr uint8_t INI_SIZE = 10;
// Изначально запросим массив размером INI_SIZE и
// заполним его разумными значениями
Kaka(void) {
len = INI_SIZE;
arr = new int[len];
for (int i = 0; i < len; arr[i] = i, i++);
}
//
// expand покороче, но идейно - точно как Ваш, сверьте
int * expand(int * p) {
int * tmp = new int[len+1]; // запрос временного массива
for (int i = 0; i < len; tmp[i] = p[i], i++); // копирование во временный массив
p = (int *) realloc(p, sizeof(int) * (len + 1)); // перезапрос основного массива
for (int i = 0; i < len; p[i] = tmp[i], i++); // копирование из временного в новый основной
len ++;
delete [] tmp;
return p;
}
// Печатаем только первые INI_SIZE элементов arr
// т.к. в добавляемый элемент мы всё равно ничего не записывали, чего грязь печатать.
void printArr(void) {
for (int i = 0; i < INI_SIZE; i++) {
Serial.print(arr[i]);
Serial.print(i == (INI_SIZE - 1) ? "\r\n" : ", ");
}
}
};
void setup(void) {
Serial.begin(9600);
static Kaka kaka;
kaka.printArr();
kaka.arr = kaka.expand(kaka.arr);
String str = "What the fucking glitches in there???";
Serial.println(str);
kaka.printArr();
}
void loop(void) {}
Результат выполнения
0, 1, 2, 3, 4, 5, 6, 7, 8, 9
What the fucking glitches in there???
0, 1, 2, 3, 4, 5, 6, 7, 8, 9
но если нам таки очень хочется изменять параметр и оставить функцию expand
void, то можно передавать параметром не сам указатель, а ссылку или указатель на него. В первом случае в пример ‘ГЛЮК’ придётся изменить (добавить) всего одну (!!!) букву!
Вот смотрите - это точно тот же пример ‘ГЛЮК’ в котором в строку №16 добавили всего один символ &
- и всё заработало!
Исправляем пример 'ГЛЮК'. Способ №2
struct Kaka {
int * arr;
int len;
static constexpr uint8_t INI_SIZE = 10;
// Изначально запросим массив размером INI_SIZE и
// заполним его разумными значениями
Kaka(void) {
len = INI_SIZE;
arr = new int[len];
for (int i = 0; i < len; arr[i] = i, i++);
}
//
// expand покороче, но идейно - точно как Ваш, сверьте
void expand(int * & p) {
int * tmp = new int[len+1]; // запрос временного массива
for (int i = 0; i < len; tmp[i] = p[i], i++); // копирование во временный массив
p = (int *) realloc(p, sizeof(int) * (len + 1)); // перезапрос основного массива
for (int i = 0; i < len; p[i] = tmp[i], i++); // копирование из временного в новый основной
len ++;
delete [] tmp;
}
// Печатаем только первые INI_SIZE элементов arr
// т.к. в добавляемый элемент мы всё равно ничего не записывали, чего грязь печатать.
void printArr(void) {
for (int i = 0; i < INI_SIZE; i++) {
Serial.print(arr[i]);
Serial.print(i == (INI_SIZE - 1) ? "\r\n" : ", ");
}
}
};
void setup(void) {
Serial.begin(9600);
static Kaka kaka;
kaka.printArr();
kaka.expand(kaka.arr);
String str = "What the fucking glitches in there???";
Serial.println(str);
kaka.printArr();
}
void loop(void) {}
Результат выполнения
0, 1, 2, 3, 4, 5, 6, 7, 8, 9
What the fucking glitches in there???
0, 1, 2, 3, 4, 5, 6, 7, 8, 9
ну, и, наконец, можно передавать не ссылку, а указатель (получается “указатель на указатель”). Смотрите на технику этого дела:
Исправляем пример 'ГЛЮК'. Способ №3
struct Kaka {
int * arr;
int len;
static constexpr uint8_t INI_SIZE = 10;
// Изначально запросим массив размером INI_SIZE и
// заполним его разумными значениями
Kaka(void) {
len = INI_SIZE;
arr = new int[len];
for (int i = 0; i < len; arr[i] = i, i++);
}
//
// expand покороче, но идейно - точно как Ваш, сверьте
void expand(int * * p) {
int * tmp = new int[len+1]; // запрос временного массива
for (int i = 0; i < len; tmp[i] = *p[i], i++); // копирование во временный массив
*p = (int *) realloc(*p, sizeof(int) * (len + 1)); // перезапрос основного массива
for (int i = 0; i < len; *p[i] = tmp[i], i++); // копирование из временного в новый основной
len ++;
delete [] tmp;
}
// Печатаем только первые INI_SIZE элементов arr
// т.к. в добавляемый элемент мы всё равно ничего не записывали, чего грязь печатать.
void printArr(void) {
for (int i = 0; i < INI_SIZE; i++) {
Serial.print(arr[i]);
Serial.print(i == (INI_SIZE - 1) ? "\r\n" : ", ");
}
}
};
void setup(void) {
Serial.begin(9600);
static Kaka kaka;
kaka.printArr();
kaka.expand(& kaka.arr);
String str = "What the fucking glitches in there???";
Serial.println(str);
kaka.printArr();
}
void loop(void) {}
Результат выполнения
0, 1, 2, 3, 4, 5, 6, 7, 8, 9
What the fucking glitches in there???
0, 1, 2, 3, 4, 5, 6, 7, 8, 9
Ну, вот как-то так. Мы избавились от самой вкусной ошибки.
Другие ошибки
они не так интересны. Я коротко перечислю их по коду из самого первого сообщения
- в строке №3 забыли проверить выделилась ли память;
- в строке №9 не проверили реально ил получен новый кусок памяти или просто изменён размер существующего. В последнем случае не нужно копировать содержимое массива, оно и так на месте. Да и старый освобождать нельзя, т.к. Вы его же ещё используете.
Как надо было делать
Во-первых, стоило до последнего избегать расширения массива. Лучше сразу запросить на максимальный размер, будет гораздо экономичнее.
Но, если уж нужно менять размер, то нужно это делать так:
- сохранить указатель на старый массив в локальной переменной;
- вызвать
realloc
- если возвращён тот же самый кусок памяти, то ничего больше не делать;
- и только если возвращён новый кусок - скопировать старый массив в новый один раз (а не два копирования, как сейчас) и после этого освободить память старого массива.