Руководство по стилю программирования и конструированию по
Скачать 7.6 Mb.
|
ГЛАВА 12 Основные типы данных 285 число большее, чем максимально возможное целое. Скажем, если вы умножаете 250 * 300, правильным ответом будет 75 000. Но если максимальное целое — 65 535, то, возможно, из-за переполнения вы получите 9464 (75 000 – 65 536 = 9464). Вот интервалы значений для часто встречающихся целых типов (табл. 12-1): Табл. 12-1. Интервалы значений некоторых целых типов Целый тип Интервал 8-битный со знаком От –128 до 127 8-битный без знака От 0 до 255 16-битный со знаком От –32 768 до 32 767 16-битный без знака От 0 до 65 535 32-битный со знаком От –2 147 483,648 до 2 147 483 647 32-битный без знака От 0 до 4 294 967 295 64-битный со знаком От –9 223 372 036 854 775 808 до 9 223 372 036 854 775 807 64-битный без знака От 0 до 18 446 744 073 709 551 615 Простейший способ предотвращения целочисленного переполнения — просмотр каждого члена арифметического выражения с целью представить наибольшее возможное значение, которое он может принимать. Так, если в целочисленном выражении m = j * k, наибольшим значением для j будет 200, а для k — 25, то мак- симальным значением для m будет 200 * 25 = 5 000. Для 32-разрядных машин это вполне допустимо, так как максимальным целым является 2 147 483 647. С другой стороны, если максимально возможное значение для j — это 200 000, а для k — 100 000, то значение m может достигать 200 000 * 100 000 = 20 000 000 000. Это уже неприемлемо, так как 20 000 000 000 больше, чем 2 147 483 647. В этом слу- чае для размещения наибольшего возможного значения m вам придется исполь- зовать 64-битные целые или числа с плавающей запятой. Кроме того, учитывайте будущее развитие программы. Если m никогда не будет больше 5 000 — отлично. Но если ожидается, что m будет постоянно расти на протяжении нескольких лет, примите это во внимание. Проверяйте на переполнение промежуточные результаты Число, полу- чаемое в конце вычислений, — не единственное, о котором следует беспокоить- ся. Представьте, что у вас есть такой код: Пример переполнения промежуточных результатов (Java) int termA = 1000000; int termB = 1000000; int product = termA * termB / 1000000; System.out.println( “( “ + termA + “ * “ + termB + “ ) / 1000000 = “ + product ); Вы можете подумать, что значение Product вычисляется как (100 000*100 000) / 100 000 и поэтому равно 100 000. Но программе приходится вычислять проме- жуточное значение 100 000*100 000 до того, как будет выполнено деление на 100 000, а это значит, что нужно хранить такое большое число, как 1 000 000 000 000. Угадайте, что получится? Вот результат: ( 1000000 * 1000000 ) / 1000000 = -727 286 ЧАСТЬ III Переменные Если значение целых чисел в вашей системе не превышает 2 147 483 647, проме- жуточный результат слишком велик для целого типа данных. В такой ситуации промежуточный результат, который должен быть равен 1 000 000 000 000, на са- мом деле равен 727 379 968, поэтому, когда вы делите его на 100 000, вы получа- ете - 727 вместо 100 000. Вы можете решить проблему переполнения промежуточных результатов так же, как и в случае целочисленного переполнения: изменив тип на длинное целое или число с плавающей запятой. 12.3. Числа с плавающей запятой Главная особенность применения чисел с плавающей запятой в том, что многие дробные десятичные числа не могут быть точно представлены с помощью нулей и единиц, используемых в цифровом компьютере. В бесконечных десятичных дробях, таких как 1/3 или 1/7, обычно сохраняется только 7 или 15 цифр после запятой. В моей версии Microsoft Visual Basic 32-бит- ное представление дроби 1/3 в виде числа с плавающей запятой равно 0,33333330. То есть точность ограничена 7 цифрами. Такая точность достаточна для большин- ства случаев, но все же способна иногда вводить в заблуждение. Вот несколько рекомендаций по использованию чисел с плавающей запятой. Избегайте сложения и вычитания слишком разных по размеру чисел Для 32-битной переменной с плавающей запятой сумма 1 000 000,00 + 0,1, вероятно, будет равна 1 000 000,00, так как в 32 битах недостаточно значимых цифр, чтобы охватить интервал между 1 000 000 и 0,1. Ана- логично 5 000 000,02 – 5 000 000,01, вероятно, равно 0,0. Решение? Если вам нужно складывать настолько разные по величине числа, сна- чала отсортируйте их, а затем складывайте, начиная с самых маленьких значений. Аналогично, если вам надо сложить бесконечный ряд значений, начните с наи- меньшего члена, т. е. суммируйте члены в обратном порядке. Это не решит про- блемы округления, но минимизирует их. Многие алгоритмические книги предла- гают решения для таких случаев. Избегайте сравнений на равенство Числа с плавающей запятой, которые должны быть равны, на самом деле рав- ны не всегда. Главная проблема в том, что два разных спо- соба получить одно и то же число не всегда приводят к одинаковому результату. Так, если 10 раз сложить 0,1, то 1,0 получается только в редких случаях. Следующий пример содержит две переменных ( nominal и sum), которые должны быть равны, но это не так. Пример неправильного сравнения чисел с плавающей точкой (Java) Переменная nominal — 64-битное вещественное число. double nominal = 1.0; double sum = 0.0; Перекрестная ссылка Книги, содержащие алгоритмы реше- ния этих проблем, см. в подраз- деле «Дополнительные ресур- сы» раздела 10.1. 1 равен 2 для достаточно боль- ших значений 1. Аноним > ГЛАВА 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. |