Kernighan, B. W. and Ritchie, D. M. "The 'C' Programming Language"; Chapter 2

Типы, операции и выражения.

Переменные и константы являются основными об'ектами, с которымиоперирует программа. Описания перечисляют переменные, которыебудут использоваться, указывают их тип и, возможно, их начальныезначения. Операции определяют, что с ними будет сделано. Выраженияоб'единяют переменые м константы для получения новых значенмй.Все это - темы настоящей главы.

Содержание

2.1. Имена переменных
2.2. Типы и размеры данных.
2.3. Константы.
2.3.1. Символьная константа.
2.3.2. Константное выражение
2.3.3. Строчная константа
2.4. Описания
2.5. Арифметические операции.
2.6. Операции отношения и логические операции
2.7. Преобразование типов
2.8. Операции увеличения и уменьшения
2.9. Побитовые логические операции
2.10. Операции и выражения присваивания.
2.11. Условные выражения
2.12. Старшинство и порядок вычисления.


Имена переменных

Хотя мы этого сразу прямо не сказали, существуют некоторые ограниченияна имена переменных и символических констант. Имена составляются избукв и цифр; первый символ должен быть буквой. Подчеркивание "_"тоже считается буквой; это полезно для удобочитаемости длинныхимен переменных. Прописные и строчные буквы различаются;традиционная практика в "C" - использовать строчные буквы дляимен переменных, а прописные - для символических констант.

Играют роль только первые восемь символов внутреннего имени,хотя использовать можно и больше. Для внешних имен,таких как имена функций и внешних переменных, это число можетоказаться меньше восьми, так как внешние имена используются различнымиассемблерами и загрузчиками. Детали приводятся вприложении A. Крометого, такие ключевые слова как if, else, int, float и т.д.,зарезервированы: вы не можете использовать их в качестве именпеременных. (они пишутся строчными буквами).

Конечно, разумно выбирать имена переменных таким образом, чтобы ониозначали нечто, относящееся к назначению переменных, и чтобы быломенее вероятно спутать их при написании.


Типы и размеры данных.

В языке "C" имеется только несколько основных типов данных:

charодин байт, в котором может находиться одинсимвол из внутреннего набора символов.
intцелое, обычно соответствующее естественномуразмеру целых в используемой машине.
floatс плавающей точкой одинарной точности.
doubleс плавающей точкой двойной точности.

Кроме того имеется ряд квалификаторов, которые можно использоватьс типом int: short (короткое), long (длинное) и unsigned (без знака).Квалификаторы short и long указывают на различные размеры целых.Числа без знака подчиняются законам арифметики по модулю 2 в степени n,где n - число битов в int; числа без знаков всегда положительны.Описания с квалификаторами имеют вид:

short int       x;long int        y;unsigned int    z;

Слово int в таких ситуациях может быть опущено, что обычно иделается.

Количество битов, отводимых под эти об'екты зависит от имеющейся машины;в таблице ниже приведены некоторые характерные значения.

DEC PDP-11Honeywell 6000IBM 370Interdata 8/32
asciiasciiebcdicascii
char8-bits9-bits8-bits8-bits
int16363232
short16361616
long32363232
float32363232
double64726464

Цель состоит в том, чтобы short и long давали возможность в зависимостиот практических нужд использовать различные длины целых; тип intотражает наиболее "естественный" размер конкретной машины. Как вывидите, каждый компилятор свободно интерпретирует short и long всоответствии со своими аппаратными средствами. Все, на что вы можететвердо полагаться, это то, что short не длиннее, чем long.


Константы.

Константы типа int и float мы уже рассмотрели. Отметим еще только,что как обычная

 123.456e7,
так и "научная" запись
 0.12e3
для float является законной.

Каждая константа с плавающей точкой считается имеющей типdouble, так что обозначение "e" служит как для float, так и дляdouble.

Длинные константы записываются в виде 123l. Обычная целая константа,которая слишком длинна для типа int, рассматривается как long.

Существует система обозначений для восьмеричных и шестнадцатеричныхконстант: лидирующий 0 (нуль) в константе типа int указывает навосьмеричную константу, а стоящие впереди 0х соответствуютшестнадцатеричной константе. Например, десятичное число 31 можноИаписать как 037 в восьмеричной форме и как 0х1f в шестнадцатеричной.Шестнадцатеричные и восьмеричные константы могут также заканчиватьсябуквой l, что делает их относящимися к типу long.

Символьная константа.

Символьная константа - это один символ, заключенный в одинарныекавычки, как, например, 'x'. Значением символьной константы являетсячисленное значение этого символа во внутреннем машинном наборесимволов. Например, в наборе символов ascii символьный нуль, или'0', имеет значение 48, а в коде ebcdic - 240, и оба эти значениясовершенно отличны от числа 0. Написание '0' вместо численногозначения, такого как 48 или 240, делает программу не зависящей отконкретного численного представления этого символа в данной машине.Символьные константы точно так же участвуют в численных операциях,как и любые другие числа, хотя наиболее часто они используются всравнении с другими символами. Правила преобразования будут изложеныпозднее.

Некоторые неграфические символы могут быть представлены как символьныеконстанты с помощью условных последовательностей, как, например, \n(новая строка), \t (табуляция),\0 (нулевой символ), \\ (обратная косаячерта), ` (одинарная кавычка)и т.д. хотя они выглядят как два символа,на самом деле являются одним. Кроме того, можно сгенерироватьпроизвольную последовательность двоичных знаков размером в байт, еслинаписать

 '\ddd'
где ddd - от одной до трех восьмеричных цифр, как в
#define formfeed '\014' /* form feed */

Символьная константа '\0', изображающая символ со значением 0,часто записывается вместо целой константы 0, чтобы подчеркнутьсимвольную природу некоторого выражения.

Константное выражение

Константное выражение - это выражение, состоящее из одних констант.Такие выражения обрабатываются во время компиляции, а не при прогонепрограммы, и соответственно могут быть использованы в любом месте,где можно использовать константу, как, например в

#define maxline 1000char    line[maxline + 1];
или
seconds = 60 * 60 * hours;

Строчная константа

Строчная константа - это последовательность, состоящая из нуля или болеесимволов, заключенных в двойные кавычки, как, например,

"i am a string" /* я - строка */
или
"" /* null string */ /* нуль-строка */

Кавычки не являются частью строки, а служат только для ее ограничения.Те же самые условные последовательности, которые использовались всимвольных константах, применяются и в строках; символ двойной кавычкиизображается как ".

С технической точки зрения строка представляет собой массив, элементамикоторого являются отдельные символы. Чтобы программам было удобноопределять конец строки, компилятор автоматически помещает в конецкаждой строки нуль-символ \0. Такое представление означает, что ненакладывается конкретного ограничения на то, какую длину может иметьстрока, и чтобы определить эту длину, программы должны просматриватьстроку полностью. При этом для физического хранения строки требуетсяна одну ячейку памяти больше, чем число заключенных в кавычки символов.Следующая функция strlen(s) вычисляет длину символьной строки s несчитая конечный символ \0.

strlen(s)			/* return length of s */	char            s[];{	int             i;	i = 0;	while (s[i] != '\0')		++i;	return (i);}

Будьте внимательны и не путайтесимвольную константу со строкой,содержащей один символ: 'x' - это не то же самое, что "x". Первое -это отдельный символ, использованный с целью получения численногозначения, соответствующего букве x в машинном наборе символов. Второе -символьная строка, состоящая из одного символа (буква х) и \0.


Описания

Все переменные должны быть описаны до их использования, хотя некоторыеописания делаются неявно, по контексту. Описание состоит изспецификатора типа и следующего за ним списка переменных, имеющихэтот тип, как, например,

int     lower, upper, step;char    c, line[1000];

Переменные можно распределять по описаниям любым образом; приведенныевыше списки можно с тем же успехом записать в виде

int     lower;int     upper;int     step;char    c;char    line[1000];

Такая форма занимает больше места, но она удобна для добавлениякомментария к каждому описанию и для последующих модификаций.

Переменным могут быть присвоены начальные значения внутри их описания,хотя здесь имеются некоторые ограничения. Если за именем переменнойследуют знак равенства и константа, то эта константа служит вкачестве инициализатора, как, например, в

char    backslash = '\\';int     i = 0;float   eps = 1.0e-5;

Если рассматриваемая переменная является внешней или статической, тоинициализация проводится только один раз, согласно концепции до началавыполнения программы. Инициализируемым явно автоматическим переменнымначальные значения присваиваются при каждом обращении к функции, вкоторой они описаны. Автоматические переменные, не инициализируемыеявно, имеют неопределенные значения, (т.е. мусор). Внешние и статическиепеременные по умолчанию инициализируются нулем, но, тем не менее, ихявная инициализация является признаком хорошего стиля.

Мы продолжим обсуждение вопросов инициализации, когда будем описыватьновые типы данных.


Арифметические операции.

Бинарными арифметическими операциями являются +, -, *, / и операцияделения по модулю %. Имеется унарная операция -, но не существуетунарной операции +.

При делении целых дробная часть отбрасывается. Выражение

х % y
дает остаток от деления х на y и, следовательно, равно нулю, когдаB делится на y точно. Например, год является високосным, если онделится на 4, но не делится на 100, исключая то, что делящиеся на 400годы тоже являются високосными. Поэтому
if(year % 4 == 0 && year % 100 != 0 || year % 400 == 0)	год високосныйelse	год невисокосный

Операцию % нельзя использовать с типами float или double.

Операции + и - имеют одинаковое старшинство, которое младше одинаковогоуровня старшинства операций *, / и %, которые в свою очередь младшеунарного минуса. Арифметические операции группируются слева направо.(Сведения о старшинстве и ассоциативности всех операций собраны втаблице в конце этой главы). Порядок выполнения ассоциативных икоммутативных операций типа + и - не фиксируется; компилятор можетперегруппировывать даже заключенные в круглые скобки выражения,связанные такими операциями. Таким образом, a+(b+c) может быть вычисленокак (a+b)+c. Это редко приводит к какому-либо расхождению, но еслинеобходимо обеспечить строго определенный порядок, то нужно использоватьявные промежуточные переменные.

Действия, предпринимаемые при переполнении и антипереполнении (т.е. приполучении слишком маленького по абсолютной величине числа), зависят отиспользуемой машины.


Операции отношения и логические операции

Операциями отношения являются

=>  >  =<  <
Все они имеют одинаковое старшинство. Непосредственно за ними поуровню старшинства следуют операции равенства и неравенства:
==   !=
которые тоже имеют одинаковое старшинство. Операции отношения младшеарифметических операций, так что выражения типа i<lim-1 понимаются какi<(lim-1), как и предполагается.

Логические связки && и || Более интересны. Выражения, связанныеоперациями && и ||, вычисляются слева направо, причем их рассмотрениепрекращается сразу же как только становится ясно, будет ли результатистиной или ложью. Учет этих свойств очень существенен для написанияправильно работающих программ. Рассмотрим, например, оператор цикла всчитывающей строку функции getline, которую мы написали вглаве 1.

for (i = 0; i < lim - 1     && (с=getchar()) != '\n'     && c != EOF; ++i)	s[i] = c;
Ясно, что перед считыванием нового символа необходимо проверить,имеется ли еще место в массиве s, так что условие i<lim-1 должнопроверяться первым. И если это условие не выполняется, мы не должнысчитывать следующий символ.

Так же неудачным было бы сравнение 'c' с EOF до обращения к функцииgetchar:прежде чем проверять символ, его нужно считать.

Старшинство операции && выше, чем у ||, и обе они младше операцийотношения и равенства. Поэтому такие выражения, как

i < lim - 1 && (c = getchar()) != '\n' && c != EOF
не нуждаются в дополнительных круглых скобках. Но так как операция !=старше операции присваивания, то для достижения правильного результатав выражении
(c = getchar()) != '\n'
скобки необходимы.

Унарная операция отрицания ! Преобразует ненулевой или истинныйоперанд в 0, а нулевой или ложный операнд в 1. Обычное использованиеоперации ! Заключается в записи

if(!inword)
вместо
if(inword == 0)
Трудно сказать, какая форма лучше. Конструкции типа ! inword читаютсядовольно удобно ("если не в слове"). Но в более сложных случаях онимогут оказаться трудными для понимания.
Упражнение 2-1.
Напишите оператор цикла, эквивалентный приведенному выше оператору for,не используя операции &&.


Преобразование типов

Если в выражениях встречаются операнды различных типов, то онипреобразуются к общему типу в соответствии с небольшим наборомправил. В общем, автоматически производятся только преобразования,имеющие смысл, такие как, например, преобразование целого вплавающее в выражениях типа f+i. Выражения же, лишенные смысла, такиекак использование переменной типа float в качестве индекса, запрещены.

Во-первых, типы char и int могут свободно смешиваться в арифметическихвыражениях: каждая переменная типа char автоматически преобразуется вint. Это обеспечивает значительную гибкость при проведении определенныхкреобразований символов. Примером может служить функцияatoi, котораяставит в соответствие строке цифр ее численный эквивалент.

atoi(s)				/* convert s то integer */	char            s[];{	int             i, n;	n = 0;	for (i = 0; s[i] >= '0' && s[i] <= '9'; ++i)		n = 10 * n + s[i] - '0';	return (n);}

как уже обсуждалось в главе 1, выражение

s[i] - '0'
имеет численное значение находящегося в s[i] символа, потому чтозначение символов '0', '1' и т.д. образуют возрастающуюпоследовательность расположенных подряд целых положительных чисел.

Другой пример преобразования char в int дает функция lower,преобразующая данную прописную букву в строчную. Если выступающий вкачестве аргумента символ не является прописной буквой, то lowerвозвращает его неизменным. Приводимая ниже программа справедливатолько для набора символов ascii.

lower(c)			/* convert c to lower				 * case; ascii only */	int             c;{	if (c >= 'a' && c <= 'z')		return (c + '@' - 'a');	else			/* @ записано вместо 'a'				 * строчного */		return (c);}
Эта функция правильно работает при коде ascii, потому что численныезначения, соответствующие в этом коде прописным и строчным буквам,отличаются на постоянную величину, а каждый алфавит является сплошным- между a и z нет ничего, кроме букв. Это последнее замечание длянабора символов ebcdic систем ibm 360/370 оказывается несправедливым, всилу чего эта программа на таких системах работает неправильно - онапреобразует не только буквы.

При преобразовании символьных переменных в целыевозникает один тонкиймомент. Дело в том, что сам язык не указывает, должны ли переменным типаchar соответствовать численные значения со знаком или без знака. Можетли при преобразовании char в int получиться отрицательное целое? Ксожалению, ответ на этот вопрос меняется от машины к машине, отражаярасхождения в их архитектуре. На некоторых машинах (pdp-11, например)переменная типа char, крайний левый бит которой содержит 1,преобразуется в отрицательное целое ("знаковое расширение"). На другихмашинах такое преобразование сопровождается добавлением нулей с левогокрая, в результате чего всегда получается положительное число.

Определение языка "C" гарантирует, что любой символ из стандартногонабора символов машины никогда не даст отрицательного числа, так чтоэти символы можно свободно использовать в выражениях как положительныевеличины. Но произвольные комбинации двоичных знаков, хранящиеся каксимвольные переменные на некоторых машинах, могут дать отрицательныезначения, а на других положительные.

Наиболее типичным примером возникновения такой ситуации являетсясучай, когда значение 1 используется в качестве EOF. Рассмотримпрограмму

char            c;c = getchar();if (c == EOF)	...
На машине, которая не осуществляет знакового расширения, переменная 'c'всегда положительна, поскольку она описана как char, а так как EOFотрицательно, то условие никогда не выполняется. Чтобы избежать такойситуации, мы всегда предусмотрительно использовали int вместо char длялюбой переменной, получающей значение от getchar.

Основная же причина использования int вместо char не связана скаким-либо вопросом о возможном знаковом расширении. Просто функцияgetchar должна передавать все возможные символы (чтобы ее можно былоиспользовать для произвольного ввода) и, кроме того, отличающеесязначение EOF. Следовательно значение EOF не может быть представленокак char, а должно храниться как int.

Другой полезной формой автоматического преобразования типов являетсято, что выражения отношения, подобные i>j, и логические выражения,связанные операциями && и ||, по определению имеютзначение 1, если ониистинны, и 0, если они ложны. Таким образом, присваивание

isdigit = c >= '0' && c <= '9';
полагает isdigit равным 1, если с - цифра, и равным 0 в противномслучае. (в проверочной части операторов if, while, for и т.д. "истинно"просто означает "не нуль").

Неявные арифметические преобразования работают в основном, как иожидается. В общих чертах, если операция типа + или *, которая связываетдва операнда (бинарная операция), имеет операнды разных типов, топеред выполнением операции "низший" тип преобразуется к "высшему" иполучается результат "высшего" типа. Более точно, к каждойарифметической операции применяется следующая последовательностьправил преобразования.

  • Типы char и short преобразуются в int, а float в double.
  • Затем, если один из операндов имеет тип double, то другой преобразуетсяв double, и результат имеет тип double.
  • В противном случае, если один из операндов имеет тип long, то другойпреобразуется в long, и результат имеет тип long.
  • в противном случае, если один из операндов имеет тип unsigned, то другойпреобразуется в unsigned и результат имеет тип unsigned.
  • В противном случае операнды должны быть типа int, и результат имеет типint.

Подчеркнем, что все переменные типа float в выражениях преобразуются вdouble; в "Си" вся плавающая арифметика выполняется с двойной точностью.

Преобразования возникают и при присваиваниях; значение правой частипреобразуется к типу левой, который и является типом результата.Символьные переменные преобразуются в целые либо со знаковым расширением,либо без него, как описано выше. Обратное преобразование int в charведет себя хорошо - лишние биты высокого порядка просто отбрасываются.Таким образом

int             i;char            c;i = c;c = i;
значение 'c' не изменяется. Это верно независимо от того, вовлекаетсяли знаковое расширение или нет.

Если x типа float, а i типа int, то как

x = i;
так и
i = x;
приводят к преобразованиям; при этом float преобразуется в intотбрасыванием дробной части. Тип double преобразуется во floatокруглением. Длинные целые преобразуются в более короткие целые и впеременные типа char посредством отбрасывания лишних битов высокогопорядка.

Так как аргумент функции является выражением, то при передаче функциямаргументов также происходит преобразование типов: в частности, char иshort становятся int, а float становится double. Именно поэтому мыописывали аргументы функций как int и double даже тогда, когдаобращались к ним с переменными типа char и float.

Наконец, в любом выражении может быть осуществлено("принуждено") явноепреобразование типа с помощью конструкции, называемой перевод (cast).В этой конструкции, имеющей вид

(имя типа) выражение
выражение преобразуется к указанному типу по правилам преобразования,изложенным выше. Фактически точный смысл операции перевода можно описатьследующим образом: выражение как бы присваивается некоторой переменнойуказанного типа, которая затем используется вместо всей конструкции.Например, библиотечная процедура sqrt ожидает аргумента типа double ивыдаст бессмысленный ответ, если к ней по небрежности обратятся счем-нибудь иным. Таким образом, если n - целое, то выражение
sqrt((double) n)
до передачи аргумента функции sqrt преобразует n к типу double.(Отметим, что операция перевод преобразует значение n в надлежащий тип;фактическое содержание переменной n при этом не изменяется). Операцияперевода имеет тот же уровень старшинства, что и другие унарныеоперации, как указывается в таблице в конце этой главы.
Упражнение 2-2.
Составьте программу для функции htoi(s), которая преобразует строкушестнадцатеричных цифр в эквивалентное ей целое значение. При этомдопустимыми цифрами являются цифры от 1 до 9 и буквы от а до f.


Операции увеличения и уменьшения

В языке "Си" предусмотрены две необычные операции для увеличения иуменьшения значений переменных. Операция увеличения ++ добавляет 1 ксвоему операнду, а операция уменьшения -- вычитает 1. Мы частоиспользовали операцию ++ для увеличения переменных, как, например, в

if (c == '\n')	++i;

необычный аспект заключается в том, что ++ и -- можно использоватьлибо как префиксные операции (перед переменной, как в ++n), либо какпостфиксные (после переменной:n++). Эффект в обоих случаях состоит вувеличении n. Но выражение ++n увеличивает переменную n до использованияее значения, в то время как n++ увеличивает переменную n после того, какее значение было использовано. Это означает, что в контексте, гдеиспользуется значение переменной, а не только эффект увеличения,использование ++n и n++ приводит к разным результатам. Если n = 5, то

x = n++;
устанавливает х равным 5, а
x = ++n;
полагает х равным 6. В обоих случаях n становится равным 6. Операцииувеличения и уменьшения можно применять только к переменным;выражениятипа х=(i+j)++ являются незаконными.

В случаях, где нужен только эффект увеличения, а само значение неиспользуется, как, например, в

if (c == '\n')	nl++;
выбор префиксной или постфиксной операции является делом вкуса. Новстречаются ситуации, где нужно использовать именно ту или другуюоперацию. Рассмотрим, например, функцию squeeze(s,c), которая удаляетсимвол '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';}
каждый раз, как встечается символ, отличный от 'c', он копируется втекущую позицию 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), котораяприписывает строку t в конец строки s, образуя конкатенацию строк s и t.При этом предполагается, что в 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 в s копируется каждый символ, то для подготовки кследующему прохождению цикла постфиксная операция ++ применяется кобеим переменным i и j.
Упражнение 2-3.
Напишите другой вариант функции squeeze(s1,s2), который удаляет изстроки s1 каждый символ, совпадающий с каким-либо символом строки s2.

Упражнение 2-4.
Напишите программу для функции any(s1,s2), которая находит местопервого появления в строке s1 какого-либо символа из строки s2 и,если строка s1 не содержит символов строки s2, возвращает значение -1.


Побитовые логические операции

В языке предусмотрен ряд операций для работы с битами; эти операциинельзя применять к переменным типа float или double.

&побитовое and
|Побитовое включающее or
^побитовое исключающее or
<<сдвиг влево
>>сдвиг вправо
~дополнение (унарная операция)

Побитовая операция and часто используется для маскирования некоторогомножества битов; например, оператор

c = n & 0177;
передает в 'c' семь младших битов n, полагая остальные равными нулю.Операция | Побитового or используется для включения битов:
c = x | mask;
устанавливает на единицу те биты в х, которые равны единице в mask.

Следует быть внимательным и отличать побитовые операции & и | отлогических связок && и ||, которые подразумевают вычисление значенияистинности слева направо. Например, если х=1, а y=2, то значениеx&y равно нулю, в то время как значение x&&y равно единице./почему?/

Операции сдвига << и >> осуществляют соответственно сдвиг влево ивправо своего левого операнда на число битовых позиций, задаваемыхправым операндом. Таким образом, х<<2 сдвигает х влево на две позиции,заполняя освобождающиеся биты нулями, что эквивалентно умножению на 4.Сдвиг вправо величины без знака заполняет освобождающиеся биты нанекоторых машинах, таких как pdp-11, заполняются содержаниемзнакового бита /"арифметический сдвиг"/, а на других - нулем/"логический сдвиг"/.

Унарная операция ~ дает дополнение к целому; это означает, чтокаждый бит со значением 1 получает значение 0 и наоборот. Эта операцияобычно оказывается полезной в выражениях типа

x & ~077
где последние шесть битов х маскируются нулем. Подчеркнем, чтовыражение x&~077 не зависит от длины слова и поэтому предпочтительнее,чем, например, x&0177700, где предполагается, что х занимает 16 битов.Такая переносимая форма не требует никаких дополнительных затрат,поскольку ~077 является константным выражением и, следовательно,обрабатывается во время компиляции.

Чтобы проиллюстрировать использование некоторых операций с битами,рассмотрим функцию getbits(x,p,n), которая возвращает /сдвинутыми кправому краю/ начинающиеся с позиции р поле переменной х длиной nбитов. Мы предполагаем, что крайний правый бит имеет номер 0, и чтоn и p - разумно заданные положительные числа. Например,getbits(x,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 >> (р+1-n) сдвигает желаемое поле в правый конец слова.Описание аргумента х как unsigned гарантирует, что при сдвиге вправоосвобождающиеся биты будут заполняться нулями, а не содержимым знаковогобита, независимо от того, на какой машине пропускается программа. Всебиты константного выражения ~0 равны 1; сдвиг его на n позиций влево спомощью операции ~0<<n создает маску с нулями в n крайних правых битахи единицами в остальных; дополнение ~ создает маску с единицами в nкрайних правых битах.
Упражнение 2-5.
Переделайте getbits таким образом, чтобы биты отсчитывались слеванаправо.

Упражнение 2-6.
Напишите программу для функции wordlength(), вычисляющей длину словаиспользуемой машины, т.е. число битов в переменной типа int. Функциядолжна быть переносимой, т.е. одна и та же исходная программа должнаправильно работать на любой машине.

Упражнение 2-7.
Напишите программу для функции rightrot(n,b), сдвигающей циклическицелое n вправо на в битовых позиций.

Упражнение 2-8.
Напишите программу для функции invert(x,p,n), которая инвертирует(т.е. заменяет 1 на 0 и наоборот) n битов x, начинающихся с позициир, оставляя другие биты неизмененными.


Операции и выражения присваивания.

Такие выражения, как

i = i + 2;
в которых левая часть повторяется в правой части могут быть записаныв сжатой форме
i += 2;
используя операцию присваивания вида +=.

Большинству бинарных операций (операций подобных +, которые имеютлевый и правый операнд) соответствует операция присваивания видаоп=, где оп - одна из операций

+  -  *  /  %  <<  >>  &  ~  |
Если e1 и e2 - выражения, то
e1 оп= e2
эквивалентно
e1 = (e1) оп (e2)
За исключением того, что выражение е1 вычисляется только один раз.Обратите внимание на круглые скобки вокруг e2:
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;
такая операция присваивания облегчает понимание программы, так какчитатель не должен скрупулезно проверять, являются ли два длинныхвыражения действительно одинаковыми, или задумываться, почему они несовпадают. Такая операция присваивания может даже помочь компиляторуполучить более эффективную программу.

Мы уже использовали тот факт, что операция присваивания имеет некотороезначение и может входить в выражения;самый типичный пример

while ((c = getchar()) != EOF)
присваивания, использующие другие операции присваивания (+=, -= и т.д.)также могут входить в выражения, хотя это случается реже.

Типом выражения присваивания является тип его левого операнда.

Упражнение 2-9.
В двоичной системе счисления операция x&(x-1) обнуляет самый правыйравный 1 бит переменной х.(почему?) используйте это замечание длянаписания более быстрой версии функции bitcount.


Условные выражения

Операторы

if (a > b)	z = a;else	z = b;
конечно вычисляют в z максимум из a и b. Условное выражение, записанноес помощью тернарной операции "?:", предоставляет другую возможностьдля записи этой и аналогичных конструкций. В выражении
e1 ? e2 : e3
сначала вычисляется выражение e1. Если оно отлично от нуля (истинно),то вычисляется выражение e2, которое и становится значением условноговыражения. В противном случае вычисляется e3, и оно становитсязначением условного выражения. Каждый раз вычисляется только одно извыражения e2 и e3. Таким образом, чтобы положить z равным максимумуиз a и b, можно написать
z = (a > b) ? a : b;  /* z = max(a,b) */

следует подчеркнуть, что условное выражение действительно являетсявыражением и может использоваться точно так же, как любое другоевыражение. Если е2 и е3 имеют разные типы, то тип результатаопределяется по правилам преобразования, рассмотренным ранее в этойглаве. Например, если f имеет тип float, а n - тип int, товыражение

(n > 0) ? f : n
имеет тип double независимо от того, положительно ли n или нет.

Так как уровень старшинства операции ?: очень низок, прямо надприсваиванием, то первое выражение в условном выражении можно незаключать в круглые скобки. Однако, мы все же рекомендуем это делать,так как скобки делают условную часть выражения более заметной.

Использование условных выражений часто приводит к коротким программам.Например, следующий ниже оператор цикла печатает n элементов массива,по 10 в строке, разделяя каждый столбец одним пробелом и заканчиваякаждую строку (включая последнюю) одним символом перевода строки.

for (i = 0; i < n; i++)	printf("%6d%c",	       a[i], (i % 10 == 9 || i == n - 1) ? '\n' : ' ');
Символ перевода строки записывается после каждого десятого элемента ипосле n-го элемента. За всеми остальными элементами следует одинпробел. Хотя, возможно, это выглядит мудреным, было бы поучительнымпопытаться записать это, не используя условного выражения.
Упражнение 2-10.
Перепишите программу для функции lower, которая переводит прописныебуквы в строчные, используя вместо конструкции if-else условноевыражение.


Старшинство и порядок вычисления.

В приводимой ниже таблице сведены правила старшинства и ассоциативностивсех операций, включая и те, которые мы еще не обсуждали. Операции,расположенные в одной строке, имеют один и тот же уровень старшинства;строки расположены в порядке убывания старшинства. Так, например,операции *, / и % имеют одинаковый уровень старшинства, который выше,чем уровень операций + и -.

operatorassociativity
() [] -> .слева направо
! ~справа налево
++ -- -справа налево
(type) * & sizeofсправа налево
* / %слева направо
+ -слева направо
<< >>слева направо
< <= > >=слева направо
== !=слева направо
&слева направо
^слева направо
|слева направо
&&слева направо
||слева направо
?:справа налево
= += -= etc.справа налево
,слева направо
Операции -> и . используются для доступа к элементам структур;они будут описаны в главе 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 служит в качествеиндекса. Компилятор может поступать разными способами и в зависимостиот своей интерпретации выдавать разные результаты. Тот случай, когдапроисходят побочные эффекты (присваивание фактическим переменным), -оставляется на усмотрение компилятора, так как наилучший порядоксильно зависит от архитектуры машины.

Из этих рассуждений вытекает такая мораль: написание программ, зависящихот порядка вычислений, является плохим методом программирования налюбом языке. Конечно, необходимо знать, чего следует избегать, но есливы не в курсе, как эти вещи реализованы на разных машинах, этоневедение может предохранить вас от неприятностей. (отладочная программаlint укажет большинство мест, зависящих от порядка вычислений).