2.9. Побитовые операторы
В Си имеются шесть операторов для манипулирования с битами. Их можно применять только к целочисленным операндам, т. е. к операндам типов int и long, знаковым и беззнаковым.
& - побитовое И.
i - побитовое ИЛИ.
- побитовое исключающее ИЛИ.
- сдвиг влево.
- сдвиг вправо.
- побитовое отрицание (унарный).
Оператор & (побитовое И) часто используется для обнуления некото- рой группы разрядов. Например n = n & 0177;
обнуляет в n все разряды, кроме младших семи.
2.9. Побитовые операторы
Оператор ! (побитовое ИЛИ) применяют для установки разрядов;
х = х SET_ON;
устанавливает единицы в тех разрядах х, которым соответствуют едини- цы в
Оператор (побитовое исключающее ИЛИ) в каждом разряде устано- вит если соответствующие разряды операндов имеют различные значе- ния, и 0, когда они совпадают.
Поразрядные операторы
! следует отличать от логических операто- ров && и которые при вычислении слева направо дают значение истин- ности. Например, если х равно а у равно 2, то х & у даст нуль, а х у - единицу.
Операторы и
сдвигают влево или вправо свой левый операнд на число битовых позиций, задаваемое правым операндом, который должен быть неотрицательным. Так, х
2 сдвигает значение х влево на 2 пози- заполняя освобождающиеся биты нулями, что эквивалентно умно- жению х на 4. Сдвиг вправо беззнаковой величины всегда сопровождает- ся заполнением освобождающихся разрядов нулями. Сдвиг вправо зна- ковой величины на одних машинах происходит с распространением зна- ка ("арифметический сдвиг"), на других - с заполнением освобождаю- щихся разрядов нулями ("логический сдвиг").
Унарный оператор поразрядно "обращает" целое т. е. превращает каждый единичный бит в нулевой и наоборот. Например х = х &
обнуляет в х последние 6 разрядов. Заметим, что запись х & 077 не зави- сит от длины слова, и, следовательно, она лучше, чем х & 0177700, по- скольку последняя подразумевает, что х занимает битов. Не зависимая от машины форма записи -077 не потребует дополнительных затрат при счете, так как
- константное выражение, которое будет вычислено во время компиляции.
Для иллюстрации некоторых побитовых операций рассмотрим функ- цию getbits(x, p, n), которая формирует поле в битов, вырезанных из х,
начиная с позиции р, прижимая его к правому краю. Предполагается, что
0-й бит - крайний правый бит, а п и р - осмысленные положительные числа. Например, getbits(x, 4, 3) вернет в качестве результата 4, 3 и 2-й биты значения х, прижимая их к правому краю. Вот эта функция:
/* getbits: получает п бит, начиная с р-й позиции */
unsigned х, int p, int n)
{
return (x
&
n);
72 Глава 2. Типы, операторы и выражения
Выражение х сдвигает нужное нам поле к правому краю. Кон- станта 0 состоит из одних единиц, и ее сдвиг влево на п бит п)
приведет к тому, что правый край этой константы займут п нулевых раз- рядов. Еще одна операция побитовой инверсии позволяет получить справа п единиц.
Упражнение 2.6. Напишите функцию p,
возвращающую значение х, в котором п битов, начиная с р-й позиции, заменены на п правых разрядов из у (остальные биты не изменяются).
Упражнение 2.7. Напишите функцию
( х , р, п), возвращающую
значение х с инвертированными п битами, начиная
с позиции р (остальные биты не изменяются).
'
Упражнение 2.8. Напишите функцию right
(х,
которая циклически сдвигает х вправо на п разрядов.
2.10. Операторы и выражения присваиванияВыражение i = i
+ 2
в котором стоящая слева переменная повторяется и справа, можно напи- сать в сжатом виде:
i
+= 2
Оператор +=, как и =, называется
оператором присваивания.Большинству бинарных операторов (аналогичных + и имеющих левый и правый операнды) соответствуют операторы присваивания
ор=, где
ор -один из операторов
+
;
Если и
- выражения,
эквивалентно
= орс той лишь разницей, что вычисляется только один раз. Обратите внимание на скобки вокруг х *= у + 1
Операторы и выражения присваивания 73
эквивалентно
X = X * (у + 1)
но не
X = X * у + 1
В качестве примера приведем функцию bitcount, подсчитывающую число единичных битов в своем аргументе целочисленного типа.
/* bitcount: подсчет единиц х */
int bitcount( unsigned x)
{
int b;
(b
= 0; x != 0; x
1)
if (x
& 01)
return b;
}
Независимо от машины, на которой будет работать эта объяв- ление аргумента х как unsigned гарантирует, что при правом сдвиге осво- бождающиеся биты будут заполняться а не знаковым битом.
Помимо краткости операторы присваивания обладают тем преимуще- ством, что они более соответствуют тому, как человек мыслит. Мы гово- рим "прибавить 2 к или "увеличить i на 2", а не "взять i, добавить 2 и затем вернуть результат в i", так что выражение i += 2 лучше, чем i = i + 2.
Кроме того, в сложных выражениях вроде yyval[yypv[p3+p4] +
+= 2
благодаря оператору присваивания запись становится более легкой для понимания, так как читателю при такой записи не потребуется старательно сравнивать два длинных выражения, совпадают ли они, или выяснять,
почему они не совпадают. Следует иметь в виду и то, что подобные опера- торы присваивания могут помочь компилятору сгенерировать более эф- фективный код.
Мы уже видели, что присваивание вырабатывает значение и может применяться внутри выражения; вот самый расхожий пример:
while
=
!= EOF).
В выражениях встречаются и другие операторы присваивания (+=, -=
и т. д.), хотя и реже.
Типом и значением любого выражения присваивания являются тип и значение его левого операнда после завершения присваивания.
74 Глава 2. Типы, операторы и выраженияУпражнение 2.9. Применительно к числам, в представлении которых использован дополнительный код, выражение х &=
уничтожает самую правую 1 в х. Объясните, почему. Используйте это наблюдение при написании более быстрого варианта функции
Условные выраженияИнструкции if (а
> Ь)
z = а;
else
г =пересылают в z большее из двух значений а и
Условное выражение,написанное с помощью тернарного (т. е. имеющего три операнда) опера- тора представляет собой другой способ записи этой и подобных ей конструкций. В выражении
1первым вычисляется выражение
Если его значение не нуль (исти- то вычисляется выражение и значение этого выражения стано- вится значением всего условного выражения. В противном случае вычис- ляется выражение и его значение становится значением условного выражения. Следует отметить, что из выражений и
вычисля- ется только одно из них. Таким образом, чтобы установить в z большее из а и можно написать z = (а >
? а : b; /* z =
b) */
'.
Следует заметить, что условное выражение и в самом деле является выражением, и его можно использовать в любом месте, где допускается выражение. Если и
принадлежат разным типам, то тип резуль- тата определяется правилами преобразования, о которых речь в этой главе ранее. Например, если f имеет тип float,
— тип то типом выражения
(л
> 0) ? f : п будет вне зависимости от того, положительно значение п или нет.
Заключать в скобки первое выражение в условном выражении не обя- так как приоритет очень низкий (более низкий приоритет имеет только присваивание), однако мы рекомендуем всегда это делать,
поскольку благодаря обрамляющим скобкам условие в выражении луч- ше воспринимается.
Приоритет и очередность вычислений 75Условное выражение часто позволяет сократить программу. В качестве примера приведем цикл, обеспечивающий печать п элементов массива по 10 на каждой строке с одним пробелом между колонками; каждая строка цикла, включая последнюю, заканчивается символом новой строки:
for (i i < n; i++)
a[i],
== 9
i ==
?
'Символ новой строки посылается после каждого десятого и после эле- мента. За всеми другими элементами следует пробел. Эта программа вы- глядит довольно замысловато, зато она более компактна, чем эквивалент- ная программа с использованием
Вот еще один хороший имеете п,
! = 11) ?
:
< 10
п%100 > 20) && п%10 >= 2
п%10 <= 4) ?
"а" :
Упражнение 2.10. Напишите функцию которая переводит большие буквы в малые, используя условное выражение не конструкцию
Приоритет и очередность вычисленийВ таблице показаны приоритеты и очередность вычислений всех операторов, включая те, которые мы еще не рассматривали. Операторы,
перечисленные на одной строке, имеют одинаковый приоритет;
строки упорядочены по убыванию приоритетов; так, например, *, / и % имеют одинаковый приоритет, который выше, чем приоритет бинарных + и -.
"Оператор" относится к вызову функции. Операторы -> и . (точка) обес- печивают доступ к элементам структур; о них пойдет речь в главе 6, там же будет рассмотрен и оператор sizeof (размер объекта). Операторы *
(косвенное обращение по указателю) и & (получение адреса объекта) об- суждаются в главе 5. Оператор "запятая" будет рассмотрен в главе 3.
Таблица Приоритеты и очередность вычислений операторовОператоры
0
[]
!
++ — +
/ %
Выполняются слева направо
&
(тип) sizeof справа налево слева направо ' Этот учитывающий русскую грамматику,
от авторского ла. -
Примеч. 76 Глава 2. Типы, операторы и выражения
Продолжение табл.
Операторы Выполняются+ - слева направо слева направо
< <= >
слева направо
== ! = слева направо
& . слева направо слева направо
! слева направо
&& слева направо слева направо справа налево
= += -= *= /= %= &=
справа налево слева направо и е операторы +, -, * и & имеют более высокий приоритет,
те же бинарные операторы.
Заметим, что приоритеты побитовых операторов и !
чем при- оритет == и ! =, из-за чего в побитовых проверках, таких как if ((х & MASK)
0)
чтобы получить правильный результат, приходится использовать скобки.
Си подобно многим языкам не фиксирует очередность вычисления операндов оператора (за исключением
Например, в инструкции вида х = f() + g();
f может быть вычислена раньше g или наоборот. Из этого следует, что если одна из функций изменяет значение переменной, от которой зави- сит другая функция, то помещаемый в х результат может зависеть от оче- редности вычислений. Чтобы обеспечить нужную последовательность вычислений, промежуточные результаты можно запоминать во времен- ных переменных.
Очередность вычисления аргументов функции также не определена,
поэтому на разных компиляторах power(2,
/* НЕВЕРНО */
может давать несовпадающие результаты. Результат вызова функции зависит от того, когда компилятор сгенерирует команды увеличения п —
до или после обращения к
Чтобы
обезопасить себя от возможного побочного эффекта, достаточно написать
Приоритет и вычислений _ 77
n, power(2, n));
Обращения к функциям, вложенные присвоения,
и де- операторы дают "побочный эффект", проявляющийся в том,
что при вычислении выражения значения некоторых переменных изме- няются. В любом выражении с побочным эффектом может быть скрыта трудно просматриваемая зависимость результата выражения от очеред- ности изменения значений переменных, входящих в выражение. В
например, типично неприятной ситуации возникает вопрос: массив а индексируется старым или измененным зна- чением i? Компиляторы могут по-разному генерировать программу, что проявится в интерпретации данной записи. Стандарт сознательно устро- ен так, что большинство подобных вопросов оставлено на усмотрение компиляторов, так как лучший порядок вычислений определяется архи- тектурой машины. Стандартом только гарантируется, что все побочные эффекты при вычислении аргументов проявятся перед входом в функ- цию. Правда, в примере с это нам не поможет.
Мораль такова: писать программы, зависящие от очередности вычис- лений, - плохая практика, какой бы язык вы ни использовали. Естествен- но, надо знать, чего следует избегать, но если вы не знаете, как образуют- ся побочные эффекты на разных машинах, то лучше и не рассчитывать выиграть на особенностях частной реализации.
3УправлениеПорядок, в котором выполняются вычисления, определяется инструк- циями управления. Мы уже встречались с наиболее распространенными конструкциями такого в предыдущих примерах; здесь мы завершим их список и более точно определим рассмотренные ранее.
Инструкции и блокиВыражение, скажем х = 0, или или printf становится
ин-струкцией, если в конце его поставить точку
с запятой, например:
х = 0;
i++;
Си точка с запятой является заключающим символом инструкции, а не разделителем, как в языке Паскаль.
Фигурные скобки { и } используются для объединения объявлений и инструкций в
составную инструкцию, или
блок, чтобы с точки зрения син- таксиса эта новая конструкция воспринималась как одна инструкция. Фи- гурные скобки,
обрамляющие группу инструкций, образующих тело функ- ции, это один пример; второй пример - это скобки, объединяющие ин- струкции, помещенные после else, while или f o r . (Переменные могут быть объявлены
любого блока, об этом разговор пойдет в главе 4.)
После правой закрывающей фигурной скобки в конце блока точка с запя- той не ставится.
: '
3.2. КонструкцияИнструкция используется для принятия решения. Формально ее синтаксисом является:
3.2. Конструкция
79
if (выражение)
else причем может и отсутствовать. Сначала вычисляется
ние, и, если оно истинно (т. е. отлично от нуля), выполняется инструк-
ция Если выражение ложно (т. е. его значение равно нулю) и существует else-часть, то выполняется
Так как if просто проверяет числовое значение выражения, условие иногда можно записывать в сокращенном виде. Так, запись if (выражение)
короче, чем if
! = 0)
Иногда такие сокращения естественны и ясны, в других случаях, наобо- рот, затрудняют понимание программы.
Отсутствие else-части в одной из вложенных друг в друга струкций может привести к неоднозначному толкованию записи. Эту не- однозначность разрешают тем, что else связывают с ближайшим у ко- торого нет своего else. Например, в if (n
> 0)
if (а
> Ь)
2 = а;
else
2 = b;
else относится к внутреннему что мы и показали с помощью отступов.
Если нам требуется иная интерпретация, необходимо должным образом расставить фигурные скобки:
if (n
> 0) {
if (a
> b)
2 = а;
}
else
2
b;
Ниже приводится пример ситуации, когда неоднозначность особенно опасна:
if (n >= 0)
for
= 0;
< n;
80 Глава 3. Управление
(s[i]
> 0) {
printf return
}
else /* НЕВЕРНО */
- отрицательное
С помощью отступов мы недвусмысленно показали, что нам нужно, од- нако компилятор не воспримет эту информацию и отнесет else к внут- реннему
Искать такого рода ошибки особенно тяжело. Здесь уместен следующий совет: вложенные обрамляйте фигурными скобками.
Кстати, обратите внимание на точку с запятой после z = а в if (а
>
z = а;
else z = b;
Здесь она обязательна, поскольку по правилам грамматики за if должна следовать инструкция, а выражение-инструкция вроде z =
всегда за- канчивается точкой с запятой.
3.3. Конструкция
Конструкция if (выражение)
инструкция
else if (выражение)
инструкция
else if (выражение)
инструкция
else if (выражение)
инструкция
else
инструкция
встречается так часто, что о ней стоит поговорить особо. Приведенная по- следовательность инструкций if - самый общий способ описания много- ступенчатого принятия решения. Выражения вычисляются по порядку;
как только встречается выражение со значением "истина", выполняется соответствующая ему на этом проверок завершается. Здесь под словом инструкция имеется в виду либо одна
3.3. Конструкцияинструкция, либо группа инструкций в фигурных скобках.
Последняя else-часть срабатывает, если не выполняются все предыду- щие условия. Иногда в последней части не
требуется производить ника- ких действий, в этом случае фрагмент else
инструкцияможно опустить или использовать для фиксации ошибочной ("невозмож- ной") ситуации.
В качестве иллюстрации трехпутевого ветвления рассмотрим функцию бинарного поиска значения х в массиве v. Предполагается, что элементы v упорядочены по возрастанию. Функция выдает положение х в v (число в пределах от 0 до если там встречается, и если его нет.
При бинарном поиске значение х сначала сравнивается с элементом,
занимающим серединное положение в массиве v. Если х меньше, чем это значение, то областью поиска становится "верхняя" половина массива v,
в противном случае - "нижняя". В любом случае следующий шаг - это сравнение с серединным элементом отобранной половины. Процесс диапазона продолжается до тех пор, пока либо не будет найдено значение, либо не станет пустым диапазон поиска. Запишем цию бинарного поиска:
/* binsearch: найти х в v[0] <= v[1] <=
<= v[n-1] */
int binsearch(int x, int v[], int n)
int low, high, mid;
low = 0;
high = n - while (low <= high) {
mid = (low + high) / 2;
if (x <
high = mid - else if (x >
low = mid +
else /* совпадение найдено */
return mid;
}
return
/* совпадения нет */
82 Глава 3. Управление
Основное действие, выполняемое на каждом шаге поиска, - сравнение значения х (меньше, больше или равно) с элементом это сравне- ние естественно поручить конструкции
Упражнение 3.1. В нашей программе бинарного поиска внутри цикла осуществляются две проверки, хотя могла быть только одна (при увеличении числа проверок вне цикла). Напишите программу, преду- смотрев в ней одну проверку внутри цикла. Оцените разницу во времени выполнения.
3.4. Переключатель switch
Инструкция switch используется для выбора одного из многих путей.
Она проверяет, совпадает ли значение выражения с одним из значений,
входящих в некоторое множество целых констант, и выполняет соответ- ствующую этому значению ветвь программы:
switch (выражение) {
case
инструкции
case конст-выр: инструкции
default: инструкции
}
\
Каждая ветвь case помечена одной или несколькими целочисленными константами или же константными выражениями. Вычисления начина- ются с той ветви в которой константа совпадает со значением выра- жения. Константы всех ветвей case должны отличаться друг от друга. Если выяснилось, что ни одна из констант не подходит, то выполняется ветвь,
помеченная словом default, если таковая имеется, в противном случае ничего не делается. Ветви case и можно располагать в любом по- рядке.
В главе 1 мы написали программу, подсчитывающую число вхожде- ний в текст каждой цифры, символов-разделителей (пробелов, табуля- ций и новых строк) и всех остальных символов. В ней мы использовали else else. Теперь приведем вариант этой программы с переключателем switch:
tfinclude /* подсчет цифр, символов-разделителей и прочих символов */
{
int с, i,
nother,
3.4. Переключатель switch _ 83
= nother = 0;
for (i
= 0; i < 10; i++)
ndigit[i] = 0;
while
=
!= EOF) {
switch (c) {
case case case case case case case case
: case case
:
ndigit[c - case '
case case
:
nwhite++;
break;
nother++;
break;
printf ("цифр for (i= 0; i < 10; i++)
printf символов-разделителей =
прочих =
nwhite, nother);
return 0;
}
Инструкция break вызывает немедленный выход из переключателя switch. Поскольку выбор ветви case реализуется как переход на метку, то после выполнения одной ветви case,
если не предпринять, програм- ма
провалится вниз на следующую ветвь. Инструкции break и ret
— наи- более распространенные средства выхода из переключателя. Инструкция break используется также для принудительного выхода из циклов while,
и do-while (мы еще поговорим об этом чуть позже).
"Сквозное" выполнение ветвей case вызывает смешанные чувства. С од- ной стороны, это хорошо, поскольку позволяет несколько ветвей case объе- динить в одну, как мы и поступили с цифрами в нашем примере. Но с дру- гой - это означает, что в конце почти каждой ветви придется ставить чтобы избежать перехода к следующей. Последовательный проход по вет- вям - вещь ненадежная, это чревато ошибками, особенно при изменении программы. За исключением случая с несколькими метками для одного вычисления, старайтесь по возможности реже пользоваться сквозным
3. Управление проходом, но если уж вы его применяете, обязательно комментируйте эти особые места.
Добрый вам совет: даже в конце последней ветви (после в на- шем примере) помещайте инструкцию break, хотя с точки зрения логики в ней нет никакой необходимости. Но эта маленькая предосторожность спасет вас, когда однажды вам потребуется добавить в конец еще одну ветвь case.