Файл: Руководство по стилю программирования и конструированию по.pdf
ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 30.11.2023
Просмотров: 866
Скачиваний: 2
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
1 ... 36 37 38 39 40 41 42 43 ... 104
ГЛАВА 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).