CHAP5 (1018805), страница 3
Текст из файла (страница 3)
{
case CIRCLE: print_circle( p );
case LINE: print_line ( p );
case TEXT: print_text ( p );
}
}
на следующий:
typedef struct
{
void (*print)( struct *shape );
union shape_data;
{ // здесь данные для различных фигур.
}
}
shape;
extern void print_circle( shape *p );
extern void print_line ( shape *p );
extern void print_text ( shape *p );
shape a_circle = { print_circle, ... };
print_shape( shape *p )
{
( p->type )( p );
}
Главные преимущества такого подхода заключаются в следующем:
-
Вам больше не нужен перечислитель shape_type.
-
Функцию print_shape() теперь написать гораздо проще.
-
print_shape() будет продолжать работать без модификации, когда вы добавите новые фигуры в эту систему.
60. Избегайте циклов do/while.
Цикл do/while опасен в принципе, так как вы обязательно выполняете его тело хотя бы один раз. Следовательно, вы должны проверить условия завершения до входа в этот цикл. Я часто вижу код, похожий на следующий:
if ( !проверить_нечто )
return ERROR;
do
{
начинка();
} while ( проверить_нечто );
Вам гораздо лучше сделать так:
while ( проверить_нечто )
начинка();
Похожий случай:
if ( некое_условие() )
do
// масса материала
while ( некое_условие() && другой_материал() );
легче трактовать следующим образом:
while ( некое_условие() )
{
// масса материала
if ( !другой_материал() )
break;
}
Я профессионально занимаюсь программированием с 1979 года и за это время использовал цикл do/while всего два раза.
60.1. Никогда не используйте do/while для бесконечного цикла.
Код, похожий на следующий:
do
{
// здесь следует несколько страниц кода
while ( 1 );
просто заставляет сопровождающего программиста шарить по листингу взглядом, ища while, вместо того, чтобы найти его сразу в случае расположения оператор while (1) в начале цикла.
61. В цикле со счетчиком его значение должно по возможности уменьшаться.
Циклы являются одним из тех мест, где малое повышение эффективности значительно улучшает выполнение программы, потому что их код выполняется многократно. Так как сравнение с нулем обычно более эффективно, чем сравнение с определенным числом, то цикл с уменьшающимся счетчиком, как правило, выполняется быстрее. Используйте
for ( i = max; --i >= 0; )
;
вместо:
for ( i = 0; i < max; ++i )
;
Заметьте, что в обоих случаях счетчики могут быть использованы в качестве действительного индекса массива, что может сделать этот код менее подверженным ошибкам вследствие того, что он ограничивает доступ к массиву его границами.
62. Не делайте одно и то же двумя способами одновременно.
В качестве контрапункта к предыдущему правилу рассмотрим следующий фрагмент (содержащий в себе ошибку):
int array[ARRAY_SIZE];
int *p = array;
for ( i = 1; i < ARRAY_SIZE ; ++i )
*p++ = 0;
Проблема состоит в том, что счетчик не совпадает по фазе с указателем (i имеет значение 1, когда указатель указывает на элемент array[0]), и последний элемент массива не будет инициализирован.
Я обычно предпочитаю для простых перемещений по массивам указатели (вместо индексов массива), потому что указатели, как правило, более эффективны, устраняя неявную операцию умножения в выражении a[i], интерпретируемом как:
( a + ( i* sizeof(a[0])))
Я бы переписал это код таким образом:
int array[ARRAY_SIZE];
int *current = array;
int *const end = array + (SIZE-1);
while ( current <= end )
*current++ = 0;
Так же надежно (хотя и менее эффективно) сделать следующее:
int array[ARRAY_SIZE];
int i;
for ( i = 0; i < ARRAY_SIZE ; ++i )
array[i] = 0;
Кстати, если вы используете указатели, то вам придется извлекать индекс при помощи арифметики указателей, а не за счет сохранения второй переменной. У вас могут возникнуть проблемы, если вы передадите i функции в предыдущем примере с ошибкой. Воспользуйтесь подобным кодом:
for ( current = array; current <= end; ++current )
{
// ...
f( current - array ); // передать функции f() текущий индекс массива
}
С другой стороны, обычно нужно избегать кода, подобного следующему, так как такой оператор цикла чрезвычайно неэффективен:
while ( (current - array) < ARRAY_SIZE )
// ...
63. Используйте оператор for, если имеются любые два из инициализурующего, условного или инкрементирующего выражений.
Иначе используйте while. Такой код:
int x = 10;
// далее следует 200 строк кода, в которых переменная x не используется
while ( x > 0 )
{
// снова следует 200 строк кода
f( x-- );
}
не очень хорош, даже если вы сэкономили немного времени, соединив операцию декрементирования -- с вызовом функции. Переместите инициализацию и x-- в оператор for. Так как объявление в С++ может располагаться везде, где можно поместить оператор, то вы даже можете объявить x непосредственно перед for:
int x = 10;
for ( ; x > 0 ; --x )
{
// следует 200 строк кода
f(x);
}
(И хотя вы можете сказать, что в С++ есть возможность сделать for ( int=0;..., такая практика вводит в заблуждение, потому что на самом деле область видимости у x внешняя, как если бы ее объявление было сделано в строке, предшествующей оператору for. Я не рекомендую этого).
Если три оператора внутри for слишком длинны, чтобы поместиться в одной строке, то вы можете их отформатировать их следующим образом:
for ( некое_длинное_имя_переменной = f();
некое_длинное_имя_переменной ;
некое_длинное_имя_переменной = f() )
{
// ...
}
но часто лучше выделить одно из предложений вот так:
int некое_длинное_имя_переменной = f();
for ( ; некое_длинное_имя_переменной ; некое_длинное_имя_переменной = f() )
{
// ...
}
или в чрезвычайном случае
int некое_чрезвычайно_длинное_имя_переменной = f();
for ( ; ; некое_чрезвычайно_длинное_имя_переменной = f() )
{
if ( !некое_чрезвычайно_длинное_имя_переменной )
break;
// ...
}
Главное - это сосредоточить инициализацию, проверку и инкрементирование в одном месте. Я никогда не сделаю так:
int некое_чрезвычайно_длинное_имя_переменной = f();
while ( некое_чрезвычайно_длинное_имя_переменной )
{
// много строк кода
некое_чрезвычайно_длинное_имя_переменной = f();
}
потому что это нарушает контроль над циклом.
64. То, чего нет в условном выражении, не должно появляться и в других частях оператора for.
Так как оператор for предназначен для того, чтобы собрать инициализирующую, условную и инкрементирующие части цикла в одном месте, так чтобы вы могли, взглянув, понять, что происходит, то вы не должны загромождать оператор for материалом, который не имеет отношения к циклу, иначе вы лишите смысла всю эту конструкцию. Избегайте подобного кода:
int *ptr;
// ...
for ( ptr = array, i = array_size; --i >= 0; f(ptr++) )
;
который лучше сформулировать так:
int *ptr = array;
for ( i = array_size; --i >= 0 ; )
f( ptr++ );
65. Допускайте, что ситуация может измениться в худшую сторону.
Одним из лучших примеров этой проблемы связан со "знаковым расширением". Большинство компьютеров используют так называемую арифметику "двоичного дополнения". Крайний левый бит отрицательного числа в этой арифметике всегда содержит 1. Например, восьмибитовый тип char со знаком, содержащий число -10, будет представлен в машине с двоичным дополнением как 11110110 (или 0xf6). То же самое число в 16-битовом типе int представлено как 0xfff6. Если вы преобразуете 8-битовый char в int явно посредством оператора приведения типов или неявно, просто используя его в арифметическом выражении, где второй операнд имеет тип int, то компилятор преобразует char в int, добавляя второй байт и дублируя знаковый бит (крайний слева) char в каждом бите добавленного байта. Это и есть знаковое расширение.
Существуют два вида операций сдвига вправо на уровне языка ассемблера: "арифметический сдвиг" дает вам знаковое расширение (то значение, что было в крайнем левом бите до сдвига, будет в нем и после сдвига); "логический сдвиг" заполняет левый бит нулем. Данное правило, независимо от того, арифметический или логический сдвиг у вас, когда вы используете оператор сдвига С/С++, звучит очень просто: если вам требуется знаковое расширение, то допустите, что у вас получится заполнение нулями. А если вам нужно заполнение нулями, то представьте, что у вас получилось знаковое расширение.
Другим хорошим примером являются возвращаемые коды ошибок. Удивительное количество программистов не заботится о проверке того, не вернула ли функция malloc() значение NULL при недостатке свободной памяти. Быть может, они полагают, что имеется бесконечный объем виртуальной памяти, но известно, что ошибка может с легкостью вызвать резервирование всей имеющейся памяти, и вы никогда не обнаружите этого, если не будете проверять возвращаемые коды ошибок. Если функция может индицировать состояние ошибки, то вы должны допустить, что ошибка произойдет по меньшей мере однажды за время жизни программы.
66. Компьютеры не знают математики.
Компьютеры - это арифметические инструменты, славные счетные машины. Они не знают математики. Поэтому даже такие простые выражения, как следующее, могут добавить вам хлопот:
int x = 32767;
x = (x * 2) / 2;
(На 16-разрядной машине x получится равным -1. 32767 - это 0x7fff. Умножение на 2 - на самом деле сдвиг влево на один бит, дает в результате 0xfffe - отрицательное число. Деление на два является арифметическим сдвигом вправо с гарантированным знаковым расширением, и так вы получаете теперь 0xffff или -1). Поэтому важно каждый раз при выполнении арифметических вычислений учитывать ограничения компьютерной системы. Если вы производите умножение перед делением, то при этом рискуете выйти за пределы разрядности при сохранении результата; если вы сначала делите, то рискуете случайно округлить результат до нуля; и так далее. Численным методам для компьютеров посвящены целые книги, и вам нужно прочитать хотя бы одну из них, если в ваших программах много математики.
Вы также должны знать мелкие грехи своего языка программирования. В С, например, преобразование типов выполняется по принципу "оператор за оператором". Я однажды потратил утро, пытаясь разобраться, почему следующий код ничего не делает:
long x;
x &= 0xffff; // очистить все, кроме младших 16-ти бит 32-битного типа long.
Компьютер имел 16-битовый тип int и 32-битовый тип long. Константа 0xffff типа int с арифметическим значением -1. Компилятор С при трансляции &= обнаруживал разные типы операндов и поэтому преобразовывал int в long. -1 в типе long представляется как 0xffffffff, поэтому логическая операция И не имела эффекта. Это как раз тот способ, которым и должен работать данный язык программирования. Я просто об этом не подумал.
Заметьте, что вы не можете исправить эту ситуацию приведением типов. Все, что делает следующий код, это заменяет неявное преобразование типа явным. Но тем не менее происходит то же самое:
x &= (long)0xffff;
Единственным методом решения этой проблемы является:
x &= 0xffffUL;
или равноценный ему.
66.1. Рассчитывайте на невозможное.
Оператор switch всегда должен иметь предложение с ключевым словом default для ситуации по умолчанию, особенно если эта ситуация не должна возникать:
f( int i ) // переменная i должна иметь значение 1 или 2.















