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

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

Содержание

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-11 Honeywell 6000 IBM 370 Interdata 8/32
ascii ascii ebcdic ascii
char 8-bits 9-bits 8-bits 8-bits
int 16 36 32 32
short 16 36 16 16
long 32 36 32 32
float 32 36 32 32
double 64 72 64 64

Цель состоит в том, чтобы 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 1000
char    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 условное выражение.


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

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

operator associativity
() [] -> . слева направо
! ~ справа налево
++ -- - справа налево
(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 укажет большинство мест, зависящих от порядка вычислений).

Наш баннер
Вы можете установить наш баннер на своем сайте или блоге, скопировав этот код:
RSS новости