В случаях, где нужен только эффект увеличения, а само значение не используется,
как, например, в
IF ( C == '\N' )
NL++;
выбор префиксной или постфиксной операции является делом вкуса. но встречаются
ситуации, где нужно использовать имен- но ту или другую операцию. Рассмотрим, например,
функцию SQUEEZE(S,C), которая удаляет символ 'с' из строки S, каждый раз, как он
встречается.
SQUEEZE(S,C) /* DELETE ALL C FROM S */ CHAR S[]; INT C; {
INT I, J;
FOR ( I = J = 0; S[I] != '\0'; I++)
IF ( S[I] != C )
S[J++] = S[I];
S[J] = '\0'; }
Каждый раз, как встечается символ, отличный от 'с', он копи- руется в текущую
позицию J, и только после этого J увеличи- вается на 1, чтобы быть готовым для поступления
следующего символа. Это в точности эквивалентно записи
IF ( S[I] != C ) {
S[J] = S[I];
J++;
}
Другой пример подобной конструкции дает функция GETLINE, которую мы запрограммировали
в главе 1, где можно заменить
IF ( C == '\N' ) {
S[I] = C;
++I; }
более компактной записью
IF ( C == '\N' )
S[I++] = C;
В качестве третьего примера рассмотрим функцию STRCAT(S,T), которая приписывает
строку т в конец строки S, образуя конкатенацию строк S и т. При этом предполагается,
что в S достаточно места для хранения полученной комбинации.
STRCAT(S,T) /* CONCATENATE T TO END OF S */
CHAR S[], T[]; /* S MUST BE BIG ENOUGH */
{
INT I, J;
I = J = 0;
WHILE (S[I] != '\0') / *FIND END OF S */
I++;
WHILE((S[I++] = T[J++]) != '\0') /*COPY T*/
;
}
Tак как из T в S копируется каждый символ, то для подготовки к следующему прохождению
цикла постфиксная операция ++ при- меняется к обеим переменным I и J.
Упражнение 2-3
Напишите другой вариант функции SQUEEZE(S1,S2), который удаляет из строки S1
каждый символ, совпадающий с каким-либо символом строки S2.
Упражнение 2-4
Напишите программу для функции ANY(S1,S2), которая нахо- дит место первого появления
в строке S1 какого-либо символа из строки S2 и, если строка S1 не содержит символов
строки S2, возвращает значение -1.
2.9. Побитовые логические операции
В языке предусмотрен ряд операций для работы с битами; эти операции нельзя применять
к переменным типа FLOAT или DOUBLE.
& Побитовое AND
\! Побитовое включающее OR
^ побитовое исключающее OR
<< сдвиг влево
>> сдвиг вправо
\^ дополнение (унарная операция)
"\" иммитирует вертикальную черту.
Побитовая операция AND часто используется для маскирования некоторого множества
битов; например, оператор
C = N & 0177 передает в 'с' семь младших битов N , полагая остальные рав- ными
нулю. Операция 'э' побитового OR используется для вклю- чения битов:
C = X э MASK
устанавливает на единицу те биты в х , которые равны единице в MASK.
Следует быть внимательным и отличать побитовые операции & и 'э' от логических
связок && и \!\! , Которые подразуме- вают вычисление значения истинности слева
направо. Например, если х=1, а Y=2, то значение х&Y равно нулю , в то время как
значение X&&Y равно единице./почему?/
Операции сдвига << и >> осуществляют соответственно сдвиг влево и вправо своего
левого операнда на число битовых позиций, задаваемых правым операндом. Таким образом
, х<<2 сдвигает х влево на две позиции, заполняя освобождающиеся биты нулями, что
эквивалентно умножению на 4. Сдвиг вправо величины без знака заполняет освобождающиеся
биты на некото- рых машинах, таких как PDP-11, заполняются содержанием зна- кового
бита /"арифметический сдвиг"/, а на других - нулем /"логический сдвиг"/.
Унарная операция \^ дает дополнение к целому; это озна- чает , что каждый бит
со значением 1 получает значение 0 и наоборот. Эта операция обычно оказывается полезной
в выраже- ниях типа
X & \^077
где последние шесть битов х маскируются нулем. Подчеркнем, что выражение X&\^077
не зависит от длины слова и поэтому предпочтительнее, чем, например, X&0177700,
где предполага- ется, что х занимает 16 битов. Такая переносимая форма не требует
никаких дополнительных затрат, поскольку \^077 явля- ется константным выражением
и, следовательно, обрабатывается во время компиляции.
Чтобы проиллюстрировать использование некоторых операций с битами, рассмотрим
функцию GETBITS(X,P,N), которая возвра- щает /сдвинутыми к правому краю/ начинающиеся
с позиции р поле переменной х длиной N битов. мы предполагаем , что крайний правый
бит имеет номер 0, и что N и р - разумно за- данные положительные числа. например,
GETBITS(х,4,3) возвра- щает сдвинутыми к правому краю биты, занимающие позиции 4,3
и 2.
GETBITS(X,P,N) /* GET N BITS FROM POSITION P */
UNSIGNED X, P, N;
{
RETURN((X >> (P+1-N)) & \^(\^0 << N));
}
Операция X >> (P+1-N) сдвигает желаемое поле в правый конец слова. Описание аргумента
X как UNSIGNED гарантирует, что при сдвиге вправо освобождающиеся биты будут заполняться
ну- лями, а не содержимым знакового бита, независимо от того, на какой машине пропускается
программа. Все биты константного выражения \^0 равны 1; сдвиг его на N позиций влево
с по- мощью операции \^0<<N создает маску с нулями в N крайних правых битах и единицами
в остальных; дополнение \^ создает маску с единицами в N крайних правых битах.
Упражнение 2-5
Переделайте GETBITS таким образом, чтобы биты отсчитыва- лись слева направо.
Упражнение 2-6
Напишите программу для функции WORDLENGTH(), вычисляющей длину слова используемой
машины, т.е. Число битов в перемен- ной типа INT. Функция должна быть переносимой,
т.е. Одна и та же исходная программа должна правильно работать на любой машине.
Упражнение 2-7
Напишите программу для функции RIGHTROT(N,B), сдвигающей циклически целое N вправо
на B битовых позиций.
Упражнение 2-8
Напишите программу для функции INVERT(X,P,N), которая инвертирует (т.е. Заменяет
1 на 0 и наоборот) N битов X, на- чинающихся с позиции P, оставляя другие биты неизмененными.
2.10. Операции и выражения присваивания
Такие выражения, как
I = I + 2
в которых левая часть повторяется в правой части могут быть записаны в сжатой
форме
I += 2
используя операцию присваивания вида +=.
Большинству бинарных операций (операций подобных +, ко- торые имеют левый и правый
операнд) соответствует операция присваивания вида оп=, где оп - одна из операций
+ - * / % << >> & \^ \!
Если е1 и е2 - выражения, то е1 оп= е2
эквивалентно
е1 = (е1) оп (е2)
за исключением того, что выражение е1 вычисляется только один раз. Обратите внимание
на круглые скобки вокруг е2:
X *= Y + 1
то
X = X * (Y + 1)
не
X = X * Y + 1
В качестве примера приведем функцию BITCOUNT, которая подсчитывает число равных
1 битов у целого аргумента.
BITCOUNT(N) /* COUNT 1 BITS IN N */ UNSIGNED N; ( INT B; FOR (B = 0; N != 0;
N >>= 1)
IF (N & 01)
B++; RETURN(B); )
Не говоря уже о краткости, такие операторы приваивания имеют то преимущество,
что они лучше соответствуют образу человеческого мышления. Мы говорим: "прибавить
2 к I" или "увеличить I на 2", но не "взять I, прибавить 2 и поместить результат
опять в I". Итак, I += 2. Кроме того, в громоздких выражениях, подобных
YYVAL[YYPV[P3+P4] + YYPV[P1+P2]] += 2
Tакая операция присваивания облегчает понимание программы, так как читатель не
должен скрупулезно проверять, являются ли два длинных выражения действительно одинаковыми,
или за- думываться, почему они не совпадают. Такая операция присваи- вания может
даже помочь компилятору получить более эффектив- ную программу.
Мы уже использовали тот факт, что операция присваивания имеет некоторое значение
и может входить в выражения; самый типичный пример
WHILE ((C = GETCHAR()) != EOF)
присваивания, использующие другие операции присваивания (+=, -= и т.д.) также
могут входить в выражения, хотя это случа- ется реже.
Типом выражения присваивания является тип его левого операнда.
Упражнение 2-9
В двоичной системе счисления операция X&(X-1) обнуляет самый правый равный 1
бит переменной X.(почему?) используйте это замечание для написания более быстрой
версии функции BITCOUNT.
2.11. Условные выражения
Операторы
IF (A > B)
Z = A;
ELSE
Z = B;
конечно вычисляют в Z максимум из а и в. Условное выражение, записанное с помощью
тернарной операции "?:", предоставляет другую возможность для записи этой и аналогичных
конструк- ций. В выражении
е1 ? Е2 : е3
сначала вычисляется выражение е1. Если оно отлично от нуля (истинно), то вычисляется
выражение е2, которое и становится значением условного выражения. В противном случае
вычисляет- ся е3, и оно становится значением условного выражения. Каж- дый раз вычисляется
только одно из выражения е2 и е3. Таким образом, чтобы положить Z равным максимуму
из а и в, можно написать
Z = (A > B) ? A : B; /* Z = MAX(A,B) */
Следует подчеркнуть, что условное выражение действитель- но является выражением
и может использоваться точно так же, как любое другое выражение. Если е2 и е3 имеют
разные типы, то тип результата определяется по правилам преобразования, рассмотренным
ранее в этой главе. например, если F имеет тип FLOAT, а N - тип INT, то выражение
(N > 0) ? F : N
Имеет тип DOUBLE независимо от того, положительно ли N или нет.
Так как уровень старшинства операции ?: очень низок, прямо над присваиванием,
то первое выражение в условном вы- ражении можно не заключать в круглые скобки.
Однако, мы все же рекомендуем это делать, так как скобки делают условную часть выражения
более заметной.
Использование условных выражений часто приводит к корот- ким программам. Например,
следующий ниже оператор цикла пе- чатает N элементов массива, по 10 в строке, разделяя
каждый столбец одним пробелом и заканчивая каждую строку (включая последнюю) одним
символом перевода строки.
OR (I = 0; I < N; I++)
PRINTF("%6D%C",A[I],(I%10==9 \!\! I==N-1) ? '\N' : ' ')
Символ перевода строки записывается после каждого десятого элемента и после N-го
элемента. За всеми остальными элемен- тами следует один пробел. Хотя, возможно,
это выглядит муд- реным, было бы поучительным попытаться записать это, не ис- пользуя
условного выражения.
Упражнение 2-10
Перепишите программу для функции LOWER, которая переводит прописные буквы в
строчные, используя вместо конструкции IF-ELSE условное выражение.
2.12. Старшинство и порядок вычисления
В приводимой ниже таблице сведены правила старшинства и ас- социативности всех
операций, включая и те, которые мы еще не обсуждали. Операции, расположенные в одной
строке, имеют один и тот же уровень старшинства; строки расположены в по- рядке
убывания старшинства. Так, например, операции *, / и % имеют одинаковый уровень
старшинства, который выше, чем уро- вень операций + и -.
OPERATOR ASSOCIATIVITY
() [] -> . LEFT TO RIGHT
! \^ ++ -- - (TYPE) * & SIZEOF RIGHT TO LEFT
* / % LEFT TO RIGHT
+ - LEFT TO RIGHT
<< >> LEFT TO RIGHT
< <= > >= LEFT TO RIGHT
== != LEFT TO RIGHT
& LEFT TO RIGHT
^ LEFT TO RIGHT
\! LEFT TO RIGHT
&& LEFT TO RIGHT
\!\! LEFT TO RIGHT
?: RIGHT TO LEFT
= += -= ETC. RIGHT TO LEFT
, (CHAPTER 3) LEFT TO RIGHT
Операции -> и . Используются для доступа к элементам струк- тур; они будут описаны
в главе 6 вместе с SIZEOF (размер объекта). В главе 5 обсуждаются операции * (косвенная
адре- сация) и & (адрес). Отметим, что уровень старшинства побитовых логических
опера- ций &, ^ и э ниже уровня операций == и !=. Это приводит к тому, что осуществляющие
побитовую проверку выражения, по- добные
IF ((X & MASK) == 0) ...
Для получения правильных результатов должны заключаться в круглые скобки.
Как уже отмечалось ранее, выражения, в которые входит одна из ассоциативных и
коммутативных операций (*, +, &, ^, э), могут перегруппировываться, даже если они
заключены в круглые скобки. В большинстве случаев это не приводит к ка- ким бы то
ни было расхождениям; в ситуациях, где такие рас- хождения все же возможны, для
обеспечения нужного порядка вычислений можно использовать явные промежуточные перемен-
ные.
В языке "C", как и в большинстве языков, не фиксируется порядок вычисления операндов
в операторе. Например в опера- торе вида
X = F() + G();
сначала может быть вычислено F, а потом G, и наоборот; поэ- тому, если либо F,
либо G изменяют внешнюю переменную, от которой зависит другой операнд, то значение
X может зависеть от порядка вычислений. Для обеспечения нужной последователь- ности
промежуточные результаты можно опять запоминать во временных переменных.
Подобным же образом не фиксируется порядок вычисления аргументов функции, так
что оператор
PRINTF("%D %D\N",++N,POWER(2,N));
может давать (и действительно дает) на разных машинах разные результаты в зависимости
от того, увеличивается ли N до или после обращения к функции POWER. Правильным решением,
конеч- но, является запись
++N; PRINTF("%D %D\N",N,POWER(2,N));
Обращения к функциям, вложенные операции присваивания, операции увеличения и
уменьшения приводят к так называемым "побочным эффектам" - некоторые переменные
изменяются как побочный результат вычисления выражений. В любом выражении, в котором
возникают побочные эффекты, могут существовать очень тонкие зависимости от порядка,
в котором определяются входящие в него переменные. примером типичной неудачной си-
туации является оператор
A[I] = I++;
Возникает вопрос, старое или новое значение I служит в ка- честве индекса. Компилятор
может поступать разными способами и в зависимости от своей интерпретации выдавать
разные ре- зультаты. Тот случай, когда происходят побочные эффекты (присваивание
фактическим переменным), - оставляется на ус- мотрение компилятора, так как наилучший
порядок сильно зави- сит от архитектуры машины.