Совершенный код. Совершенный код. Мастер-класс. Стив Макконнелл. Руководство по стилю программирования и конструированию по
Скачать 5.88 Mb.
|
ГЛАВА 12 Основные типы данных 309 Перечислимые типы Используются ли в программе перечислимые типы вместо именованных констант ради их улучшенной читабельности, надежности и модифицируе- мости? Используются ли перечислимые типы вместо логических переменных, если все значения переменной не могут быть переданы с помощью true и false? Проверяются ли некорректные значения перечислимых типов в условных операторах? Зарезервирован ли первый элемент перечислимого типа как недопустимый? Перечислимые константы Используются ли в программе именованные константы вместо магических чисел для объявления данных и границ циклов? Используются ли именованные константы последовательно, чтобы одно значение не представлялось в одном месте константой, а в другом — лите- ралом? Массивы Находятся ли все индексы массива в его границах? Свободны ли ссылки на массив от ошибок потери единицы? Указаны ли все индексы многомерных массивов в правильном порядке? В правильном ли порядке используются переменные-индексы во вложенных циклах, не происходит ли пересечения индексов? Создание типов Используются ли в программе отдельные типы для каждого вида данных, который может измениться? Ориентируются ли имена типов на реальные сущности, которые эти типы представляют, а не на типы языка программирования? Достаточно ли наглядны имена типов, чтобы помочь документированию объявлений данных? Не произошло ли переопределение предопределенных типов? Рассматривался ли вопрос создания нового класса вместо простого пере- определения типа? Ключевые моменты Работа с определенными типами данных требует запоминания множества правил для каждого из них. Используйте список контрольных вопросов из этой главы, чтобы убедиться, что вы учли основные проблемы с ними. Создание собственных типов, если ваш язык это позволяет, упрощает модифи# кацию вашей программы и делает ее более самодокументируемой. Прежде чем создавать простой тип с помощью typedef или его эквивалента, подумайте, не следует ли создать вместо него новый класс. 310 ЧАСТЬ III Переменные Г Л А В А 1 3 Нестандартные типы данных Содержание 13.1. Структуры 13.2. Указатели 13.3. Глобальные данные Связанные темы Фундаментальные типы данных: глава 12 Защитное программирование: глава 8 Нестандартные управляющие структуры: глава 17 Сложность в разработке ПО: раздел 5.2 Некоторые языки программирования поддерживают экзотические виды данных в дополнение к типам, обсуждавшимся в главе 12. В разделе 13.1 рассказывается, при каких обстоятельствах вы могли бы использовать структуры вместо классов. В разделе 13.2 описываются детали использования указателей. Если у вас возни# кали проблемы с использованием глобальных данных, из раздела 13.3 вы узнае# те, как их избежать. Если вы думаете, что типы данных, описанные в этой главе, — это не те типы, о которых вы обычно читаете в современных книгах по объек# тно#ориентированному программированию, то вы абсолютно правы. Поэтому эта глава и называется « Нестандартные типы данных». 13.1. Структуры Термин «структура» относится к типу данных, построенному на основе других типов. Так как массивы — особый случай, они рассматриваются отдельно в главе 12. В этом разделе обсуждаются структурированные данные, созданные пользо# вателем: structs в C/C++ и Structures в Microsoft Visual Basic. В Java и C++ классы тоже иногда выглядят, как структуры (когда они состоят только из открытых членов данных и не содержат открытые методы). Чаще всего вы предпочтете создавать классы, а не структуры, чтобы задейство# вать преимущества закрытости и функциональности, предлагаемой классами, в http://cc2e.com/1378 ГЛАВА 13 Нестандартные типы данных 311 дополнение к открытым данным, поддерживаемым структурами. Но иногда пря# мое манипулирование блоками данных может быть полезно. Используйте структуры для прояснения взаимоотношений между дан' ными Структуры объединяют группы взаимосвязанных элементов. Иногда труднее всего понять, какие данные в программе используются вместе. Это похоже на прогулку по маленькому городку с вопросами о том, кто с кем в родстве. Вы вы# ясняете, что каждый кому#то кем#то приходится, но вы не уверены в этом, и ни# когда не можете получить внятного ответа. Если данные хорошо структурированы, выяснение, что с чем связано, сильно упрощается. Вот пример данных, которые не были структурированы: Пример неструктурированных, вводящих в заблуждение переменных (Visual Basic) name = inputName address = inputAddress phone = inputPhone title = inputTitle department = inputDepartment bonus = inputBonus Так как данные не структурированы, кажется, что эти операторы присваивания объединены. На самом деле, переменные name, address, и phone относятся к ря# довому служащему, а title, department и bonus — к менеджеру. В этом фрагменте нет подсказок о том, что используется два вида данных. В следующем фрагменте применение структур делает взаимоотношения яснее: Пример более информативных, структурированных переменных (Visual Basic) employee.name = inputName employee.address = inputAddress employee.phone = inputPhone supervisor.title = inputTitle supervisor.department = inputDepartment supervisor.bonus = inputBonus В этом коде, содержащем структурированные переменные, очевидно, что часть данных относится к работнику, а остальные — к менеджеру. Используйте структуры для упрощения операций с блоками данных Вы можете объединить взаимосвязанные элементы в структуру и выполнять опера# ции над ней. Проще обрабатывать структуру целиком, чем выполнять те же дей# ствия над каждым элементом. Это надежней и требует меньше строк кода. Допустим, у вас есть группа взаимосвязанных элементов данных — скажем, ин# формация о работнике в базе данных персонала. Если данные не объединены в структуру, простое копирование всей группы может потребовать большого чис# ла операторов. Вот пример на Visual Basic: 312 ЧАСТЬ III Переменные Пример громоздкого копирования группы (Visual Basic) newName = oldName newAddress = oldAddress newPhone = oldPhone newSsn = oldSsn newGender = oldGender newSalary = oldSalary Каждый раз, желая передать сведения о работнике, вам приходится иметь дело со всеми этими операторами. Если вам нужно добавить новый элемент данных, на# пример numWithholdings , вам придется найти все места, где написаны эти при# сваивания, и добавить еще одно: newNumWithholdings = oldNumWithholdings. Представьте, как ужасно будет менять местами данные о двух работниках. Вам не надо напрягать воображение — вот пример: Пример утомительного способа обмена двух групп данных (Visual Basic) ‘ Поменять местами старые и новые данные о работнике. previousOldName = oldName previousOldAddress = oldAddress previousOldPhone = oldPhone previousOldSsn = oldSsn previousOldGender = oldGender previousOldSalary = oldSalary oldName = newName oldAddress = newAddress oldPhone = newPhone oldSsn = newSsn oldGender = newGender oldSalary = newSalary newName = previousOldName newAddress = previousOldAddress newPhone = previousOldPhone newSsn = previousOldSsn newGender = previousOldGender newSalary = previousOldSalary Более легкое решение проблемы — объявить структурную переменную: Пример объявления структуры (Visual Basic) Structure Employee name As String address As String phone As String ssn As String gender As String salary As long ГЛАВА 13 Нестандартные типы данных 313 End Structure Dim newEmployee As Employee Dim oldEmployee As Employee Dim previousOldEmployee As Employee Теперь вы можете поменять элементы в старой и новой структуре с помощью трех операторов: Пример более легкого способа обмена двух групп данных (Visual Basic) previousOldEmployee = oldEmployee oldEmployee = newEmployee newEmployee = previousOldEmployee Если вы хотите добавить новое поле, например numWithholdings, вы просто вно# сите его в объявление Structure. Ни одно из вышеприведенных выражений не потребует никаких изменений. C++ и другие языки также содержат подобные возможности. Используйте структуры для упрощения списка па' раметров Сократить список параметров метода позволя# ют структурированные переменные. Технология похожа на только что продемонстрированную. Вместо того чтобы пе# редавать параметры по одному, можно объединить взаимо# связанные элементы в структуру и передать все скопом. Вот пример утомительного способа передачи группы общих параметров: Пример громоздкого вызова метода, не использующего структуру (Visual Basic) HardWayRoutine( name, address, phone, ssn, gender, salary ) А вот пример простого способа вызвать метод с помощью структурированной переменной, состоящей из параметров первого метода: Пример элегантного вызова метода, использующего структуру (Visual Basic) EasyWayRoutine( employee ) Если вы хотите добавить numWithholdings к первому варианту программы, вам придется прочесать весь код и изменить каждый вызов HardWayRoutine(). Если же вы добавите элемент к структуре Employee, вам совсем не придется изменять па# раметры вызова EasyWayRoutine(). Вы можете довести эту идею до крайности, поместив все переменные вашей программы в одну большую, жирную структуру и передавая ее всюду. Аккуратные программисты избегают объединения большего количества данных, чем это необходимо логически. Более того, аккуратисты стараются не передавать параметры в виде структур, если нужны лишь одно#два поля из этой структуры — в этом случае передаются указанные поля. Это аспект вопроса о сокрытии информации: часть данных скрыта в методе, а часть — от метода. Между методами должна передаваться только та информация, ко# торую необходимо знать. Перекрестная ссылка О том, как распределять данные между методами, см. подраздел «Под- держивайте сопряжение сла- бым» раздела 5.3. Перекрестная ссылка Об опас- ностях передачи слишком боль- шого объема данных см. под- раздел «Поддерживайте сопря- жение слабым» раздела 5.3. 314 ЧАСТЬ III Переменные Используйте структуры для упрощения сопровождения Так как, приме# няя структуры, вы группируете взаимосвязанные данные, изменение структуры тре# бует минимальных исправлений в программе. В большей степени это относится к участкам кода, не связанным с вносимым изменением логически. Поскольку изменения часто приводят к ошибкам, то чем меньше изменений, тем меньше ошибок. Если в структуре Employee есть поле title и вы решаете его удалить, вам не нужно исправлять ни списки параметров, ни операторы присваивания, исполь# зующие эту структуру целиком. Конечно, вам придется поправить код, напрямую работающий со званиями работников, но эти действия связаны с процессом уда# ления поля title концептуально, и поэтому вы вряд ли о них забудете. На участках кода, логически не связанных с полем title, преимущество структури# рования данных еще более очевидно. Иногда программы содержат выражения, концептуально работающие скорее с набором данных, а не с отдельными компо# нентами. В этих случаях отдельные элементы, такие как поле title, упоминаются только потому, что они — часть набора. Эти участки кода не имеют логических причин работать конкретно с полем title, поэтому при изменении title такие уча# стки очень легко пропустить. Если же вы используете структуру, все будет нор# мально, потому что код ссылается на набор взаимосвязанных данных, а не на каждый элемент индивидуально. 13.2. Указатели Использование указателей — одна из наиболее подверженных ошибкам областей программирования. Это привело к тому, что современные язы# ки, такие как Java, C# и Visual Basic, не предоставляют указатель в каче# стве типа данных. Применять указатели и так сложно, а правильное применение требует от вас отличного понимания того, как ваш компилятор управляет распре# делением памяти. Многие общие проблемы с безопасностью, особенно случаи пе# реполнения буфера, могут быть сведены к ошибочному использованию указате# лей. (Howard and LeBlanc, 2003). Даже если в вашем языке не нужны указатели, их хорошее понимание поможет вам разобраться, как работает ваш язык программирования. А щедрая доза защит# ного программирования будет еще полезнее. Парадигма для понимания указателей Концептуально каждый указатель состоит из двух частей: области памяти и зна# ния, как следует интерпретировать содержимое этой области. Область памяти Область памяти — это адрес, часто представленный в шестнадцатеричном виде. В 32#разрядном процессоре адрес будет 32#битным числом, например 0x0001EA40. Сам по себе указатель содержит только этот адрес. Чтобы обратиться к данным, на которые этот указатель указывает, надо пойти по этому адресу и как#то интер# претировать содержимое памяти в этой области. Сам по себе этот участок памя# ти — просто набор битов. Чтобы он обрел смысл, его надо как#то истолковать. ГЛАВА 13 Нестандартные типы данных 315 Знание, как интерпретировать содержимое Информация о том, как интерпретировать содержимое области памяти, предо# ставляется основным типом указателя. Если указатель указывает на целое число, то на самом деле это значит, что компилятор интерпретирует область памяти, за# даваемую указателем, как целое число. Конечно, у вас может быть указатель на целое число, строку или число с плавающей точкой, которые ссылаются на одну и ту же область памяти. Но только один из них будет корректно интерпретировать содержимое этой области. Говоря об указателях, полезно помнить, что память сама по себе не имеет одно# значной интерпретации. И только с помощью конкретного типа указателя набор битов в некоторой области памяти истолковывается как осмысленное значение. Рис. 13#1. содержит несколько представлений одной и той же области памяти, интерпретированной разными способами. Рис. 13'1. Объем памяти, используемый каждым типом данных, показан двойной линией В каждом случае на рис. 13#1 указатель ссылается на область памяти, содержащую шестнадцатеричное значение 0x0A. Количество байт, используемых после 0A, за# висит от того, как память интерпретируется. Содержимое памяти также зависит от ее интерпретации. (Еще содержимое зависит и от используемого процессора, так что не забудьте об этом, если будете пытаться повторить эти результаты на вашем Cray#десктопе.) Одна и та же необработанная область памяти может быть представлена как строка, целое число, число с плавающей точкой или иначе — все зависит от основного типа указателя, ссылающегося на эту область памяти. 316 ЧАСТЬ III Переменные Основные советы по использованию указателей Обычно ошибку легко найти, но трудно исправить. Но это не относится к про# блемам с указателями. Ошибка в указателе — чаще всего результат того, что он указывает не туда, куда должен. Когда вы присваиваете значение некорректной переменной#указателю, вы записываете данные туда, куда не должны записывать. Это называется «повреждением памяти». Порой оно приводит к ужасным круше# ниям системы, порой изменяет результаты вычислений в другой части програм# мы, порой вынуждает программу неожиданно завершить работу метода, а порой вообще ничего не делает. В последнем случае ошибка в указателе как бомба с часовым механизмом — она разрушит вашу программу за пять минут до ее пока# за самому главному заказчику. По симптомам таких ошибок тяжело понять, что их вызывало. Поэтому самым трудоемким в процессе исправления ошибок указа# теля является поиск их причины. Для успешной работы с указателями требуется двухэтапная стратегия. Во#первых, старайтесь изначально не делать в них ошибок. Проблемы с указателями так сложно обнаружить, что дополнительные превентив# ные меры вполне оправданны. Во#вторых, выявляйте ошибки в указателях как можно быстрее после того, как они закодированы. Симптомы ошибок в указате# лях настолько изменчивы, что дополнительные меры с целью сделать эти симп# томы более предсказуемыми, также вполне оправданны. Вот как можно добиться этих ключевых целей. Изолируйте операции с указателями в методах или классах Допустим, в нескольких частях программы используется связный список. Вместо того чтобы каждый раз обрабатывать его вручную, напишите методы доступа NextLink(), Pre% viousLink(), InsertLink() и DeleteLink(). Минимизировав количество мест, в которых выполняется обращение к указателю, вы уменьшите вероятность неосторожных ошибок, распространяющихся по всей программе, на поиск которых уходит веч# ность. Поскольку такой код становится относительно независимым от деталей пред# ставления данных, вы также увеличиваете шансы его повторного использования в других программах. Написание методов, распределяющих память для указате# лей, — еще один способ централизовать управление вашими данными. Выполняйте объявление и определение указателей одновременно При# своение переменной начального значения рядом с местом ее объявления — как правило, хорошая практика программирования. Она обладает особой ценностью при работе с указателями. Вот как не надо делать: Пример неправильной инициализации указателя (C++) Employee *employeePtr; // много кода employeePtr = new Employee; Даже если этот код изначально работает правильно, при дальнейших модифика# циях он подвержен ошибкам, так как существует шанс, что кто#нибудь попробует ГЛАВА 13 Нестандартные типы данных 317 использовать employeePtr после его объявления, но до инициализации. Вот более безопасный подход: Пример правильной инициализации указателя (C++) // много кода Employee *employeePtr = new Employee; Удаляйте указатели в той же области действия, где они были созданы Соблюдайте симметрию при выделении и освобождении памяти для указателей. Если вы используете указатель в единственном блоке кода, вызывайте new для выделения памяти и deletе для ее освобождения в том же блоке. Если вы распре# деляете память внутри метода, освобождайте ее внутри аналогичного метода, а если в конструкторе объекта — освобождайте в деструкторе этого объекта. Метод, выделяющий память для указателя, а затем ожидающий, что клиентский код вручную его освободит, нарушает целостность, что прямиком ведет к ошибкам. Проверяйте указатели перед их применением Прежде чем использовать ука# затель в критической части вашей программы, удостоверьтесь, что он указывает на осмысленную область памяти. Так, если вы ожидаете, что память распределя# ется между адресами StartData и EndData, у вас должно вызывать подозрение, если значение указателя меньше, чем StartData, или больше, чем EndData. Вам надо определить значения StartData и EndData в вашей системе. Эту проверку можно выполнять автоматически, если обращаться к указателям не напрямую, а через методы доступа. Проверяйте переменную, на которую ссылается указатель, перед ее ис' пользованием Иногда вы можете выполнить корректную проверку значения, на которое ссылается указатель. Скажем, если вы предполагаете, что он указывает на целое число от 0 до 1000, значения больше 1000 должны вызывать у вас подозре# ние. Если указатель ссылается на строку в стиле C++, ее длина свыше 100 симво# лов также может вызывать недоверие. Эти проверки тоже могут быть выполнены автоматически при работе с указателями с помощью методов доступа. Используйте закрепленные признаки для проверки повреждения памяти «Поле#тэг» (tag field) или «закрепленный признак» (dog tag) — это поле, которое вы добавляете к структуре исключительно с целью проверки ошибок. Когда вы выделяете память для переменной, поместите в это поле закрепленного призна# ка значение, которое должно остаться неизменным. Используя структуру, особенно освобождая для нее память, проверяйте значение закрепленного признака. Если это поле не содержит ожидаемого значения, значит, данные были повреждены. Удаляя указатель, измените значение этого поля. Так вы сможете выявить ошибку, если случайно попытаетесь освободить этот указатель еще раз. Например, пусть нам нужно выделить 100 байт памяти: 1. Выделите 104 байта — на 4 байта больше, чем требуется. 318 ЧАСТЬ III Переменные 2. Укажите в первых четырех байтах значение обязательного признака, а затем верните указатель на область, следующую за этими четырьмя байтами. 3. Когда понадобится удалить указатель, проверьте значение признака. 4. Если значение признака корректно, присвойте ему 0 или другое значение, которое ваша программа будет считать недопустимым. Главное, чтобы его ошибочно не посчитали корректным после освобождения памяти. С той же целью заполните всю область памяти 0, 0xCC или любым другим неслучайным значением. 5. В заключение удалите указатель. Размещение закрепленного признака в начале выделенного блока памяти помо# жет выявить попытки повторного освобождения блока. При этом вам не нужно поддерживать список используемых областей памяти. Размещение признака в конце блока позволит выявить попытки записи за допустимые границы области памя# ти. Для достижения обеих целей закрепленные признаки можно размещать и в начале, и в конце блока. Вы можете использовать этот подход и для дополнительных проверок, предло# женных ранее, — того, что значение указателя должно быть между адресами Start% Data и EndData. Однако, чтобы убедиться, что указатель содержит корректный адрес, вместо проверки возможного диапазона адресов следует проверять наличие это# го указателя в списке используемых областей памяти. Если вы проверите поле признака один раз — перед удалением переменной, то некорректный признак будет означать, что когда#то на протяжении жизни пере# менной ее содержимое было повреждено. Но чем чаще вы будете проверять этот признак, тем ближе к источнику проблемы будет обнаружено повреждение. Добавьте явную избыточность Альтернативой полю признака будет исполь# зование двух таких полей. Если данные в избыточных полях не совпадают, вы знаете, что память была повреждена. Этот способ может потребовать большого количества дополнительного кода, если напрямую манипулировать указателями. Но если работу с указателями изолировать в методах, то дублировать код придет# ся лишь в нескольких местах. Используйте для ясности дополнительные переменные указателей Ни# когда не экономьте на переменных#указателях. Одну и ту же переменную нельзя вызвать для разных целей. Особенно это касается переменных#указателей. Довольно тяжело выяснить, какие действия выполняются со связным списком и без того, чтобы разбираться, почему одна переменная genericLink используется снова и снова и куда указывает pointer%>next%>last%>next. Рассмотрим фрагмент: |