Файл: Руководство по стилю программирования и конструированию по.pdf
ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 30.11.2023
Просмотров: 862
Скачиваний: 2
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
ГЛАВА 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.
1 ... 37 38 39 40 41 42 43 44 ... 104
ГЛАВА 13 Нестандартные типы данных
327
Распространенные проблемы с глобальными данными
Если вы без разбора используете глобальные переменные или считаете невозмож- ность их применения ненужным ограничением, то, вероятно, вы еще не проник- лись значимостью принципов модульности и сокрытия информации. Модульность,
сокрытие информации и связанное с ними использование хорошо спроектиро- ванных классов, может, и не панацея, но они помогают сделать большие программы понятнее и легче в сопровождении. Когда вы это поймете, вам захочется писать методы и классы как можно меньше взаимодействующие с глобальными перемен- ными и внешним миром.
Можно привести массу проблем, связанных с глобальными данными, но в основ- ном они сводятся к следующим вариантам.
Непреднамеренные изменения глобальных данных Вы можете изменить зна- чение глобальной переменной в одном месте и ошибочно думать, что оно оста- лось прежним где-то в другом. Такая проблема известна как «побочный эффект».
Например, в этом фрагменте
theAnswer является глобальной переменной:
Пример побочного эффекта (Visual Basic)
theAnswer — глобальная переменная.
theAnswer = GetTheAnswer()
GetOtherAnswer() изменяет theAnswer.
otherAnswer = GetOtherAnswer()
Значение averageAnswer неправильно.
averageAnswer = (theAnswer + otherAnswer) / 2
Вы предполагаете, что вызов
GetOtherAnswer() не изменяет значение theAnswer,
потому что иначе среднее значение в третьей строке будет вычислено неверно.
На самом деле
GetOtherAnswer() все-таки изменяет theAnswer, и в программе воз- никает ошибка.
Причудливые и захватывающие проблемы при использовании псевдони-
мов для глобальных данных Использование псевдонима означает обращение к переменной по двум и более именам. Это происходит, когда глобальная пере- менная передается в метод, а там используется и в качестве глобальной перемен- ной, и в качестве параметра. Вот пример метода, работающего с глобальной пе- ременной:
Пример метода, подверженного
проблеме с псевдонимами (Visual Basic)
Sub WriteGlobal( ByRef inputVar As Integer )
inputVar = 0
globalVar = inputVar + 5
MsgBox( “Input Variable: “ & Str( inputVar ) )
MsgBox( “Global Variable: “ & Str( globalVar ) )
End Sub
>
>
>
328
ЧАСТЬ III Переменные
А вот код вызывающего метода с глобальной переменной в качестве аргумента:
Пример вызова метода с аргументом,
демонстрирующим проблему псевдонимов (Visual Basic)
WriteGlobal( globalVar )
Поскольку
inputVar инициализируется 0, и WriteGlobal() добавляет 5 к inputVar, чтобы получить новое значение
globalVar, вы ожидаете, что globalVar будет на 5 больше,
чем
inputVar. Но вот неожиданный результат:
Результат проблемы с псевдонимами
Input Variable: 5
Global Variable: 5
Хитрость в том, что
globalVar и inputVar — на самом деле одна и та же перемен- ная! Поскольку
globalVar передается в WriteGlobal() вызывающим методом, к ней обращаются с помощью двух разных имен. Поэтому результат вызовов
MsgBox()
отличается от ожидаемого: они показывают одну и ту же переменную дважды, хотя и используют два разных имени.
Проблемы реентерабельности глобальных данных Сейчас все чаще встречается код, который может выполняться одновременно нескольки- ми потоками. Многопоточное программирование создает вероятность обращения к глобальным данным будут обращаться не только из разных методов,
но и из разных экземпляров одной и той же программы. В такой среде вы долж- ны быть уверены, что глобальные данные сохранят свои значения, даже если бу- дет запущено несколько копий программы. Это важная проблема, и вы сможете ее избежать, используя технологии, предложенные ниже.
Затруднение повторного использования кода, вызванное глобальными
данными Для использования кода из одной программы в другой вам нужно выта- щить его из первой программы и внедрить во вторую. В идеале вы могли бы извлечь отдельный метод или класс, встроить в другую программу и наслаждаться жизнью.
Глобальные данные усложняют картину. Если класс, который вы хотите исполь- зовать повторно, читает или записывает глобальные данные, вы не сможете про- сто перенести его в новую программу. Вам придется изменить либо новую про- грамму, либо старый класс, чтобы они стали совместимы. Правильным решением будет модификация старого класса, чтобы он не использовал глобальные данные:
сделав это, вы сможете в следующий раз повторно использовать этот класс без дополнительных усилий. Неправильным решением будет модификация новой программы с целью создания таких же глобальных данных, какие требуются ста- рому классу. Это как вирус — глобальные данные не только влияют на исходный код, но и распространяются по новым программам, использующим какие-либо классы из старой.
Проблемы с неопределенным порядком инициализации глобальных данных
Порядок, в котором данные из разных «единиц трансляции» (файлов) будут ини- циализироваться, в некоторых языках программирования (в частности, C++) не определен. Если при инициализации глобальной переменной из одного файла
ГЛАВА 13 Нестандартные типы данных
329
используется глобальная переменная из другого файла, значение второй перемен- ной предсказать сложно, если только вы не предпримете специальные действия для их инициализации в правильном порядке.
Эта проблема решается с помощью обходного маневра, описанного в правиле 47 книги
Скотта Мейерса «Effective C++» (Meyers, 1998). Но изощренность решения как раз и иллюстрирует ту излишнюю сложность, которую привносят глобальные данные.
Нарушение модульности и интеллектуальной управляемости, привноси-
мое глобальными данными Сущность создания программ, состоящих из бо- лее чем нескольких сотен строк кода, заключается в управлении сложностью. Един- ственный способ, позволяющий интеллектуально управлять большой программой,
— это разбить ее на части так, чтобы в каждый момент времени думать только об одной из них. Модульность — наиболее мощный инструмент для разбиения про- граммы на части.
Глобальные данные проделывают дыры в возможности модуляризации. Если вы используете глобальные данные, разве вы можете сосредоточиться только на од- ном методе? Нет. Вам приходится сосредоточиваться на этом методе и на всех других, в которых используются те же глобальные данные. Хотя эти данные и не разрушают модульность программы полностью, они ее ослабляют, и это доста- точная причина, чтобы найти лучшее решение ваших проблем.
Причины для использования глобальных данных
Ревнители чистоты данных иногда утверждают, что программисты никогда не должны использовать глобальные данные. Но большинство программ работают с
«глобальными данными» в широком смысле этого слова. Записи в базе данных являются глобальными, так же как и данные конфигурационных файлов, напри- мер реестра Windows. Именованные константы — это тоже глобальные данные,
хотя и не глобальные переменные.
При аккуратном применении глобальные переменные могут быть полезны в не- которых ситуациях.
Хранение глобальных значений Иногда какие-то данные концептуально от- носятся к целой программе. Это может быть переменная, отражающая состояние программы, скажем, режим командной строки, или интерактивный, или нормаль- ный режим, или режим восстановления после сбоев. Или это может быть инфор- мация, необходимая в течение всей программы, например, таблица с данными,
используемая всеми методами программы.
Эмуляция именованных констант Хотя C++, Java, Visual
Basic и большинство современных языков поддерживают именованные константы, некоторые языки, такие как Python,
Perl, Awk и язык сценариев UNIX, до сих пор — нет. Вы мо- жете использовать глобальные переменные как подстановки для именованных кон- стант, если ваш язык их не поддерживает. Так, вы можете заменить константные значения
1 и 0 глобальными переменными TRUE и FALSE, установленными в 1 и
0. Или вы можете заменить число 66, используемое как число строк на странице,
переменной
LINES_PER_PAGE = 66. Этот подход позволяет упростить дальнейшее изменение кода, кроме того, его легче читать. Такое упорядоченное применение
Перекрестная ссылка Об имено- ванных константах см. раздел 12.7
330
ЧАСТЬ III Переменные глобальных данных — отличный пример программирования
с использованием
языка, а не
на языке (см. раздел 34.4).
Эмуляция перечислимых типов Вы также можете использовать глобальные переменные для эмуляции перечислимых типов в таких языках, как Python, кото- рые напрямую такие типы не поддерживают.
Оптимизация обращений к часто используемым данным Иногда перемен- ная так часто вызывается, что упоминается в списке параметров каждого метода.
Вместо того чтобы включать ее в каждый список параметров, вы можете сделать ее глобальной. Однако случаи, когда к переменной обращаются отовсюду, редки.
Обычно она используется ограниченным набором методов. Их вы можете объе- динить в класс вместе с данными, с которыми они работают. Позднее мы вернем- ся к этому вопросу.
Исключение бродячих данных Иногда вы передаете данные методу или классу только для того, чтобы передать в другой метод или класс. Например, у вас может быть объект-обработчик ошибок, применяемый в каждом методе. Если метод в се- редине цепочки вызовов не использует этот объект, он называется «бродячим» (tramp data). Применение глобальных переменных помогает исключить бродячие данные.
Используйте глобальные данные
только как последнее средство
Прежде чем вы решите использовать глобальные данные, рассмотрите следующие альтернативы.
Начните с объявления всех переменных локальными и делайте их глобаль-
ными только по необходимости Изначально сделайте все переменные локаль- ными по отношению к конкретным методам. Если выяснится, что они нужны еще где-то, сделайте их сначала закрытыми или защищенными переменными класса,
прежде чем вы решите сделать их глобальными. Если в конце концов выяснится,
что их придется сделать глобальными, сделайте, но только после того, как в этом убедитесь. Если вы с самого начала объявите переменную глобальной, вы никог- да не сделаете ее локальной, но если она сначала будет локальной, вам, возмож- но, не понадобится делать ее глобальной.
Различайте глобальные переменные и переменные-члены класса Некото- рые переменные действительно глобальны в том плане, что к ним обращаются из любого места программы. Другие — на самом деле классовые переменные —
интенсивно используются только некоторымо набором методов. Вполне нормально обращаться к классовой переменной из нескольких методов сколь угодно интен- сивно. Если методу вне класса нужно использовать эту переменную, предоставьте ее значение посредством метода доступа. Не обращайтесь к членам класса напря- мую (как если бы эти переменные были глобальными), даже если ваш язык про- граммирования это позволяет. Этот совет равносилен высказыванию «Модуляри- зируйте! Модуляризируйте! Модуляризируйте!».
Используйте методы доступа Создание методов доступа — основной под- ход для решения проблем с глобальными данными (см. следующий раздел).