Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
kernigan_paik.doc
Скачиваний:
0
Добавлен:
01.07.2025
Размер:
2.91 Mб
Скачать

1.5. Загадочные числа

Загадочные числа — это константы, размеры массивов, позиции сим­волов и другие числовые значения, появляющиеся в программе непо­средственно, как "буквальные константы".

Давайте имена загадочным числам. В принципе нужно считать, что любое встречающееся в программе число, отличное от 0 и 1, является загадочным и должно получить собственное имя. Просто "сырые" числа в тексте про­граммы не дают представления об их происхождении и назначении, затруд­няя понимание и изменение программы. Вот отрывок из программы, кото­рая печатает гистограмму частот букв на терминале с разрешением 24 X 80. Этот отрывок неоправданно запутан из-за целого сонма непонятных чисел:

? fac = lim / 20; /* установка масштаба */

? if (fac < 1)

? fac = 1;

? /* генерация гистограммы */

? for (i = 0, col = 0; i < 27; i++, j++) {

? col += 3;

? k = 21 - (let[i] / fac);

? star = (let[i] == 0) ? ' ' : ' * ' ;

? for (j = k; j < 22; j++)

? draw(j, col, star);

? }

? draw(23, 2, ' '); /* разметка оси х */

? for (i = 'A'; i< ‘Z’ ; i++)

? printf("%c” , i ) ;

Наряду с другими в коде присутствуют числа 20, 21, 22, 23 и 27. Они как-то тесно связаны... или нет? На самом деле есть только три критических числа, существенных для программы: 24 — число строк экрана; 80 — чис­ло столбцов экрана и, наконец, 26 — количество букв в английском алфа­вите. Однако, как мы видим, ни одно из этих чисел в коде не встречается, отчего числа, используемые в коде, становятся еще более загадочными.

Присвоив имена числам, имеющим принципиальное значение, мы облег­чим понимание кода. Например, станет понятно, что число 3 берется из арифметического выражения (80-1)/26, а массив let должен иметь 26 эле­ментов, а не 27 (иначе возможна ошибка его переполнения на единицу — из-за того, что экранные координаты индексируются, начиная с 1). Сделав еще пару усовершенствований, мы придем к следующему результату:

enum {

MINROW = 1, /* верхняя граница,*/

MINCOL = 1, /* левая граница */

MAXROW = 24, /* нижняя граница (<=) */

MAXCOL = 80, /* правая граница (<=) */

LABELROW = 1, /* позиция подписей оси */

NLET = 26, /* количество букв алфавита*/

HEIGHT = MAXROW – 4, /* высота столбиков */

WIDTH = (MAXCOL-1 / NLET ) /*- ширина столбиков */

};

fac = (lim + HEIGHT-1) / HEIGHT; /* установка масштаба */

if (fac < 1)

fac = 1;

for (i = 0; i < NLET; i++) { /* генерация гистограммы */

if (let[i] == 0)

continue;

for (j = HEIGHT - let[i]/fac; j < HEIGHT; j++)

draw(j+1 + LABELROW, (i+1)*WIDTH, ' * ');

}

draw(MAXROW-1, MINCOL+1, ' '); /* разметка оси х */

for (i = 'A'; i <= 'Z'; i++)

printf("%c ", i);

Теперь стало гораздо понятнее, что же делает основной цикл: в нем ис­пользуется стандартный идиоматический цикл от 0 до NLET-1, то есть по всем элементам данных. Вызовы функции d raw также стали более понят­ны — названия вроде MAXROW и MINCOL дают четкое представление об аргу­менте. Главное же — программу теперь можно без труда адаптировать к другому разрешению экрана или другому алфавиту: с чисел и с кода снята завеса таинственности.

Определяйте числа как константы, а не как макросы. Программисты, пишущие на С, традиционно использовали для определения загадочных чисел директиву ffdefine. Однако препроцессор С— мощный, но не­сколько туповатый инструмент, а макросы — вообще довольно опасная вещь, поскольку они изменяют лексическую структуру программы. Пусть лучше язык делает свойственную ему работу. В С и 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.isUpperCase(c))

............

в Java.

Сходный вопрос — использование в программе числа 0. Оно использу­ется очень часто и в различных контекстах. Компилятор преобразует это число в соответствующий тип, однако читателю гораздо проще понять роль каждого конкретного 0, если тип этого числа каждый раз обозначен явным образом. Так, например, стоит использовать (void * )0 или NULL для обозначения нулевого указателя в С, а ' \0' вместо просто 0 — для обозна­чения нулевого байта в конце строки. Другими словами, не пишите

? str = 0;

? name[i] = 0;

? х = 0;

а пишите

str = NULL;

name[i] = ‘\0’;

x = 0.0;

Мы рекомендуем использовать различные явные константы, оставив О для простого целого нуля, — такие константы обозначат цель исполь­зования данного значения. Надо отметить, правда, что в C++ для обозна­чения нулевого указателя принято использовать все же 0, а не NULL. Луч­ше всего проблема решена в Java — там ключевое слово null определено как ссылка на объект, которая ни к чему не относится.

Используйте средства языка для определения размера объекта. Не ис­пользуйте явно заданного размера ни для каких типов данных — так, на­пример, используйте sizeof (int) вместо прямого указания числа 2,4 и т.п. По сходным причинам лучше использовать sizeof(аггау[0]) вместо sizeof (int) — меньше придется исправлять при изменении типа массива. Использование оператора sizeof избавит вас от необходимости выду­мывать имена для чисел, обозначающих размер массива. Например, если написать

char buf[1024];

fgets(buf, sizeof(buf), stdin);

то размер буфера хоть и станет "загадочным числом", от которого мы предостерегали ранее, но зато оно появится только один раз — непосред­ственно в описании. Может быть, и не стоит прилагать слишком боль­шие усилия, чтобы придумать имя для размера локального массива, но определенно стоит постараться и написать код, который не нужно пере­писывать при изменении размера или типа.

У массивов в Java есть поле length, которое содержит количество эле­ментов:

char buf[] = new char[1024];

for (int i = 0; i < buf.length; i++)

............

В С и C++ нет эквивалента этому полю, но для массива (не указате­ля), описание которого является видимым, количество элементов мож­но вычислить с помощью следующего макроса:

#define NELEMS(array) (sizeof(array) / sizeof(array[0]))

double dbuf [100];

for (i = 0; i < NELEMS(dbuf); i++)

............

Здесь опять-таки размер массива задается лишь в одном месте, и при его изменении весь остальной код менять не придется.

В данном макросе нет проблем с многократным вычислением ар­гумента, поскольку в нем нет никаких побочных эффектов, и на са­мом деле все вычисление происходит во время компиляции. Это пример грамотного использования макроса — здесь он делает то, чего не может сделать функция: вычисляет размер массива исходя из его описания.

Упражнение 1-10

Как бы вы переписали приведенные определения, чтобы уменьшить число потенциальных ошибок?

? #define FT2METER 0.3048

? #define METER2FT 3.28084

? #define MI2FT 5280.0

? #define MI2KM 1.609344

? #define SQMI2SQKM 2.589988

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]