cast double to long приводит к неожиданным результатам

Я написал небольшую программу, чтобы показать свою проблему:

void setup() 
{
  Serial.begin(9600);

  double K=39.85;
  double a=K*100;
  Serial.print("a= ");
  Serial.println(a);

  long val_long = 0;
  val_long = a;
  Serial.print("val_long= ");
  Serial.println(val_long);
}

void loop() {
}

Я получил это от своего Arduino uno:

a= 3985.00
val_long=3984

Давно раздумываю и не могу найти ошибку! Почему длинное значение уменьшается на единицу? Кто-нибудь может мне помочь?

, 👍1

Обсуждение

проголосовать за исключение минимального скетча, @jsotola


1 ответ


7

Проблема в том, что числа с плавающей запятой редко являются точным представлением. Таким образом, число с плавающей запятой сохраняется как ближайшее число, которое точно может быть представлено как число с плавающей запятой, а именно
10446438 × 2−18 = 39,84999847412109375.

Если умножить это число на 100, получится 1 6322 559 × 2−12 = 3984,999755859375.

А затем приведение к типу int или long приводит к результату 3984, а не 3985.

поэтому всякий раз, когда вы переводите значение из числа с плавающей запятой или двойного числа в целое или длинное, вы всегда должны добавлять 0,5 (поэтому все значения из [x - 0,5, x + 0,5) округляются до x, предполагая, что x >= 0, таким образом :

val_long = int(a + 0.5);

Остерегайтесь отрицательных значений, поэтому см. https://www.cs .cmu.edu/~rbd/papers/cmj-float-to-int.html

Лучше использовать функцию round, см. комментарий Эдгара Боне ниже.

ОБНОВЛЕНИЕ

См. комментарий к гонкам на легкость с Моникой

В случае, если округление не требуется, но вы все равно хотите, чтобы 39,84999847412109375 равнялось 39,85, вам нужно добавить значение, имеющее значение младшего значащего бита (на самом деле немного меньше, но это невозможно определить). В большинстве случаев лучше добавить половину значения, которое вы хотите. Поэтому, если вам нужна точность до двух знаков после запятой, добавьте 0,005.

,

Большое спасибо, вы сделали мой день! Теперь все работает нормально., @Hardy72

Serial.println(a,5);, возможно, уже выявил эту проблему. Особенно на 8-битных Arduinos, где «double» имеет ширину всего 32 бита. (то же, что и поплавок), @DataFiddler

1. Я отредактировал ответ, добавив точные цифры. 2. Вы можете использовать round() из ядра Arduino. Это макрос, который добавляет ±0,5 (в зависимости от знака) перед приведением к длинному значению., @Edgar Bonet

@EdgarBonet Большое спасибо. Я также добавлю в свой ответ примечание к вашему комментарию., @Michel Keijzers

_ "поэтому всякий раз, когда вы переводите значение из числа с плавающей запятой или двойного числа в целое или длинное, вы всегда должны добавлять 0,5" _ _Если_ округление - это то, что вам нужно. Иногда это не так, тогда вам нужен другой обходной путь для небольших неточностей. Ваше решение изменит поведение, например, когда K == 39,856. Ваш ответ должен касаться этого., @Lightness Races in Orbit

@LightnessRaceswithMonica Я не уверен, что понимаю, что вы имеете в виду… когда K == 39,856, добавление половины и округление (в меньшую сторону) дает 40, что является обычным способом преобразования. Что (другие небольшие неточности) вы имеете в виду?, @Michel Keijzers

@MichelKeijzers Если «K» равно 39,856, то «a» равно 3985,6, поэтому вывод программы равен 3985, что, по-видимому, сделано намеренно. Ваше предложение изменяет вывод на 3986. Вы изменили смысл программы, не упомянув об этом. Альтернативой является использование «другого обходного пути для [этих] небольших неточностей [например, вызывающего эту ошибку]», например эпсилонов. При этом, если «K» будет иметь максимум 2 значащих десятичных знака, то это спорно., @Lightness Races in Orbit

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