Жанр: Учеба
Правила программирования на Си и Си плюс плюс.
...сто необходимость в явном сравнении на истину или ложь можно
Правила, от носящиеся к языку Си 125
устранить при помощи переименования:
if
( я_сонливый(p) )
значительно лучше, чем:
if
( я_сонливый(p) != FALSE )
Так как определения TRUE и FALSE спрятаны в макросах, то хороший
сопровождающий программист не может делать каких-либо
предположений об их действительных значениях. Например, FALSE
может быть -1, а TRUE - 0. И следственно, если функция возвращает в
явном виде TRUE или FALSE, то наш прилежный сопровождающий
программист должен будет потратить несколько дней, чтобы убедиться,
что при проверке возвращаемого значения для каждого вызова
используется явная проверка на равенство TRUE или FALSE (сравните для
примера с простым логическим отрицанием ! перед вызовом).
Следующий фрагмент:
if
( я_сердитый() )
более не может удовлетворять, так как компилятор ожидает, что ложь
обозначается 0.
И напоследок - имейте в виду, что следующий вариант не будет
работать:
#define FALSE 0
#define TRUE !FALSE
Операция !, подобно всем операторам отношений, преобразует операнд в
1
, если он имеет значение "истина" (отличен от нуля), и 0, если наоборот.
Предыдущий вариант идентичен следующему:
#define FALSE 0
#define TRUE 1
Вот более надежный, но нелепый вариант:
#define IS_TRUE(x) ((x) == 0)
#define IS_FALSE(x) ((x) != 0)
Правила программирования на Си и Си++
86. Для битового поля размером 1 бит должен быть
определен тип unsigned
После того, как ANSI Си позволил назначать битовому полю знаковый
тип, мне доводилось видеть код, подобный:
struct fred
{
int i : 1;
}
a_fred;
Возможными значениями являются 0 и -1. Оператор типа:
#define TRUE 1
// ...
if
( a_fred.i == TRUE )
// ...
не будет работать, потому что поле a_fred.i может иметь значение 0
или -1, но оно никогда не будет равняться 1. Следовательно, оператор if
никогда не выполняется.
87. Указатели должны указывать на адрес, больший,
чем базовый для массива
Это правило подтверждено стандартом ANSI Си, но многие
программисты, похоже, не подозревают о том способе, которым язык
должен работать. ANSI Си говорит, что указатель может переходить на
ячейку, следующую после окончания массива, но он не может иметь
величину меньше, чем базовый адрес массива. Нарушение этого правила
может прервать программу, которую пытаются выполнить, например, в
сегментной модели памяти процессоров 80x86. Следующий код не будет
работать:
int array[ SIZE ];
int *p = array + SIZE; // Здесь все в порядке; вы можете
// двигаться дальше.
while ( --p "= array ) // Это не работает - возможен
// бесконечный цикл.
//...
Проблема состоит в том, что при сегментной архитектуре есть
возможность того, что массив попадет на начало сегмента и получит
исполнительный адрес 0x0000. (В архитектуре 8086 это будет
смещением - частью адреса любого байта, состоящего из адреса
сегмента и смещения). Если p установлен на начало массива (0x0000), то
Правила, от носящиеся к языку Си 127
операция --p вызывает его перемещение на адрес 0xfffe (если у типа
int
размер 2 байта), который считается большим, чем p. Другими
словами, предыдущий цикл никогда не закончится. Исправьте эту
ситуацию следующим образом:
while ( --p "= array )
{
// ...
if ( p == array )
break
;
}
Вы можете выйти из положения так:
int *p = array + (SIZE - 1);
do
{
// ...
} while ( p-- " array );
но позаботьтесь, чтобы p был внутри массива перед началом цикла.
(Указатель должен быть инициализирован значением p+(SIZE-1), а не
p+SIZE
).
88. Используйте указатели вместо индексов массива
Вообще, инкрементирование указателя - лучший способ перемещения
по массиву, чем индекс массива. Например, простой цикл, подобный
следующему, страшно неэффективен:
struct thing
{
int field;
int another_field;
int another_field;
};
thing array[ nrows ][ ncols ];
int row, col;
for ( row = 0; row " nrows ; ++nrows )
for ( col = 0; col " ncols; ++cols )
array[row][col].field = 0;
Выражение array[row][col] требует двух умножений и одного
сложения во время выполнения. Вот что происходит на самом деле:
array + (row * size_of_one_row) + (col * size_of_a_thing)
Каждая структура имеет размер 12 байтов, и 12 не является степенью 2,
Правила программирования на Си и Си++
поэтому вместо умножения нельзя использовать более эффективный
сдвиг.
Вы можете сделать то же самое посредством указателей следующим
образом:
thing *p = (thing *)array;
int n_cells = nrows * ncols;
while ( --n_cells "= 0 )
(p++)-"field = 0;
При этом здесь вообще нет умножения во время выполнения. Оператор
инкрементирования p++ просто прибавляет 12 к p.
С другой стороны, указатель лучше только тогда, когда вы можете его
инкрементировать, то есть когда вы обращаетесь к последовательным
элементам. Если вам нужен по настоящему случайный доступ в массив, то
запись с квадратными скобками намного проще читается, и разницы в
скорости выполнения нет.
Аналогично, если внутренняя часть цикла в принципе неэффективна -
скажем, например, мы сделали следующее:
for ( row = 0; row " nrows ; ++nrows )
for ( col = 0; col " ncols ; ++cols )
f( array[row][col] );
и f() требует для выполнения две секунды - тогда относительный
выигрыш от использования указателей будет существенно перевешен
накладными расходами на вызов функции, и, естественно, вы можете
утверждать, что квадратные скобки легче читаются. Конечно, если f()
является встроенной функцией Си++, то накладные расходы на вызов
функции могут быть минимальными и есть смысл использовать указатель,
поэтому вы можете возразить, что вариант с указателем лучше, ибо
накладные расходы тяжело определить.
Наконец, верно, что оптимизатор часто может преобразовать вариант
цикла с индексами массива в вариант с указателями, но я думаю, что это
плохой стиль - писать неэффективный код в надежде на то, что
оптимизатор очистит его после вас. Указатели так же хорошо читаемы,
как и индексы массивов, для того, кто знает язык программирования.
89. Избегайте goto, за исключением!
Правила в этом разделе применяйте только к программам на Си.
Оператор goto не должен никогда использоваться в Си++ по причинам,
рассмотренным в правиле 54 - существует вероятность того, что
конструкторы и деструкторы будет невозможно вызвать.
Вообще вы должны избегать оператора goto не потому, что goto -
Правила, от носящиеся к языку Си 129
унаследованный порок, а потому что существуют лучшие решения. Язык
Си, например, дает вам массу отличающихся от goto способов выхода из
циклов.
Оператор goto может также ухудшать читаемость. Я на самом деле
видел код, подобный нижеследующему, и чтобы разобраться, как он
работает, потребовалось полчаса:
while ( 1 )
{
while ( условие )
{
// ...
while ( другое_условие )
{
метка1:
// ...
goto метка2;
}
// ...
}
if ( третье_условие )
{
// ...
if ( другое_условие )
goto метка1;
else
{
метка2:
// ...
}
}
}
Но самое интересное, что после того, как я разобрался с этим, стало легко
переписать его, исключив переходы goto.
Проблема читаемости все же сохраняется, даже если goto в явном
виде отсутствует. Оператор switch, например, неявно выполняет goto
для перехода к оператору case. Последующий пример вполне законен с
точки зрения Си, но я не стал бы его вам рекомендовать:
switch
( некоторое_условие )
{
case A: if ( некоторое_другое_условие )
// ...
else
{
case b: // ...
}
}
Правила программирования на Си и Си++
Оператор goto полезен в некоторых случаях. Вот два из них:
+ Множество переходов goto к единственной метке, стоящей перед
оператором return, лучше, чем множество операторов return.
Такую процедуру легче отлаживать, так как для перехвата выхода из
нее вы можете установить единственную точку прерывания. Имейте в
виду, что метка должна предшествовать оператору; она не может
стоять перед закрывающей фигурной скобкой. При необходимости
пользуйтесь следующим приемом:
// ...
exit:
return ;
}
+ Переходы goto вниз по программе, обеспечивающие выход из
системы вложенных циклов, лучше, чем флаг завершения типа "готов",
который должен проверяться в каждом операторе управления циклом.
Если каждый из операторов while в следующем примере выполнить
по 100 раз, то флаг "готов" нужно проверить 1000000 раз, хотя он
установлен всего лишь на случай ошибки
int готов = 0;
int условие1, условие2, условие3;
// ...
while ( !
готов && условие1 )
{
while ( !
готов && условие2 )
{
while ( !
готов && условие3 )
{
if ( нечто_ужасное )
готов = 1;
}
}
}
Исключите миллионы ненужных проверок при помощи goto следующим
образом:
while ( условие1 )
{
while ( условие2 )
{
while ( условие3 )
{
if ( нечто_ужасное )
goto выход;
}
Правила, от носящиеся к языку Си 131
}
}
выход:
// ...
Проверка в операторе управления циклом - единственное место, где
эффективность действительно является важным обстоятельством, потому
что код выполняется многократно. Это особенно верно для внутренних
операторов управления вложенных циклов. Проверка флага завершения
во внутреннем цикле может существенно замедлить выполнение, и ее
лучше избегать.
Часть
Правила программирования на Си++
Эта часть книги содержит правила, уникальные для программирования на
Си++. Как мной было сказано во "Введении", эта книга не является
учебником по Си++, так что следующие правила предполагают, что вы по
крайней мере знакомы с синтаксисом этого языка. Я не буду тратить
слова попусту, описывая, как работает Си++. Имеется множество
хороших книг, которые познакомят вас с Си++, включая и мою
собственную "С+С++". Вы должны также ознакомиться с принципами
объектно-ориентированного проектирования. Я рекомендую 2-е издание
книги Гради Буча "Object-Oriented Analysis and Design with Applications"
(Redwood City: Benjamin Cummings, 1994).
Так же, как и в книге в целом, правила вначале адресуются к общим
вопросам, переходя затем к частностям.
Вопросы проект ирования и реализации 133
и реализации
90. Не смешивайте объектно-ориентированное и
"структурное" проектирование
90.1. Если проект не ориетирован на объекты, то
используйте Си
Позвольте мне начать, сказав, что нет абсолютно ничего дурного в
хорошо выполненном структурном проектировании. Как-то так
получилось, что я предпочитаю объектно-ориентированный (ОО) подход,
ибо мне кажется, что я мыслю ОО способом, но было бы самонадеянным
назвать ОО проектирование "лучшим". Я верю, что ОО подход дает вам
легче сопровождаемый код, если программа большая. Выгода менее явна
в случае программ меньшего размера, потому что объектная ориентация
обычно добавляет сложность на уровне приложения. (Главная выгода ОО
заключается в лучшем сопровождении за счет абстракции данных, а не в
сокращении сложности).
Си++ особенно не выносит небрежного проектирования. Мой опыт
говорит, что программы на Си++, которые не придерживаются объектноориентированного
подхода, почти несопровождаемы, соединяя все
худшие свойства структурного и объектно-ориентированного проектов и
не давая каких-либо выгод как ни от того, так и ни от другого. Со мной не
проходит такой аргумент, что можно использовать Си++ как
"улучшенный" Си. Для того, чтобы это было правдой, этот язык слишком
сложный - кривая обучения слишком крутая. Если вы не используете
преимущества объектно-ориентированных свойств этого языка, то в его
использовании мало смысла. Некорректное использование объектноориентированных
свойств лишь увеличит число проблем.
К сожалению, многие программисты знают, как сделать объектноориентированный
проект, но на самом деле этого не делают. Оправдания
варьируются в пределах от "слишком много хлопот (или у меня нет
времени), чтобы делать все правильно" до "строгий объектноориентированный
проект - это учебное упражнение: на него нет времени
в реальной жизни, где вы вынуждены работать быстро и не очень чисто".
Возможно, что наиболее возмутительным оправданием, слышанным мной
по поводу плохого проекта (в этом случае библиотеки классов), было
Правила программирования на Си++
следующее: "Недостаточное число наших заказчиков знают Си++
достаточно хорошо, чтобы его правильно использовать, поэтому мы
спроектировали библиотеку классов так, чтобы ей было легче
пользоваться". (В переводе на нормальный язык: "Средние пользователи
слишком тупые, чтобы делать все правильно; на самом деле они даже не
заинтересованы в том, чтобы научиться работать правильно, и научить их
будет очень трудно. Так что мы даже не будем делать ни того, ни другого.
Мы просто оглупим свой продукт"). Проблема была отягощена учебным
руководством, которое нарушало объектно-ориентированные принципы
налево и направо, и, к сожалению, это руководство используется
тысячами программистов, которые не знают ничего лучшего в качестве
примера того, как написать приложение при помощи этой библиотеки
классов. Они вполне разумно ожидают, что руководство покажет им, как
сделать все правильно, поэтому они никогда не подозревают, что все
было намеренно сделано неверно, чтобы сделать руководство "более
понятным".
Си++ - язык трудный как для изучения, так и для использования. При
написании программ на Си++ столько тонкостей, что даже опытные
программисты временами их забывают. Кроме того, даже простой поиск
достаточного количества программистов на Си++, чтобы писать, и
значительно меньшего, чтобы сопровождать ваш код - трудный процесс.
Вы вводите себя в заблуждение, если верите, что Си++ может быть
использован безыскусно. Слишком просто для неопытного программиста
сделать что-нибудь неправильно и даже не знать об этом, способствуя
бесполезным затратам времени на выслеживание ошибки, которая и так
хорошо видна. Многие ошибки такого типа даже проникают
необнаруженными через этап тестирования и попадают в конечный
продукт, делая сопровождение сомнительным предприятием.
Зачем же вообще использовать Си++? Ответ состоит в том, что
должным образом использованный Си++ дает вам существенные выгоды
в сопровождении. Вы можете делать значительные изменения в
поведении программы (типа перевода всей программы с английского
языка на японский или переноса в другую операционную среду) при
помощи незначительных изменений в исходном коде, ограниченных
малым уголком этого кода. Подобные изменения в структурной системе
обычно потребуют модификации поистине каждой функции в программе.
Однако если вы не придерживаетесь правил, то вы в итоге получите в
свое распоряжение недостатки обоих систем. Структурные проекты
обычно имеют проблемы с отношениями сцепления, которые не
встречаются в хорошем объектно-ориентированном проекте, но если вы
Вопросы проект ирования и реализации 135
остановитесь на полдороге, многие из этих ошибок будут скрыты в
классах, где их будет трудно найти. Кроме того, многие объектноориентированные
проекты обычно бывают очень сложными, а
взаимоотношения между объектами иногда непредсказуемы. (Это также
одно из главных преимуществ методологии: возможно моделирование
системы такой сложности, что ее поведение заранее предсказать
невозможно). Инструменты типа диаграмм объектов становятся
необходимыми, потому что если эта система не работает, то вероятнее
всего причина в потоке сообщений. Если не работает индивидуальный
объект, то его легко исправить при условии, что его интерфейс корректен,
потому что изменения будут ограничены определением одного класса.
Когда вы делаете что-то неверно, то эти проблемы становится очень
тяжело выследить, потому что для передачи информации используются
тайные ходы, а изменения в одном классе могут передаваться в другие.
Поэтому, если у вас "нет времени, чтобы делать все правильно", то вам
гораздо лучше остановиться на структурном проектировании и простой
реализации на языке Си. Будет проще искать ошибки, потому что код
более однороден, и у вас не будет дополнительных сложностей с
системой передачи сообщений, сбивающей с толку. Введение упрощений
сегодня может сделать программу "типа объектно-ориентированной"
неподдающейся сопровождению год спустя: вам придется выбросить всю
программу и начать сначала. Перспектива лучшего сопровождения может
реализоваться, лишь если вы следуете правилам.
Так как эта книга не является книгой по ОО-проектированию, то я
отсылаю вас к книге Буча, упомянутой во введении к этой главе, если вам
нужно познакомиться с процессом объектно-ориентированного
проектирования. Эта книга рассматривает правила, которые облегчают
протекание этого процесса.
91. Рассчитывайте потратить больше времени на
проектирование и меньше на разработку
Мой опыт свидетельствует, что, если исключить период изучения Си++,
объектно-ориентированные системы требуют на разработку столько же
времени, сколько и структурные системы. Тем не менее, при объектноориентированном
подходе вы затрачиваете гораздо более высокую долю
общего времени на проектирование, и процесс программирования идет
быстрее. На практике этап проектирования большой системы может
продолжаться от четырех до шести месяцев, прежде чем будет написана
первая строка кода. К несчастью, это слишком горькая пилюля для тех,
кто измеряет производительность числом строк кода в день, чтобы
Правила программирования на Си++
проглотить ее. Так как общее время разработки остается прежним, то рост
производительности происходит после того, как начинается
сопровождение кода. Корректно выполненные объектнопроектированные
системы проще кодировать и проще сопровождать.
92. Библиотеки классов Си++ обычно не могут быть
использованы неискушенными пользователями
Одна большая ложь о Си++, которая распространяется продавцами с
острыми зубами и зачесанными назад волосами, сводится к тому, что
ваши второсортные программисты на Си могут использовать библиотеки
классов, созданные гуру, без реальной необходимости знать Си++. К
несчастью, любая, кроме самой тривиальной, библиотека классов будет
использовать сложные для понимания разделы Си++ типа наследования и
виртуальных функций - по крайней мере, она должна их использовать,
если спроектирована как следует. Библиотека, не использующая эти
свойства Си++, могла бы запросто быть реализована на Си. Пользователи
библиотеки будут должны довольно хорошо знать Си++.
Любая программа, написанная людьми, которые не слишком сведущи в
используемом ими языке программирования, будет в лучшем случае
иметь много ошибок, в худшем случае она будет несопровождаемой.
Вероятно, тяжелее всего найти ту ошибку, про которую вы не думаете,
что это ошибка. Если ваше понимание того, как работает этот язык,
неполное, то вы можете думать, что все в порядке с фрагментом кода,
патологически напичканным ошибками, потому что этот код внешне
кажется правильным.
Программная индустрия сталкивалась с этой проблемой и раньше,
когда коллективы разработчиков были вынуждены переходить с языка
КОБОЛ на Си, но при этом не была обеспечена необходимая тренировка
программистов, позволяющая им использовать Си правильно. После этого
в качестве урока осталась масса несопровождаемого, переполненного
ошибками кода на Си. Си++ показывает все признаки еще более
серьезной проблемы, так как руководители часто делают ставку на
популярность Си++, в действительности не зная, во что они впутываются.
Масса кишащего ошибками кода на Си++ пишется ежедневно людьми,
которые даже не знают язык в степени, достаточной, чтобы понять, что
они делают что-то неправильно.
Вопросы проект ирования и реализации 137
93. Пользуйтесь контрольными таблицами
Одной из причин того, что Си++ имеет такую крутую кривую обучения,
заключается в том, что вы должны отслеживать большое количество
деталей, чтобы выполнить даже простые задачи. Просто забыть что-то,
даже если вы это сделаете не надолго. Я решаю эту проблему, применяя
повсюду несколько образцовых шаблонных файлов - по одному для
каждой распространенной ситуации. (У меня есть один для определения
базового класса, один - для определения производного класса, и т.д.). Я
начинаю с копирования соответствующего шаблона в свой текущий
рабочий файл и затем использую возможности своего редактора по
поиску и замене для заполнения пустот. Я также перемещаю подходящие
функции в файлы .cpp, когда нужно, и т.п.. Листинги 5 и 6 показывают
простые шаблонные (в смысле естественного языка, а не языка С++)
файлы для базового и производного классов (где кое-что опущено по
сравнению с теми, которыми я пользуюсь на самом деле, но идею вы
поняли).
Листинг 5. base.tem - контрольная таблица для определения базового
класса
1 class base
2 {
3 cls obj;
4 public
:
5 virtual
6 ~base ( void );
7 base ( void );
8 base ( const base &r );
10 const base &operator=( const base &r );
11 private:
12 };
13 //------------------------------------------------------
14 /* виртуальный */ base:: ~base( void )
15 {
16 }
17 //------------------------------------------------------
18 inline base::base( void ) : obj( value )
19 {
20 }
21 /-------------------------------------------------------
22 inline base::base( const base &r ) : obj( r.obj )
23 {}
24 //------------------------------------------------------
25 inline const base& base::operator=( const base &r )
26 {
Правила программирования на Си++
27 if
( this != &r )
28 {
29 obj = r.obj;
30 }
31 return *this;
32 }
Листинг 6. derived.tem - контрольная таблица для определения
производного класса
1 class derived : public base
2 {
3 cls obj;
4 public
:
5 virtual
6 ~derived ( void );
7 derived ( void );
8 derived ( const derived& r );
10 const derived &operator=( const derived &r );
12 private:
13 };
14 //------------------------------------------------------
15 /* виртуальный */ derived:: ~derived( void )
16 {
17 }
18 //------------------------------------------------------
19 inline derived::derived( void ) : base( value ) ,
20 obj( value )
21 {
22 }
23 //------------------------------------------------------
24 inline derived::derived( const derived &r ) : base ( r ),
25 obj( r.obj )
26 {}
27 //------------------------------------------------------
28 inline const derived& derived::operator=( const derived &r )
29 {
30 if
( this != &r )
31 {
32 *((base *)this) = r;
33 obj = r.obj;
34 }
35 return *this;
36 }
Вопросы проект ирования и реализации 139
94. Сообщения должны выражать возможности, а не
запрашивать информацию
Объектно-ориентированные и структурные системы склонны подходить к
проблемам с диаметрально противоположных направлений. Возьмите в
качестве примера скромную запись employee. В структурных системах
вы бы использовали тип struct и имели бы доступ к полям этого типа
повсюду из своей программы. Например, код для печати записи мог бы
свободно повторяться в нескольких сотнях мест программы. Если вы
меняете что-то в основе, вроде изменения типа поля name с массива char
на 16-битные символы Unicode, то вы должны разыскать каждую ссылку
на name и модифицировать ее для работы с новым типом.
В хорошо спроектированной объектно-ориентированной системе было
бы невозможно получить доступ к полю name.4 Позвольте мне повторить
это, потому что эта концепция так фундаментальна: невозможно получить
доступ к полю внутри объекта, даже такому простому, как name в объекте
employee
. Скорее всего вы попросите employee проявить какую-нибудь
способность, такую как "напечатать себя", "сохранить себя в базе
данных" или "модифицировать себя, взаимодействуя с пользователем". В
этом последнем случае обработчик сообщ
...Поділитися у соцмережах