Руководство по стилю программирования и конструированию по
Скачать 7.6 Mb.
|
ГЛАВА 13 Нестандартные типы данных 319 Пример кода традиционной вставки в список нового узла (C++) void InsertLink( Node *currentNode, Node *insertNode ) { // добавляем “insertNode” после “currentNode” insertNode->next = currentNode->next; insertNode->previous = currentNode; if ( currentNode->next != NULL ) { Эта строка излишне сложна. currentNode->next->previous = insertNode; } currentNode->next = insertNode; } Этот традиционный код добавления нового узла в связный список излишне сло- жен для понимания. В добавлении элемента задействованы три объекта: текущий узел, узел, в данный момент следующий за текущим, и узел, который надо вста- вить между ними. Однако в коде явно упомянуты только два объекта: insertNode и currentNode. Из-за этого вам придется запомнить, что currentNode->next тоже уча- ствует в алгоритме. Если вы попробуете изобразить диаграммой, что происходит, не используя элемент, изначально следующий за currentNode, у вас получится что- то вроде этого: Гораздо лучшая диаграмма содержит все три объекта. Она может выглядеть так: Вот пример кода, который явно упоминает все три объекта, участвующих в алго- ритме: Пример более читабельного кода для вставки узла (C++) void InsertLink( Node *startNode, Node *newMiddleNode ) { // вставляем “newMiddleNode” между “startNode” и “followingNode” Node *followingNode = startNode->next; newMiddleNode->next = followingNode; newMiddleNode->previous = startNode; if ( followingNode != NULL ) { followingNode->previous = newMiddleNode; } startNode->next = newMiddleNode; } Этот код содержит одну дополнительную строку, но без участия выражения current- Node->next->previous из первого фрагмента этот пример легче для понимания. > 320 ЧАСТЬ III Переменные Упрощайте сложные выражения с указателями Сложные выражения с ис- пользованием указателей тяжело читать. Если в вашем коде есть выражения вро- де p->q->r->s.data, подумайте о том человеке, которому придется это читать. Вот особенно вопиющий пример: Пример сложного для понимания выражения с указателем (C++) for ( rateIndex = 0; rateIndex < numRates; rateIndex++ ) { netRate[ rateIndex ] = baseRate[ rateIndex ] * rates->discounts->factors->net; } Подобные выражения заставляют разбираться в коде, а не читать его. Если в ва- шей программе есть сложное выражение, присвойте его понятно названной пе- ременной, чтобы прояснить смысл операции. Вот улучшенная версия примера: Пример упрощения сложного выражения с указателем (C++) quantityDiscount = rates->discounts->factors->net; for ( rateIndex = 0; rateIndex < numRates; rateIndex++ ) { netRate[ rateIndex ] = baseRate[ rateIndex ] * quantityDiscount; } Это упрощение не только позволяет увеличить удобочитаемость, но, возможно, и повысить производительность, упростив операцию с указателем внутри цикла. Но, как обычно, улучшение производительности надо измерить до того, как делать на это крупные ставки. Нарисуйте картинку Описание указателей в коде про- граммы может сбивать с толку. Обычно помогает картинка. Например, изображение задачи по вставке элемента в связ- ный список может выглядеть так (рис. 13-2): Рис. 13-2. Пример рисунка, помогающего осмыслить шаги, необходимые для изменения связей между элементами Удаляйте указатели в связных списках в правильном порядке Обычной проблемой в работе с динамически созданными связными списками является освобождение сначала первого указателя, после чего становится невозможно Перекрестная ссылка Такие диаграммы, как на рис. 13-2, могут стать частью внешней документации вашей програм- мы. О хорошей практике доку- ментирования см. главу 32. ГЛАВА 13 Нестандартные типы данных 321 получить указатель на следующий узел списка. Чтобы избежать этой проблемы, перед удалением текущего элемента убедитесь, что у вас есть указатель на следу- ющий элемент списка. Выделите «запасной парашют» памяти Если в программе используется ди- намическая память, необходимо избежать проблемы ее внезапной нехватки, при- водящей к исчезновению пользовательских данных на бескрайних просторах оперативной памяти. Один из способов дать вашей программе запас прочности — заранее выделить «парашют» памяти. Определите, какой объем памяти нужен программе для сохранения работы, освобождения ресурсов и аккуратного завер- шения. Зарезервируйте эту память в начале работы программы как запасной па- рашют и оставьте ее в покое. Когда памяти станет не хватать, раскройте резерв- ный парашют — освободите эту память и завершите работу программы. Уничтожайте мусор Ошибки указателей сложно отсле- живать, потому что момент времени, когда память, адресу- емая указателем, станет недействительной, не определен. Иногда содержимое памяти после освобождения указателя долго еще выглядит корректным. В другой раз ее содержи- мое изменится сразу. Вы можете избежать ошибок с освобожденными указателями, записывая мусор в блоки памяти прямо перед их освобождением. Если вы используете методы дос- тупа, то это, как и многие другие операции, можно делать автоматически. В C++ при каждом удалении указателя можно делать так: Пример принудительной записи мусорных данных в освобождаемую память (C++) memset( pointer, GARBAGE_DATA, MemoryBlockSize( pointer ) ); delete pointer; Естественно, эта технология требует поддержки списка размеров памяти, выде- ленной для указателей, которые можно было бы получить функцией MemoryBlock- Size(). Мы обсудим это позднее. Устанавливайте указатели null при их удалении или освобождении Из- вестный тип ошибок указателей — это «висячий указатель» (dangling pointer), т. е. обращение к нему после вызова функций delete или free. Одна из причин, по ко- торым ошибки в указателях так сложно обнаружить, в том, что иногда симптомы ошибки никак не проявляются. Записывая в указатели пустое значение после их освобождения, вы не измените факт чтения данных, адресуемых висячим указа- телем. Но вы добьетесь того, что запись данных по этому адресу приведет к ошибке. Возможно, это будет ужасная, катастрофическая ошибка, но по крайней мере ее обнаружите вы, а не кто-то другой. Код, предшествующий операции delete в предыдущем примере, можно дополнить, чтобы обрабатывать и эту ситуацию: Пример установки указателя в null после его удаления (C++) memset( pointer, GARBAGE_DATA, MemoryBlockSize( pointer ) ); delete pointer; pointer = NULL; Дополнительные сведения От- личное обсуждение безопасных подходов к обработке указате- лей в языке C см. в книге «Wri- ting Solid Code» (Maguire, 1993). 322 ЧАСТЬ III Переменные Проверяйте корректность указателя перед его удалением Один из луч- ших способов обрушить программу — вызвать функции delete() или free() для ука- зателя, который уже был освобожден. Увы, лишь немногие языки обнаруживают такой тип ошибок. Если вы присваиваете освобождаемым указателям пустое значение, то перед ис- пользованием или повторным удалением указателя вы сможете проверить его на равенство null. Разумеется, если вы не устанавливаете в null освобождаемые ука- затели, у вас такой возможности не будет. В связи с этим можно предложить сле- дующее дополнение к коду удаления указателя: Пример проверки утверждения о неравенстве указателя null перед его удалением (C++) ASSERT( pointer != NULL, “Attempting to delete null pointer.” ); memset( pointer, GARBAGE_DATA, MemoryBlockSize( pointer ) ); delete pointer; pointer = NULL; Отслеживайте распределение памяти для указателей Ведите список ука- зателей, для которых была выделена память. Это позволит вам проверить, нахо- дится ли указатель в этом списке перед его освобождением. Вот как для этих це- лей может быть изменен код удаления указателя: Пример проверки, выделялась ли память для указателя (C++) ASSERT( pointer != NULL, “Attempting to delete null pointer.” ); if ( IsPointerInList( pointer ) ) { memset( pointer, GARBAGE_DATA, MemoryBlockSize( pointer ) ); RemovePointerFromList( pointer ); delete pointer; pointer = NULL; } else { ASSERT( FALSE, “Attempting to delete unallocated pointer.” ); } Напишите методы-оболочки, чтобы централизовать стратегию борь- бы с ошибками в указателях Как видно из этого примера, каждый вызов опе- раторов new и delete может сопровождаться достаточно большим количеством дополнительного кода. Некоторые технологии, описанные в этом разделе, явля- ются взаимоисключающими или избыточными, и не хотелось бы использовать несколько конфликтующих стратегий в одной программе. Например, вам не надо создавать и проверять обязательные признаки, если вы поддерживаете собствен- ный список действительных указателей. Вы можете минимизировать избыточность в программе и уменьшить вероятность ошибок, написав методы-оболочки для общих операций с указателями. В C++ вы могли бы использовать следующие методы: 쐽 SAFE_NEW Вызывает new для выделения памяти, добавляет указатель в спи- сок задействованных указателей и возвращает вновь созданный указатель вы- ГЛАВА 13 Нестандартные типы данных 323 зывающей стороне. Он может также проверить, что оператор new не вернул null (ошибка нехватки памяти). Поскольку это надо сделать только единожды в этом месте, упрощается процесс обработки ошибок в других частях вашей программы. 쐽 SAFE_DELETE Проверяет, находится ли переданный ему указатель в списке действительных указателей. Если он там есть, метод записывает мусор в адре- суемую им память, удаляет указатель из списка, вызывает C++-оператор delete для освобождения памяти и устанавливает указатель в null. Если указатель не найден в списке, SAFE_DELETE выводит диагностическое сообщение и преры- вает программу. Метод SAFE_DELETE, реализованный в виде макроса, может выглядеть так: Пример добавления оболочки для кода удаления указателя (C++) #define SAFE_DELETE( pointer ) { \ ASSERT( pointer != NULL, “Attempting to delete null pointer.”); \ if ( IsPointerInList( pointer ) ) { \ memset( pointer, GARBAGE_DATA, MemoryBlockSize( pointer ) ); \ RemovePointerFromList( pointer ); \ delete pointer; \ pointer = NULL; \ } \ else { \ ASSERT( FALSE, “Attempting to delete unallocated pointer.” ); \ } \ } В C++ этот метод будет освобождать единичные указатели, поэтому вам придется создать похожий макрос SAFE_DELE- TE_ARRAY для удаления массивов. Централизовав управление памятью в этих двух методах, вы также сможете менять поведение SAFE_NEW и SAFE_DELETE в отладочной и промышленных версиях продукта. Напри- мер, обнаружив попытку освободить пустой указатель в период разработки, SAFE_ DELETE может остановить программу. Но если это происходит во время эксплуа- тации, он может просто записать ошибку в журнал и продолжить выполнение. Вы легко сможете адаптировать эту схему для функций calloc и free в языке C, а также для других языков, использующих указатели. Используйте технологию, не основанную на указателях Указатели в це- лом сложнее для понимания, они подвержены ошибкам и приводят к созданию машинно-зависимого, непереносимого кода. Если вы можете придумать разумную альтернативу указателям, избавьте себя от головной боли и возьмите ее за основу. Указатели в C++ Язык C++ добавил специфические тонкости при работе с указателями и ссылка- ми. В следующих подразделах описаны основные принципы, применяемые в ра- боте с указателями на C++. Перекрестная ссылка О планах по удалению отладочного кода см. подраздел «Запланируйте удаление отладочных средств» раздела 8.6. 324 ЧАСТЬ III Переменные Осознайте различие между указателями и ссылка- ми В C++ и указатели (*), и ссылки (&) косвенно ссылают- ся на объект. Для непосвященных единственной, чисто кос- метической разницей между ними будет способ обращения к полю: object->field или object.field. Наиболее значительным различием является то, что ссылка обязана всегда ссылаться на объект, тогда как указатель может быть равен null. Кроме того, после инициализации ссылки нельзя изменить то, куда она ссылается. Используйте указатели для передачи параметров «по ссылке» и констан- тные ссылки для передачи параметров «по значению» По умолчанию C++ передает в методы аргументы по значению, а не по ссылке. Когда объект переда- ется по значению, C++ создает копию объекта, и при передаче объекта вызываю- щей программе вновь создается копия. Для больших объектов такое копирование может съедать много времени и ресурсов. Следовательно, при передаче объектов в метод вы обычно стараетесь избегать копирования объектов, а это означает, что вы хотите передавать их по ссылке, а не по значению. Однако иногда хотелось бы использовать семантику передачи по значению (т. е. передаваемый объект должен остаться неизменным) и реализацию передачи па- раметра по ссылке (т. е. передавать сам объект, а не его копию). В C++ решением этой проблемы является применение указателей для передачи по ссылке, и — как ни странно может звучать — « константных ссылок» для пе- редачи по значению! Приведем пример: Пример передачи параметров по значению и по ссылке (C++) void SomeRoutine( const LARGE_OBJECT &nonmodifiableObject, LARGE_OBJECT *modifiableObject ); Дополнительным преимуществом этого подхода является синтаксическое разли- чие между изменяемыми и неизменяемыми объектами в вызванном методе. В изменяемых объектах ссылка на элементы будет осуществляться с помощью но- тации object->member, тогда как в неизменяемых будет использоваться нотация object . member. Недостаток этого подхода состоит в необходимости постоянного применения кон- стантных ссылок. Хорошим тоном считается использование модификатора const везде, где это возможно (Meyers, 1998). Поэтому в своем коде вы сможете объяв- лять передаваемые по значению параметры как константные ссылки. В библио- течном коде и других неподконтрольных вам местах вы столкнетесь с проблемой константных параметров. Компромиссной позицией будет все же задавать пара- метры, предназначенные только для чтения, с помощью ссылок, но не объявлять их константными. При этом подходе вы не в полной мере реализуете преимуще- ство проверки компилятором попыток модификации неизменяемых аргументов метода, однако по крайней мере предоставляете возможность визуального разли- чения object->member и object. member. Дополнительные сведения Мно- жество других советов по при- менению указателей в C++ см. в «Effective C++», 2d ed. (Meyers, 1998) и «More Effective C++» (Meyers, 1996). ГЛАВА 13 Нестандартные типы данных 325 Используйте автоматические указатели auto_ptr Если вы еще не вырабо- тали привычку использовать указатели auto_ptr, займитесь этим! Удаляя занятую память автоматически при выходе auto_ptr из области видимости, такие указате- ли решают множество проблем с утечками памяти, присущих обычным указате- лям. Книга «More Effective C++» Скотта Мейерса в правиле 9 содержит интересное обсуждение auto_ptr (Meyers, 1996). Изучите интеллектуальные указатели Интеллектуальные указатели — это замена обычных или «тупых» указателей (Meyers, 1996). Они действуют аналогично обычным, но предоставляют дополнительные возможности по управлению ресур- сами, операциям копирования, присваивания, создания и удаления объектов. Пе- речисленные действия характерны для C++. Более полное обсуждение см. в пра- виле 28 книги «More Effective C++». Указатели в C Вот несколько советов по применению указателей, которые в особенности име- ют отношение к языку C. Используйте явный тип указателя вместо типа по умолчанию Язык C позволяет использовать указатели на char или void для любого типа переменной. Главное, что указатель куда-то указывает, и языку, в общем, не важно, на что именно. Но если вы используете явные типы для указателей, компилятор может выдавать предупреждение о несовпадающих типах указателей и некорректных преобразо- ваниях. Если же явные типы не используются, он этого сделать не сможет. Ста- райтесь применять конкретные типы где только можно. Из этого правила следует необходимость явного преобразования типа в тех слу- чаях, когда нужно его изменить. Так, в этом фрагменте очевидно, что выделяется память для переменной типа NODE_ PTR: Пример явного преобразования типа (C) NodePtr = (NODE_PTR) calloc( 1, sizeof( NODE ) ); Избегайте преобразования типов Этот совет о преобразовании типов не име- ет ничего общего с учебой в актерской школе или отказом всегда играть негодя- ев. Он предлагает избегать втискивания переменной одного типа в переменную другого типа. Такое преобразование выключает способность вашего компилято- ра проверять несовпадения типов и тем самым пробивает брешь в броне защит- ного программирования. В программе, требующей многочисленных преобразо- ваний типов, вероятно, существуют какие-то архитектурные нестыковки, которые нужно пересмотреть. Попробуйте перепроектировать систему, в противном слу- чае старайтесь избегать преобразований типов, насколько это возможно. Следуйте правилу звездочки при передаче параметров Вы можете полу- чить значение аргумента из функции на языке C, только если в операции присва- ивания перед этим аргументом была указана звездочка (*). Многие программис- ты испытывают трудности при определении, когда C позволяет передавать зна- чение обратно в вызывающий метод. Легко запомнить, что если вы указываете звез- дочку перед параметром, которому присваиваете значение, то это значение бу- 326 ЧАСТЬ III Переменные дет возвращено в вызывающий метод. Независимо от того, сколько звездочек вы указали в объявлении, для передачи значения в операторе присваивания должна быть хотя бы одна. Так, в следующем фрагменте значение, присвоенное перемен- ной parameter, не будет передано в вызывающий метод, потому что операция при- сваивания не содержит звездочки: Пример передачи параметра, который не будет работать (C) void TryToPassBackAValue( int *parameter ) { parameter = SOME_VALUE; } А здесь значение, присвоенное параметру parameter, будет возвращено, потому что перед parameter указана звездочка: Пример передачи параметра, который сработает (C) void TryToPassBackAValue( int *parameter ) { *parameter = SOME_VALUE; } Используйте sizeof() для определения объема памяти, необходимой для раз- мещения переменной Легче использовать sizeof(), чем выяснять размер типа в справочнике. Кроме того, sizeof() работает с вашими собственными структурами, которые в справочнике не описаны. Так как значение вычисляется в момент ком- пиляции, то sizeof() не влияет на производительность. Кроме того, он переносим: перекомпиляция в другой среде автоматически изменяет размеры, вычисленные sizeof(). И еще он прост в сопровождении, поскольку при изменении используе- мого типа изменится и рассчитываемый размер. 13.3. Глобальные данные Глобальные переменные доступны из любого места програм- мы. Иногда этот термин небрежно используют для обозна- чения переменных с более широкой областью видимости, чем локальные переменные, таких как классовые перемен- ные, доступные во всех методах класса. Но сама по себе доступность внутри единственного класса не означает, что переменная является глобальной. Наиболее опытные программисты пришли к выводу, что применять глобальные переменные рискованней, чем локальные. Эти программисты также считают, что полезней осуществлять доступ к данным с помощью методов. Даже если применение глобальных переменных не всегда ведет к ошиб- кам, оно все-таки вряд ли представляет собой хороший способ програм- мирования. Перекрестная ссылка О разли- чиях между глобальными данны- ми и данными класса, см. под- раздел «Ошибочное представле- ние о данных класса как о гло- бальных данных» раздела 5.3. |