Файл: Руководство по стилю программирования и конструированию по.pdf
ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 30.11.2023
Просмотров: 882
Скачиваний: 2
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
ГЛАВА 12 Основные типы данных
287
for ( int i = 0; i < 10; i++ ) {
sum вычисляется как 10*0,1. Она должна быть равна 1,0.
sum += 0.1;
}
Здесь неправильное сравнение.
if ( nominal == sum ) {
System.out.println( “Numbers are the same.” );
}
else {
System.out.println( “Numbers are different.” );
}
Как вы, наверное, догадались, программа выводит:
Numbers are different.
Вывод каждого значения
sum в цикле for выглядит так:
0.1 0.2 0.30000000000000004 0.4 0.5 0.6 0.7 0.7999999999999999 0.8999999999999999 0.9999999999999999
Таким образом, хорошей идеей будет найти альтернативу операции сравнения на равенство для чисел с плавающей запятой. Один эффективный подход состоит в том, чтобы определить приемлемый интервал точности, а затем использовать логические функции для выяснения, достаточно ли близки сравниваемые значе- ния. Для этого обычно пишется функция
Equals(), которая возвращает true, если значения попадают в этот интервал, и
false — в противном случае. На языке Java такая функция может выглядеть так:
Пример метода для сравнения чисел с плавающей запятой (Java)
final double ACCEPTABLE_DELTA = 0.00001;
boolean Equals( double Term1, double Term2 ) {
if ( Math.abs( Term1 - Term2 ) < ACCEPTABLE_DELTA ) {
return true;
}
else {
return false;
}
}
Перекрестная ссылка Этот при- мер — доказательство того, что из каждого правила есть исклю- чения. Переменные здесь содер- жат цифры в именах. Правило против использования цифр в именах переменных см. в раз- деле 11.7.
>
>
288
ЧАСТЬ III Переменные
Если код в примере «неправильного сравнения чисел с плавающей запятой» из- менить так, чтобы для сравнения использовался этот метод, новое выражение получит следующий вид:
if ( Equals( Nominal, Sum ) ) ...
При запуске теста программа выведет сообщение:
Numbers are the same.
В зависимости от требований вашего приложения использование жестко закодиро- ванного значения
ACCEPTABLE_DELTA может быть недопустимо. Возможно, придет- ся вычислять
ACCEPTABLE_DELTA на основании размера двух сравниваемых чисел.
Предупреждайте ошибки округления Проблемы с ошибками округления сход- ны с проблемами слишком разных по размеру чисел. У них одинаковые причи- ны и похожие методики решения. Кроме того, далее перечислены способы реше- ния проблем округления.
쐽
Измените тип переменной на тип с большей точностью. Если вы используете числа с одинарной точностью, замените их числами с двойной точностью и т. д.
쐽
Используйте двоично-десятичные переменные (binary coded decimal, BCD). BCD-числа обычно работают медлен- нее и требуют больше памяти для хранения, но предотвра- щают множество ошибок округления. Это особенно важно,
если используемые переменные представляют собой долла- ры и центы или другие величины, которые должны точно балансироваться.
쐽
Измените тип с плавающей запятой на целые значения. Это такая самодельная замена BCD-переменных. Возможно, вам придется использовать 64-битные це- лые, чтобы получить нужную точность. Этот способ предполагает, что вы сами будете отслеживать дробные части чисел. Допустим, изначально вы вели учет денежных сумм, применяя числа с плавающей запятой, при этом центы указы- вались как дробная часть. Это обычный способ обработки долларов и центов.
Когда вы переключаетесь на целые числа, вам нужно вести учет центов с помо- щью целых, а долларов — с помощью чисел, кратных 100 центам. Иначе говоря,
вы умножаете сумму в долларах на 100 и храните центы в этой переменной в интервале от 0 до 99. Такое решение может показаться абсурдным, но оно эф- фективно и с точки зрения скорости, и с точки зрения точности. Вы можете упростить эти манипуляции, создав класс
DollarsAndCents, скрывающий целое представление чисел и предоставляющий необходимые числовые операции.
Проверяйте поддержку специальных типов данных в языке и дополнитель-
ных библиотеках Некоторые языки, включая Visual Basic, предоставляют такие типы данных, как
Currency, предназначенные для данных, чувствительных к ошиб- кам округления. Если ваш язык содержит встроенный тип данных, предоставляю- щий такую функциональность, используйте его!
Перекрестная ссылка Как пра- вило, конвертация в тип BCD
минимально влияет на произво- дительность. Если вы озабоче- ны проблемой производитель- ности, см. раздел 25.6.
ГЛАВА 12 Основные типы данных
289
12.4. Символы и строки
Этот раздел предлагает несколько советов по использованию строк. Первый от- носится к строкам во всех языках.
Избегайте магических символов и строк Магические символы — это литеральные символы (например, '
А'), а ма- гические строки — это литеральные строки (например,
”Gigamatic Accounting Program”), которые разбросаны по всей программе. Если ваш язык программирования поддерживает применение именованных констант, то лучше задействуй- те их. В противном случае используйте глобальные переменные. Далее перечис- лено несколько причин, по которым надо избегать литеральных строк.
쐽
Для таких часто встречающихся строк, как имя программы, названия команд,
заголовки отчетов и т. п., вам может понадобиться поменять содержимое. На- пример,
”Gigamatic Accounting Program” в более поздней версии может изме- ниться на
”New and Improved! Gigamatic Accounting Program”.
쐽
Все большее значение приобретают международные рынки, и строки, сгруп- пированные в файле ресурсов переводить гораздо легче, чем раскиданные по всей программе.
쐽
Строковые литералы обычно занимают много места. Они используются для меню, сообщений, экранов помощи, форм ввода и т. д. Если их слишком мно- го, они выходят из-под контроля и вызывают проблемы с памятью. Во многих системах объем памяти, занимаемый строками, не является причиной для бес- покойства. Однако при программировании встроенных систем и других при- ложений, в которых каждый байт на счету, проблему хранения строк легче решить, если эти строки относительно независимы от кода.
쐽
Символьные и строковые литералы могут быть загадочными. Комментарии или именованные константы проясняют ваши намерения. В следующем примере смысл
0x1B неясен. Константа ESCAPE делает значение более понятным.
Пример сравнений с использованием строк (C++)
Плохо!
if ( input_char == 0x1B ) ...
Лучше!
if ( input_char == ESCAPE ) ...
Следите за ошибками завышения/занижения на единицу Поскольку подстро- ки могут индексироваться аналогично массивам, не забывайте об ошибках завы- шения/занижения на 1, которые приводят к чтению или записи за концом строки.
Узнайте, как ваш язык и система поддерживают Uni-
code В некоторых языках, например в Java, все строки хра- нятся в формате Unicode. В других — таких, как C и C++ —
работа со строками в Unicode требует применения отдельного набора функций. Пре- образование между Unicode и другими наборами символов часто необходимо для взаимодействия со стандартными библиотеками и библиотеками сторонних про-
Перекрестная ссылка Вопросы использования магических сим- волов и строк аналогичны во- просам применения магических чисел (см. раздел 12.1).
http://cc2e.com/1285
>
>
290
ЧАСТЬ III Переменные изводителей. Если часть строк не будет поддерживать Unicode (скажем, в C или C++),
как можно раньше решите, стоит ли вообще использовать символы Unicode. Если вы решились на это, подумайте, где и когда будете это делать.
Разработайте стратегию интернационализации/локализации в ранний
период жизни программы Вопросы, связанные с интернационализацией, от- носятся к разряду ключевых. Решите, будут ли все строки храниться во внешних ресурсах и будет ли создаваться отдельный вариант программы для каждого язы- ка или конкретный язык будет определяться во время выполнения.
Если вам известно, что нужно поддерживать толь-
ко один алфавит, рассмотрите вариант использова-
ния набора символов ISO 8859 Для приложений, исполь- зующих только один алфавит (например, английский), которым не надо поддер- живать несколько языков или какой-либо идеографический язык (такой как пись- менный китайский), расширенный ASCII-набор стандарта ISO 8859 — хорошая альтернатива символам Unicode.
Если вам необходимо поддерживать несколько языков, используйте Uni-
code Unicode обеспечивает более полную поддержку международных наборов символов, чем ISO 8859 или другие стандарты.
Выберите целостную стратегию преобразования строковых типов Если вы используете несколько строковых типов, общим подходом, помогающим хра- нить строковые типы в порядке, будет хранение всех строк программы в одном формате и преобразование их в другой формат как можно ближе к операциям ввода и вывода.
Строки в языке C
Строковый класс в стандартной библиотеке шаблонов C++ решил большинство проблем со строками языка C. А тот, кто напрямую работает с C-строками, ниже узнает о способах избежать часто встречающихся ошибок.
Различайте строковые указатели и символьные массивы Проблемы со строковыми указателями и символьными массивами возникают из-за способа об- работки строк в C. Учитывайте различия между ними в двух случаях.
쐽
Относитесь с недоверием к строковым выражениям, содержащим знак равен- ства. Строковые операции в C практически всегда выполняются с помощью
strcmp(), strcpy(), strlen() и аналогичных функций. Знаки равенства часто сигна- лизируют о каких-то ошибках в указателях. Присваивание в C не копирует стро- ковые константы в строковые переменные. Допустим, у нас есть выражение:
StringPtr = “Some Text String”;
В этом случае
”Some Text String” — указатель на литеральную текстовую строку,
и это присваивание просто присвоит указателю
StringPtr адрес данной стро- ки. Операция присваивания не копирует содержимое в
StringPtr.
쐽
Используйте соглашения по именованию, чтобы различать переменные —
массивы символов и указатели на строки. Одно из общепринятых соглашений —
использование
ps как префикса для обозначения указателя на строку, и ach —
как префикса для символьного массива. И хотя они не всегда ошибочны, от- http://cc2e.com/1292
ГЛАВА 12 Основные типы данных
291
носитесь все-таки с подозрением к выражениям, включающим переменные с обоими префиксами.
Объявляйте для строк в стиле C длину, равную КОНСТАНТА+1 В C и C++
ошибки завышения на 1 в C-строках — обычное явление, потому что очень легко забыть, что строка длины
n требует для хранения n + 1 байт, и не выделить место для нулевого терминатора (байта в конце строки, установленного в 0). Эффектив- ный способ решения этой проблемы — использовать именованные константы при объявлении всех строк. Суть в том, что именованные константы применяются всегда одинаково: Сначала длина строки объявляется как
КОНСТАНТА+1, а затем КОНСТАН-
ТА используется для обозначения длины строки во всем остальном коде. Вот пример:
Пример правильных объявлений строк (С)
/* Объявляем строку длиной «константа+1».
Во всех остальных местах программы используем «константа»,
а не «константа +1». */
Эта строка объявлена с длиной NAME_LENGTH +1.
char name[ NAME_LENGTH + 1 ] = { 0 }; /* Длина строки — NAME_LENGTH */
/* Пример 1: Заполняем строку символами ‘A’, используя константу NAME_LENGTH
для определения количества символов ‘A’, которые можно скопировать.
Заметьте: используется NAME_LENGTH, а не NAME_LENGTH + 1. */
В действиях со строкой NAME_LENGTH используется здесь…
for ( i = 0; i < NAME_LENGTH; i++ )
name[ i ] = ‘A’;
/* Пример 2: Копируем другую строку в первую, используя константу для определения максимальной длины, которую можно копировать. */
…и здесь.
strncpy( name, some_other_name, NAME_LENGTH );
Если у вас не будет соглашения по этому поводу, иногда вы будете объявлять строки длиной
NAME_LENGTH, а в операциях использовать NAME_ LENGTH-1; а иногда вы будете объявлять строки длиной
NAME_LENGTH+1 и работать с NAME_LENGTH. Каждый раз при использовании строки вам придется вспоминать, как вы ее объявили.
Если же вы всегда одинаково объявляете строки, думать, как работать с каждой из них, не надо, и вы избежите ошибок из-за того, что забыли особенность объявле- ния данной строки. Выработка соглашения минимизирует умственную перегруз- ку и ошибки при программировании.
Инициализируйте строки нулем во избежание строк
бесконечной длины Язык C определяет конец строки пу- тем поиска нулевого терминатора — байта в конце строки,
установленного в 0. Какой предполагалась длина строки, зна-
>
>
>
Перекрестная ссылка Подроб- нее об инициализации данных см. раздел 10.3.
292
ЧАСТЬ III Переменные чения не имеет: C никогда не найдет ее конец, если не найдет нулевой байт. Если вы забыли поместить нулевой байт в конец строки, строковые операции могут ра- ботать не так, как вы ожидаете.
Вы можете предупредить появление бесконечных строк двумя способами. Во-пер- вых, при объявлении инициализируйте символьные массивы
0:
Пример правильного объявления символьного массива (C)
char EventName[ MAX_NAME_LENGTH + 1 ] = { 0 };
Во-вторых, при динамическом создании строк инициализируйте их
0, используя функцию
calloc() вместо malloc(). Функция calloc() выделяет память и инициали- зирует ее
0. malloc() выделяет память без инициализации, поэтому вы рискуете,
используя память, выделенную с помощью
malloc().
Используйте в C массивы символов вместо указате-
лей Если объем занимаемой памяти некритичен (а часто так и есть), объявляйте все строковые переменные как мас- сивы символов. Это поможет избежать проблем с указателями, а компилятор бу- дет выдавать больше предупреждений в случае неправильных действий.
Используйте strncpy() вместо strcpy() во избежание строк бесконечной
длины Строковые функции в C существуют в опасной и безопасной версиях. Более опасные функции, такие как
strcpy() и strcmp(), продолжают работу до обнаруже- ния нулевого терминатора. Их более безобидные спутники —
strncpy() и strncmp()
— принимают максимальную длину в качестве параметра, так что, даже если строки будут бесконечными, ваши вызовы функций не зациклятся.
12.5. Логические переменные
Логические или булевы переменные сложно использовать неправильно, а их вдум- чивое применение сделает вашу программу аккуратней.
Используйте логические переменные для документи-
рования программы Вместо простой проверки логиче- ского выражения вы можете присвоить его значение пере- менной, которая сделает смысл теста очевидным. Например,
в этом фрагменте из условия
if не ясно, выполняется ли проверка завершения, ошибочной ситуации или чего-то еще:
Пример логического условия, чье назначение неочевидно (Java)
if ( ( elementIndex < 0 ) || ( MAX_ELEMENTS < elementIndex ) ||
( elementIndex == lastElementIndex )
) {
}
В следующем фрагменте применение логических переменных делает назначение
if-проверки яснее:
Перекрестная ссылка О масси- вах см. раздел 12.8.
Перекрестная ссылка Об ис- пользовании комментариев для документирования программы см. главу 32.
Перекрестная ссылка Пример использования логической фун- кции для документирования программы см. в подразделе
«Упрощение сложных выраже- ний» раздела 19.1.