Новая рубрика «Карта граблей» принципиально отличается от уже привычной «Говнокод по пятницам». Дело в том, что «говнокод» – это работающий код (возможно, с граблями), который написан каким-то необычным способом разной степени перверсивности. «Карта …» – более простой формат. Здесь показывается чаще всего не работающий код (или работающий «не всегда») с разбросанными кругом граблями. Причём, грабли не прячутся (как в «говнокоде»), предлагая читателю догадываться где они, а показываются открыто – «весомо, грубо, зримо». Т.е. загадок, типа «что случилось?» не будет, будет унылая, будничная демонстрация граблей. Посмотрим, какой формат приживётся. Или оба.
Итак, приступим. Скажите, пожалуйста, есть ли какая-либо разница между константами (литералами) 3 и 3u?
Нет, ну, конечно, понятно, что первая – знаковое целое, а вторая – беззнаковое. И? Они ведь обе положительны! Более того, если сравнить их побитово, то они абсолютно одинаковы – не отличаются ни одним битом! Так есть между ними разница или нет?
Давайте посмотрим
void setup(void) {
Serial.begin(9600);
int a = digitalRead(10); // a может быть только 0 или 1
//
Serial.print("With '3': ");
Serial.println((a - 3 < 1) ? "Okay!" : "It sucks!");
//
Serial.print("With '3u': ");
Serial.println((a - 3u < 1) ? "Okay!" : "It sucks!");
}
void loop(void) {}
Результат:
With '3': Okay!
With '3u': It sucks!
Что имеем? Переменная a
у нас не может быть ничем, кроме 0 или 1. Стало быть, условия в строках №№ 6 и 9 всегда истинны и, стало быть, всегда должно печататься «Okay!».
Запускаем! Опаньки! Не всё так просто! Оказывается, какая-то разница всё же есть!
Ладно, попробуем переменную a
объявить не int
, а long
(больше ничего не меняем) и
опять запускаем
void setup(void) {
Serial.begin(9600);
long a = digitalRead(10); // a может быть только 0 или 1
//
Serial.print("With '3': ");
Serial.println((a - 3 < 1) ? "Okay!" : "It sucks!");
//
Serial.print("With '3u': ");
Serial.println((a - 3u < 1) ? "Okay!" : "It sucks!");
}
void loop(void) {}
Результат:
With '3': Okay!
With '3u': Okay!
На этот раз оба варианта выдают “Okay!”
Это что ж получается? Значение переменной a
(либо 0, либо 1) не поместилось в int
, а в long
поместилось? Интересно кто ж там в int
не помещается – ноль или единица?
Давайте ещё раз, чтоб не было сомнений:
Почему так?
Ну, это несложно
дело в операндах операции вычитания.
В первом случае (когда a
– int
, а 3u – unsigned
) а
преобразуется к типу unsigned
, т.к. множество значений этого типа – надмножество множества значений типа int
.
Во втором случае (когда a
– long
, а 3u – unsigned
) 3u
преобразуется к типу long
, опять потому, что множество значений этого типа – надмножество множества значений типа unisgned
.
Далее операция производится в преобразованных типах и потому во втором случае получается отрицательное число, а в первом положительно и очень большое.
Вот и вся разгадка.
Только что нам это объяснение даёт? Вы видели хоть одного программиста, который бы так дотошно разбирал каждое арифметическое выражение на предмет что там куда преобразуется (ну, пока не наступит на грабли?)? Я не видел. Зато я видел программистов, которые обходят эти грабли далеко стороной, соблюдая очень простое правило
Никогда не смешивайте знаковые и беззнаковые величины в одном выражении!
Ну, конечно, если у Вас нет цели привнести в свою жизнь немного остроты, неожиданности и веселья, то не смешивайте! Вот просто никогда, от слова совсем. И тогда Вы никогда не наступите на эти грабли, просто потому, что не суётесь в тот огород, где они лежат. Это как в боксе, где говорят: лучший блок – не попадать под удар.
Немного философии
На самом деле, я здесь привёл сложный и хитрый случай такой ошибки, в котором компилятор не может нам помочь и предупредить о граблях. В более простых случаях он таки предупреждает, за что ему большое спасибо. Например, запись
for (int i = 0; i < sizeof(int); i++)
вызовет предупреждение:
warning: comparison between signed and unsigned integer expressions [-Wsign-compare]
Что в этом случае сделает программист? Очевидно, поправит. А что сделает «прогер» или «кодер»? Забьёт! Потому, что он точно знает, что в этом случае всё отработает правильно. В принципе, он прав, в этом случае – да, всё отработает.
Но!!!
Он умышленно залез в огород, где валяются грабли! Да, пока он на них не наступает, но он полез туда, где они есть! И забил на это!
Всякий раз, когда я вижу программу (библиотеку, ядро и т.п.), автор которой на это забивает, у меня возникает вопрос: «А на что ещё он забил?» и … после недолгого раздумья, свежескачанная библиотека отправляется в помойку. Советую и вам поступать подобным образом.