И снова об указателях

Ну, тогда на здоровье.

1 лайк

МММ посоветовал включить предупреждения.
Какая-то инвалидная конверсия. Как этопонимать? Ведь работает. Контрольные примеры проходят.

C:\#ARDINO\LINK\Eprm3\Eprm3.ino: In function 'void to_eeprom(void*, int, int)':
C:\#ARDINO\LINK\Eprm3\Eprm3.ino:27:15: warning: invalid conversion from 'void*' to 'byte* {aka unsigned char*}' [-fpermissive]
   byte* ptr = x;
               ^
C:\#ARDINO\LINK\Eprm3\Eprm3.ino: In function 'void from_eeprom(void*, int, int)':
C:\#ARDINO\LINK\Eprm3\Eprm3.ino:38:15: warning: invalid conversion from 'void*' to 'byte* {aka unsigned char*}' [-fpermissive]
   byte* ptr = x;
               ^

ptr у вас byte*, а x - void*. И вы присваиваете одно другому

1 лайк

Потому что каждый указатель имеет свой тип. Указатель на long нельзя просто так присвоить указателю на float, надо сначала привести типы

неправильно:

long x =2;
float* f = &x;

правильно:

long x =2;
float* f = (float *)&x;

Этим вы показываете компилятору, что присваиваете разные типы не случайно, а знаете, что делаете.

хотя по новым стандартам и такой кастинг вроде тоже ошибка Ж)

1 лайк

Вот так выходит без предупреждений.

byte* ptr = (byte*) x;

И работает!

Если вместо этого

написать так:

количество “подводных камней” немного уменьшится.

1 лайк

Боятся надо не только “подводных камней”, но … “небесных камней”/метеоритов. Они обычно прилетают после обновлений.

1 лайк

А тут ещё соседи пугают. Говорят, что по городу ходят сиреноголовый и человек-унитаз.

Позвольте вернуться к началу темы. Там (в самом первом сообщении) был код, который мне сильно не нравится, хочется обсудить.

Для начала я поправил то, что здесь уже обсуждалось (преобразование типов указателей и sizeof, а также переформатировал код под свои привычки. Получилась вот такая копия кода ТС (функционально, ничего не менялось)

Изначальный код (№1)
void peregon(void *x, void *y, int z) {
	byte* ptr1 = reinterpret_cast<byte *>(x);
	byte* ptr2 = reinterpret_cast<byte *>(y);
	//
	for (int i = 0; i <= z - 1; i++) {
		*(ptr2 + i) = *(ptr1 + i);
	}
}

void setup(void) {
	float istok1 = 3.1415;
	float priem1;

	int istok2 = 12345;
	int priem2;

	peregon(& istok1, & priem1, sizeof(priem1)); // istok1 копируется в priem1 (float)
	peregon(& istok2, & priem2, sizeof(priem2)); // istok2 копируется в priem2 (int)

	Serial.begin(9600);
	Serial.println(String(priem1,4));
	Serial.println(priem2);
}

void loop(void) {}
Сообщения компилятора
Скетч использует 4462 байт (13%) памяти устройства. Всего доступно 32256 байт.
Глобальные переменные используют 198 байт (9%) динамической памяти, оставляя 1850 байт для локальных переменных. Максимум: 2048 байт.
Результат работы примера
3.1415
12345

Теперь я задался вопросом, а что делает String в строке №21? Зачем оно там? Ведь если его убрать, ничего не изменится. Убрал:

Код без String (№2)
void peregon(void *x, void *y, int z) {
	byte* ptr1 = reinterpret_cast<byte *>(x);
	byte* ptr2 = reinterpret_cast<byte *>(y);
	//
	for (int i = 0; i <= z - 1; i++) {
		*(ptr2 + i) = *(ptr1 + i);
	}
}

void setup(void) {
	float istok1 = 3.1415;
	float priem1;

	int istok2 = 12345;
	int priem2;

	peregon(& istok1, & priem1, sizeof(priem1)); // istok1 копируется в priem1 (float)
	peregon(& istok2, & priem2, sizeof(priem2)); // istok2 копируется в priem2 (int)

	Serial.begin(9600);
	Serial.println(priem1,4);
	Serial.println(priem2);
}

void loop(void) {}
Сообщения компилятора
Скетч использует 3018 байт (9%) памяти устройства. Всего доступно 32256 байт.
Глобальные переменные используют 200 байт (9%) динамической памяти, оставляя 1848 байт для локальных переменных. Максимум: 2048 байт.
Результат работы примера
3.1415
12345

Действительно, ничего не изменилось, за исключением того, что мы сэкономили 1444 байта памяти кода и “якобы потеряли” 2 байта памяти данных. “Якобы” потому, что на самом деле мы сэкономили, т.к. String запрашивает память динамически и это в наших результатах не учитывалось.

В общем 1444 байта сэкономили, но это, на самом деле, не то, о чём я хотел поговорить.

Давайте посмотрим на функцию

void peregon(void *x, void *y, int z)

У неё два параметра одинакового типа x и y. @Optron, вот скажите честно, Вы всегда сможете вспомнить, кто из них “источник”, а кто – “приёмник”?

Давайте сделаем с этими параметрами две вещи. Во-первых, дадим им нормальные имена (скажем source и destination), а, во-вторых, тому из них, который “источник” (который мы не собираемся изменять) добавим модификатор const. Заодно, дадим "читабельное имя третьему параметру и, поскольку мы его тоже менять не собираемся, также скажем об этом компилятору:

Получилась вот такая функция:

void peregon(const void * source, void * destintion, const int size) {
	const byte * ptr1 = reinterpret_cast<const byte *>(source);
	byte * ptr2 = reinterpret_cast<byte *>(destintion);
	//
	for (int i = 0; i <= size - 1; i++) {
		*(ptr2 + i) = *(ptr1 + i);
	}
}

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

Всё, что напишу дальше никак не повлияет на текущий код, но это просто хорошая привычка, от которой “хуже точно не будет”, а “лучше – как повезёт”, т.е. в каких-то более сложных случаях это может помочь оптимизатору (в нашем простом случае он и так всё понимает).

Давайте смотреть на указатели ptr1 и ptr2. Мы ведь никогда не меняем их внутри нашей функции! Меняем память, на которую они указывают, а самих их не изменяем. Так давайте скажем об этом компилятору, зачем мы от него это скрываем? Чтобы сказать компилятору, что сам указатель мы изменять не собираемся, нужно прописать слово const после звёздочки. Теперь наша функция выглядит вот так:

void peregon(const void * source, void * destintion, const int size) {
	const byte * const ptr1 = reinterpret_cast<const byte * const>(source);
	byte * const ptr2 = reinterpret_cast<byte * const>(destintion);
	//
	for (int i = 0; i <= size - 1; i++) {
		*(ptr2 + i) = *(ptr1 + i);
	}
}

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

void peregon(const void * source, void * destintion, const int size) {
	const byte * ptr1 = reinterpret_cast<const byte *>(source);
	byte * ptr2 = reinterpret_cast<byte *>(destintion);
	//
	for (int i = 0; i <= size - 1; i++) {
		*ptr2++ = *ptr1++;
	}
}

Ну, вот, собственно о чём хотелось позанудствовать.

2 лайка

Где-то только сегодня мелькало…. Не эффективнее ли будет сделать так:

for (int i = 0; i < size; i++) {

В теории – да, эффективнее, т.к. если нет вообще никакой оптимизации, то выражение size-1 должно заново вычисляться при каждом проходе цикла. Любой компиляторный теоретик глотку бы перегрыз, т.к. “на оптимизатор только лохи надеются”.

Но в реальности, любой современный компилятор вычислит это выражение один раз (тем более, что size мы явно объявили константой, так что поменяться он не может). А в этом случае, в общем-то, без разницы.

Ну, хотя, на одно вычитание больше, если уж совсем буквоедски походить.

1 лайк

Об оптимизаторе.
Мне он, иногда, сильно мешает.
Для прошивки - да, он нужен. Но для проверки скетча…
Когда разрабытываешь программу для Arduino UNO, пусть Вам не покажется странным, забить память мусором до предела. А затем, по мере добавления переменных этот мусор убирать так, чтобы память оставалась быть занятой до отказа.
А когда программа отлажена - этот мусор убрать полностью.

И вопрос о передаче данных.
Пусть надо передать через Serial две переменных:
a=24.8, b=1.6.
Тогда, если на том конце связи знают, что шесть символов под переменную a и четыре символа под переменную b,
строку надо сформироваьт так: “0024.801.6”.
Или же применить разделители: “@24.8#1.6”?

Сам подумай - как контроллер будет догадываться с какой позиции он прием начал - с первой или шестой.

А передавать в двоичном виде с фиксированной точкой - не судьба? Можно обойтись даже менее чем двумя байтами.

Это сколько? Полтора байта?
@Optron , если вы больше 25.5 будете передовать нужно два байта, множите вашу температуру на 10 и шлете два байта uint16_t, на другой стороне обратные действия.

Вообще-то байт состоит из 8 частей - битов.
Число 24.8 можно умножить на 5, передать целое, а на том конце поделить на 5.
Для этого достаточно 7 битов.
С числом 1.6 поступить аналогично.
Итого 14 битов = 1.75 байта.

понятно, опять занудство, тогда вы ему еще, заодно, объясните, как “эффективно” эти 14 бит через сериал передать, да так чтоб на второй стороне все это приняли.

2 лайка

А отряд из восьми ваххабитов называется ваххабайт.

А зачем через сериал? Передавайте телеграфом - там как раз 7-битное слово.
Да и через И-нет в свое время тоже передавалось только по 7 бит. Из-за чего специально придумали UUE-кодировку.