Загадочные числа
Загадочные числа — это константы, размеры массивов, позиции символов и другие числовые значения, появляющиеся в программе непосредственно, как "буквальные константы".
Давайте имена загадочным числам. В принципе нужно считать, что любое встречающееся в программе число, отличное от 0 и 1, является загадочным и должно получить собственное имя. Просто "сырые" числа в тексте программы не дают представления об их происхождении и назначении, затрудняя понимание и изменение программы. Вот отрывок из программы, которая печатает гистограмму частот букв на терминале с разрешением 24 X 80. Этот отрывок неоправданно запутан из-за целого сонма непонятных чисел:
Наряду с другими в коде присутствуют числа 20, 21, 22, 23 и 27. Они как-то
тесно связаны... или нет? На самом деле есть только три критических числа,
существенных для программы: 24 — число строк экрана; 80 — число столбцов
экрана и, наконец, 26 — количество букв в английском алфавите. Однако,
как мы видим, ни одно из этих чиеел в коде не встречается, отчего числа,
используемые в коде, становятся еще более загадочными.
Присвоив имена числам, имеющим принципиальное значение, мы облегчим понимание кода. Например, станет понятно, что число 3 берется из арифметического выражения (80-1)/26, а массив let должен иметь 26 элементов, а не 27 (иначе возможна ошибка его переполнения на единицу — из-за того, что экранные координаты индексируются, начиная с 1). Сделав еще пару усовершенствований, мы придем к следующему результату:
Теперь стало гораздо понятнее, что же делает основной цикл: в нем используется
стандартный идиоматический цикл от 0 до NLET-1, то есть по всем элементам
данных. Вызовы функции d raw также стали более понятны — названия вроде
MAXROW и MINCOL дают четкое представление об аргументе. Главное же — программу
теперь можно без труда адаптировать к другому разрешению экрана или другому
алфавиту: с чисел и с кода снята завеса таинственности.
Определяйте числа как константы, а не как макросы. Программисты, пишущие на С, традиционно использовали для определения загадочных чисел директиву #def те. Однако препроцессор С — мощный, но несколько туповатый инструмент, а макросы — вообще довольно опасная вещь, поскольку они изменяют лексическую структуру программы. Пусть лучше язык делает свойственную ему работу. В С и C++ целые (integer) константы можно определять с помощью выражения en urn (которое мы и использовали в предыдущем примере). В C++ любые константы можно определять с помощью ключевого слова const:
const int MAXROW = 24, MAXCOL = 80;
В Java для этого служит слово final:
static final int MAXROW = 24, MAXCOL = 80;
В С также можно определять значения с помощью ключевого слова const, но эти значения нельзя использовать как границы массива, так что зачастую придется прибегать все к тому же enum.
Используйте символьные, а не целые константы. Для проверки свойств символов должны использоваться функции из <ctype. h> или их эквиваленты. Если проверку организовать так:
? if (с >= 65 && с <= 90) ?....
то ее результат будет всецело зависеть от представления символов на конкретной
машине. Лучше использовать
if (с >= 'А' с <= 'Z')
?...
но и это может не принести желаемого результата, если буквы в имеющейся кодировке идут не по порядку или если в алфавите есть и другие буквы. Лучшее решение — привлечь на помощь библиотеку:
if (isupper(c))
в С и C++ или
if (Character. isllpperCase(c))
в Java.
Сходный вопрос — использование в программе числа 0. Оно используется очень часто и в различных контекстах. Компилятор преобразует этр число в соответствующий тип, однако читателю гораздо проще понять роль каждого конкретного 0, если тип этого числа каждый раз обозначен явным образом. Так, например, стоит использовать (void * )0 или NULL для обозначения нулевого указателя в С, а ' \0' вместо просто 0 — для обозначения нулевого байта в конце строки. Другими словами, не пишите
а пишите
Мы рекомендуем использовать различные явные константы, оставив О для простого
целого нуля, — такие константы обозначат цель использования данного значения.
Надо отметить, правда, что в C++ для обозначения нулевого указателя принято
использовать все же 0, а не NULL. Лучше всего проблема решена в Java —
там ключевое слово null определено как ссылка на объект, которая ни к
чему не относится.
Используйте средства языка для определения размера объекта. Не используйте явно заданного размера ни для каких типов данных — так, например, используйте sizeof (int) вместо прямого указания числа 2,4 и т.п. По сходным причинам лучше использовать sizeof(array[OJ) вместо sizeof (int) — меньше придется исправлять при изменении типа массива.
Использование оператора sizeof избавит вас от необходимости выдумывать имена для чисел, обозначающих размер массива. Например, если написать
char buf[1024];
fgets(buf, sizeof(buf), stdin);
то размер буфера хоть и станет "загадочным числом", от которого
мы предостерегали ранее, но зато оно появится только один раз — непосредственно
в описании. Может быть, и не стоит прилагать слишком большие усилия, чтобы
придумать имя для размера локального массива, но определенно стоит постараться
и написать код, который не нужно переписывать при изменении размера или
типа:
У массивов в Java есть поле length, которое содержит количество элементов:
char buf[] = new char[1024];
for (int 1=0; i < but.length; i++)
.....
В С и C++ нет эквивалента этому полю, но для массива (не указателя), описание которого является видимым, количество элементов можно вычислить с помощью следующего макроса:
Здесь опять-таки размер массива задается лишь в одном месте, и при его изменении весь остальной код менять не придется.
В данном макросе нет проблем с многократным вычислением аргумента, поскольку в нем нет никаких побочных эффектов, и на самом деле все вычисление происходит во время компиляции. Это пример грамотного использования макроса — здесь он делает то, чего не может сделать функция: вычисляет размер массива исходя из его описания.
Упражнение 1-10
Как бы вы переписали приведенные определения, чтобы уменьшить число
потенциальных ошибок?