Переменные и константы являются основными объектами, с которыми имеет дело программа. Переменные перечисляются в описаниях, где устанавливаются их типы, а возможно, и начальные значения. Операции специфицируют те действия, которые с ними совершаются. Для получения новых значений выражения могут оперировать с переменными и константами. Тип объекта определяет множество значений, которые этот объект может принимать, и операций, которые над ними могут выполняться. Названные «кирпичики» и будут предметом обсуждения в этой главе.
Стандартом ANSI было утверждено значительное число небольших изменений и
добавлений к основным типам и выражениям. Любой целый тип теперь может быть
со знаком, signed
, и без знака, unsigned
. Предусмотрен способ записи
беззнаковых констант и шестнадцатиричных литерных констант. Операции с
плавающей точкой допускаются теперь и с одинарной точностью. Введен тип long
double
, обеспечивающий повышенную точность. Стринговые константы
конкатенируются («склеиваются») теперь во время компиляции. Перечислимый тип
стал частью языка, формализующей установку диапазона значений типа. Объекты
разрешено помечать как const
для защиты их от каких-либо изменений. В связи с
введением новых типов расширены правила автоматического преобразования из
одного арифметического типа в другой.
Хотя мы ничего не говорили об этом в гл. 1, но существуют некоторые
ограничения на задание имен переменных и именованных констант. Имена
составляются из букв и цифр; первой литерой должна быть буква. Знак
подчеркивания '_'
считается буквой; его иногда удобно использовать, чтобы
улучшить восприятие длинных имен переменных. Не начинайте имена переменных с
подчеркивания, так как многие переменные библиотечных программ начинаются
именно с этого знака. Большие (прописные) и малые (строчные) буквы
различаются, так что x
и X
— два разных имени. Обычно в программах на Си
малыми буквами набирают переменные, а большими — именованные константы.
Для внутренних имен значимыми являются первые 31 литера. Для имен функций и
внешних переменных число значимых литер может быть меньше 31, так как эти
имена обрабатываются ассемблерами и загрузчиками и языком не контролируются.
Уникальность внешних имен гарантируется только в пределах 6 литер, набранных
безразлично в каком регистре. Ключевые слова if
, else
, int
, float
и т.д.
зарезервированы, и их нельзя использовать в качестве имен переменных. Все они
набираются на нижнем регистре (т.е. малыми буквами).
Разумно переменным давать осмысленные имена в соответствии с их назначением, причем такие, чтобы их было трудно спутать друг с другом. Мы предпочитаем короткие имена для локальных переменных, особенно для счетчиков циклов, и более длинные для внешних переменных.
В Си существует всего лишь несколько базовых типов:
char |
единичный байт, который может содержать одну литеру из допустимого набора литер |
int |
целое, обычно отображаемое на естественное представление целых в машине |
float |
число с плавающей точкой одинарной точности |
double |
число с плавающей точкой двойной точности |
Имеется также несколько квалификаторов, которые можно использовать вместе с
указанными базовыми типами. Например, квалификаторы short
(короткий) и long
(длинный) применяются к целым:
short int sh; long int counter;
В таких описаниях слово int
можно опускать, что обычно и делается.
Если только не возникает противоречий со здравым смыслом, целое short
и целое
long
должны быть разной длины, а int
соответствовать естественному размеру
целых на данной машине. Чаще всего для представления целого, описанного с
квалификатором short
, отводится 16 бит, с квалификатором long
— 32 бита, а
значению типа int
— или 16, или 32 бита. Разработчики компилятора вправе сами
выбирать подходящие размеры, сообразуясь с характеристиками своего компьютера
и соблюдая только следующие ограничения: значения типов short
и int
представляются по крайней мере 16 битами, типа long
— по крайней мере 32
битами, размер short
не больше размера int
, который в свою очередь не больше
размера long
.
Квалификаторы signed
(со знаком) или unsigned
(без знака) можно применять к
типу char
и любому целому типу. Значения unsigned
всегда положительны или
равны нулю и подчиняются законам арифметики по модулю 2n
, где n
— количество
бит в представлении типа. Так, например, если значению char
отводится 8 бит,
то unsigned
char
имеет значения в диапазоне от 0 до 255, а signed char
— от
-128 до 127 (в машине с двоичным дополнительным кодом). Являются ли значения
типа просто char
знаковыми или беззнаковыми, зависит от машины, но в любом
случае коды печатаемых литер положительны.
Тип long double
предназначен для арифметики с плавающей точкой повышенной
точности. Как и в случае целых, размеры объектов с плавающей точкой зависят
от реализации; float
, double
и long double
могут представляться одним
размером, а могут — двумя или тремя разными размерами.
Именованные константы для всех размеров вместе с другими характеристиками
машины и компилятора содержатся в стандартных головных файлах <limits.h>
и
<float.h>
. (См. приложение B.)
Напишите программу, которая будет выдавать диапазоны значений
типов char
, short
, int
и long
, описанных как signed
и как unsigned
, с помощью
печати соответствующих значений из стандартных головных файлов и путем
прямого вычисления. Определите диапазоны чисел с плавающей точкой различных
типов. Вычислить эти диапазоны сложнее.
Целая константа, например, 1234, имеет тип int
. Константа типа long
завершается буквой l
или L
, например 123456789L; слишком большое целое,
которое невозможно представить как int
, будет представлено как long
.
Беззнаковые константы заканчиваются буквой u
или U
, а окончание ul
или UL
говорит о том, что тип константы — unsigned long
.
Константы с плавающей точкой имеют десятичную точку (123.4) или
экспоненциальную часть (1e-2) или же и то и другое. Если у них нет окончания,
считается, что они типа double
. Окончание f
или F
указывает на тип float
, а l
или L
— на тип long double
.
Помимо десятичного целое значение может иметь восьмеричное или
шестнадцатиричное представление. Если константа начинается с нуля, то она
представлена в восьмеричном виде, если с 0x
или с 0X
, то — в
шестнадцатиричном. Например, десятичное целое 31 можно записать как 037 или
как 0X1F
. Записи восьмеричной и шестнадцатиричной констант могут завершаться
буквой L
(для указания на тип long
) и U
(если нужно показать, что константа
беззнаковая). Например, константа 0XFUL
имеет значение 15 и тип unsigned
long
.
Литерная константа есть целое, записанное в виде литеры, обрамленной
одиночными кавычками, например 'x'
. Значением литерной константы является
числовой код литеры из набора литер на данной машине. Например, литерная
константа '0'
в кодировке ASCII имеет значение 48, которое никакого отношения
к числовому значению 0 не имеет. Если мы пишем '0'
, а не какое-нибудь
значение (например, 48), которое следует из способа кодировки, мы тем самым
делаем программу независимой от частного значения кода, к тому же она и легче
читается. Литерные константы могут участвовать в операциях над числами точно
так же, как и любые другие целые, хотя чаще они используются для сравнения с
другими литерами.
Некоторые литеры в литерных и стринговых константах записываются с помощью
эскейп-последовательностей, например \n
(новая_строка); такие
последовательности изображаются двумя литерами, но обозначают одну. Кроме
того, произвольный восьмеричный код можно задать в виде
'\ooo'
где ooo
— одна, две или три восьмеричные цифры (0...7) или
'\xhh'
где hh
— одна, две или более шестнадцатиричные цифры (0...9, a...f, A...F).
Таким образом, мы могли бы написать
#define VTAB '\013' /* верт. табуляция в ASCII */ #define BELL '\007' /* звонок в ASCII */
или в шестнадцатиричном виде:
#define VTAB '\xb' /* верт. табуляция в ASCII */ #define BELL '\x7' /* звонок в ASCII */
Полный набор эскейп-последовательностей следующий:
\a |
сигнал_звонок |
\b |
возврат_на_шаг |
\f |
перевод_страницы |
\n |
новая_строка |
\r |
возврат_каретки |
\t |
гор_табуляция |
\v |
верт_табуляция |
\\ |
обратная_наклонная_черта |
\? |
знак_вопроса |
\' |
одиночная_кавычка |
\" |
двойная_кавычка |
\ ooo |
восьмеричный_код |
\x hh |
шестнадцатиричный_код |
Литерная константа '\0'
— это литера с нулевым значением — так называемая
литера null
. Вместо просто 0
часто используют запись '\0'
, чтобы подчеркнуть
литерную природу выражения, хотя и в том и другом случае запись обозначает
нуль.
Константные выражения — это выражения, оперирующие только с константами. Такие выражения вычисляются во время компиляции, а не во время счета, и поэтому их можно использовать в любом месте, где допустимы константы, как, например, в
#define MAXLINE 1000 char line[MAXLINE+1];
или в
#define LEAP 1 /* in leap years - в високосные годы */ int days[31+28+LEAP+31+30+31+30+31+31+30+31+30+31];
Стринговая константа, или стринговый литерал, — это нуль или более литер, заключенных в двойные кавычки, как, например,
"Это стринговая константа"
или
"" /* пустой стринг */
Кавычки не входят в стринг, а служат только ее ограничителями. Так же, как и
в литерные константы, в стринги можно включать эскейп-последовательности; \"
,
например, представляет собой двойную кавычку. Стринговые константы можно
конкатенировать («склеивать») во время компиляции; например, запись двух
стрингов
"Здравствуй," "мир!"
эквивалентна записи одного следующего стринга:
"Здравствуй, мир!"
Указанное свойство позволяет разбивать длинные стринги на части и располагать эти части на отдельных строчках.
Фактически стринговая константа — это массив литер. Во внутреннем
представлении стринга в конце обязательно присутствует null
-литера '\0'
,
поэтому памяти для стринга требуется на один байт больше, чем число литер,
расположенных между двойными кавычками. Это означает, что нет ограничения на
длину задаваемого стринга, но чтобы определить его длину, требуется
просмотреть весь стринг. Функция strlen(s)
вычисляет длину стринга s
без
учета завершающей его литеры '\0'
. Ниже приводится наша версия этой функции:
/* strlen: возвращает длину стринга s */ int strlen(char s[]) { int i; i = 0; while (s[i] != '\0') ++i; return i; }
Функция strlen
и некоторые другие, применяемые к стрингам, описаны в
стандартном головном файле <string.h>
.
Будьте внимательны и помните, что литерная константа и стринг, содержащий
одну литеру, не одно и то же: 'x'
не то же самое, что "x"
. Запись 'x'
обозначает целое значение, равное коду буквы x
из стандартного набора литер,
а запись "x"
— массив литер, который содержит одну литеру (букву x) и '\0'
.
В Си имеется еще один вид константы, константа перечисления. Перечисление — это список целых констант, как, например, в
enum boolean { NO, YES };
Первое имя в enum
(от английского слова enumeration — перечисление. —
Примеч-ред.) имеет значение 0, следующее — 1 и т.д. (если не было явных
спецификаций значений констант). Если не все значения специфицированы, то они
продолжают прогрессию, начиная от последнего специфицированного значения, как
в следующих двух примерах:
enum escapes { BELL = '\a', BACKSPACE = '\b', TAB = '\t', NEWLINE = '\n', VTAB = '\v', RETURN = '\r' }; enum months { JAN = 1, FEB, MAR, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV, DEC }; /* FEB есть 2, MAR есть 3 и т.д. */
Имена в различных перечислениях должны отличаться друг от друга. Значения внутри одного перечисления могут совпадать.
Средство enum
обеспечивает удобный способ присвоить константам имена, причем
в отличие от #define
при этом способе значения констант могут генерироваться
автоматически. Перечислимый тип разрешено использовать для определения
переменных, однако компилятор не обязан контролировать, входят ли
присваиваемые этим переменным значения в их тип. Но сама возможность такой
проверки часто делает enum
лучше, чем #define
. Кроме того, отладчик получает
возможность печатать значения перечислимых переменных в символьном виде.
Все переменные должны быть декларированы раньше, чем будут использоваться, при этом некоторые декларации могут быть получены неявно — из контекста. Декларация специфицирует тип и содержит список из одной или нескольких переменных этого типа, как, например, в
int lower, upper, step; char c, line[1000];
Переменные можно распределять по декларациям произвольным образом, так что указанные выше списки можно записать и в следующем виде:
int lower; int upper; int step; char c; char line[1000];
Эта последняя форма записи занимает больше места, тем не менее она лучше, поскольку позволяет добавлять к каждой декларации комментарий и более удобна для последующих модификаций.
В своей декларации переменная может быть инициализирована, как, например:
char esc = '\\'; int i = 0; int limit = MAXLINE+1; float eps = 1.0e-5;
Инициализация неавтоматической переменной осуществляется только один раз — перед тем, как программа начнет выполняться, при этом инициализатор должен быть константным выражением. Явно инициализируемая автоматическая переменная получает начальное значение каждый раз при входе в функцию или блок, ее инициализатором может быть любое выражение. Внешние и статические переменные по умолчанию получают нулевые значения. Автоматические переменные, явным образом не инициализированные, содержат неопределенные значения («мусор»).
К любой переменной в декларации может быть применен квалификатор const
для
указания того, что ее значение далее не будет изменяться.
const double e = 2.71828182845905; const char msg[] = "предупреждение: ";
Применительно к массиву квалификатор const
указывает на то, что ни один из
его элементов не будет меняться. Указание const
можно также применять к
аргументу–массиву, чтобы сообщить, что функция не изменяет этот массив:
int strlen(const char[]);
Реакция на попытку изменить переменную, помеченную квалификаторам const
,
оставлена на усмотрение компилятора.
Бинарными арифметическими операторами являются +
, -
, *
, /
, а также оператор
взятия модуля %
. Деление целых сопровождается отбрасыванием дробной части,
какой бы она ни была. Выражение
x % y
дает остаток от деления x
на y
и, следовательно, нуль, если x
делится на y
нацело. Например, год является високосным, если он делится на 4 (но не на
100). Кроме того, високосным считается год, если он делится на 400.
Следовательно,
if ((year % 4 == 0 && year % 100 != 0 year % 400 == 0) printf("%d високосный год\n", year); else printf("%d не високосный год\n", year);
Оператор %
к операндам типов float
и double
не применяется. В какую сторону,
(в сторону увеличения или уменьшения числа) будет усечена дробная часть при
выполнении /
и каким будет знак результата операции %
с отрицательными
операндами, это зависит от машины.
Бинарные операторы +
и -
имеют одинаковый приоритет, который ниже приоритета
операторов *
, /
и %
, который в свою очередь ниже приоритета унарных
операторов +
и -
. Арифметические операции одного приоритетного уровня
выполняются слева направо.
В конце этой главы приводится табл. 2.1, в которой показаны приоритеты всех операторов и порядок их выполнения.
Операторами отношения являются
> >= < <=
Все они имеют одинаковый приоритет. Ровно на одну ступень ниже приоритет операторов сравнения на равенство:
== !=
Операторы отношения имеют более низкий приоритет, чем арифметические, поэтому
выражение типа i < lim-1
будет выполняться так же, как i < (lim-1)
, т.е. как
мы и ожидаем.
Более интересны логические операторы &&
и ||
. Выражения, между которыми стоят
операторы &&
или ||
, вычисляются слева направо, и вычисление прекращается,
как только становится известна истинность или ложность результата. Многие
Си-программы опираются на это свойство, как, например, цикл из функции
getline
, которую мы приводили в гл. 1:
for (i = 0; i < lim-1 && (c = getchar()) != '\n' && c != EOF; ++i) s[i] = c;
Прежде чем читать очередную литеру, нужно проверить, есть ли место для нее в
массиве s
. иначе говоря, сначала необходимо проверить условие i < lim-1
. Если
это условие не выполняется, мы не должны продолжать вычисление, в частности
читать следующую литеру. Так же было бы неправильным сравнивать c
с EOF
до
обращения к getchar
; следовательно, и вызов getchar
, и присваивание должны
выполняться перед указанной проверкой.
Приоритет оператора &&
выше, чем оператора ||
, однако их приоритеты ниже, чем
приоритет операторов отношения и равенства; поэтому выражение вида
i < lim-1 && (c = getchar()) != '\n' && c != EOF
не нуждается в дополнительных скобках. Но, так как приоритет !=
выше, чем
приоритет присваивания, в
(c = getchar()) != '\n'
скобки необходимы, чтобы сначала выполнить присваивание, а затем сравнение с
'\n'
.
По определению численным результатом вычисления выражения отношения или логического является 1 в случае, если оно истинно, и 0 в случае, если оно ложно.
Унарный оператор ! преобразует ненулевой операнд в 0, а нуль в 1. Обычно оператор ! используют в конструкциях вида
if (!valid)
что эквивалентно
if (valid == 0)
Трудно сказать, какая из форм записи лучше. Конструкция вида !valid
хорошо
читается («если не valid
»), но в случае более сложных выражений может
оказаться, что ее не так-то легко понять.
Напишите цикл, эквивалентный приведенному выше for
-циклу, не
пользуясь операторами &&
и ||
.
Если операнды оператора принадлежат разным типам, то они приводятся к
некоторому общему типу. Приведение выполняется в соответствии с небольшим
числом правил. Обычно автоматически производятся лишь те преобразования,
которые без какой-либо потери информации превращают операнды с меньшим
диапазоном значений в операнды с большим диапазоном значений, как, например,
преобразование целого в число с плавающей точкой в выражении вроде f + i
.
Выражения, не имеющие смысла, например число с плавающей точкой в роли
индекса, не допускаются. Выражения, в которых могла бы теряться информация
(скажем, при присваивании длинных целых переменным более коротких типов или
при присваивании значений с плавающей точкой целым переменным), могут повлечь
предупреждение, но они допустимы.
Значения типа char
— это всего лишь малые целые, и их можно свободно
использовать в арифметических выражениях, что значительно облегчает
всевозможные манипуляции с литерами. В качестве примера приведем простенькую
реализацию функции atoi
, преобразующей последовательность цифр в ее числовой
эквивалент.
/* atoi: преобразование s в целое */ int atoi(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
, которая
одиночную литеру из набора ASCII, если она является заглавной буквой,
превращает в прописную. Если же литера не является заглавной буквой, lower
ее
не изменяет.
/* lower: преобразование c в строчную: только для ASCII */ int lower(int c) { if (c >= 'A' && c <= 'Z') return c + 'a' - 'A'; else return c; }
В случае ASCII эта программа будет правильно работать потому, что между одноименными буквами верхнего и нижнего регистров — одинаковое расстояние (если их рассматривать как числовые значения), и, кроме того, латинский алфавит — плотный в том смысле, что между буквами A и Z ничего кроме букв не существует. Для набора EBCDIC последнее условие не выполняется, и поэтому наша программа в этом случае будет преобразовывать не только буквы.
Стандартный головной файл <ctype.h>
, описанный в приложении B, определяет
семейство функций, которые позволяют проверять и преобразовывать литеры
независимо от набора литер. Например, функция tolower(c)
возвращает букву c
в
коде нижнего регистра, если она была в коде верхнего регистра, поэтому
tolower
— универсальная замена функции lower
, рассмотренной выше. Аналогично
проверку
c >= '0' && c <= '9'
можно заменить на
isdigit(c);
Далее мы будем пользоваться функциями из <ctype.h>
.
Существует одна тонкость, касающаяся преобразования литер в целые: язык не
определяет, являются ли переменные типа char
знаковыми или беззнаковыми. При
преобразовании char
в int
может когда-нибудь получиться отрицательное целое?
На машинах с разной архитектурой ответы могут отличаться. На некоторых
машинах значение типа char
с единичным старшим битом будет превращено в
отрицательное целое (посредством «размножения знака»). На других —
преобразование char
в int
осуществляется добавлением нулей слева, и, таким
образом, получаемое значение всегда положительно.
Гарантируется, что любая литера из стандартного набора печатаемых литер
никогда не будет отрицательным числом, поэтому в выражениях такие литеры
всегда являются положительными операндами. Но произвольный восьмибитовый код
в переменной типа char
на одних машинах может быть отрицательным числом, а на
других — положительным. Для совместимости переменные типа char
, в которых
хранятся нелитерные данные, следует специфицировать явно как signed
или
unsigned
.
Отношения типа i > j
и логические выражения, перемежаемые операторами &&
и
||
, определяют выражение — условие, которое имеет значение 1, если оно
истинно, и 0, если ложно. Так, присваивание
d = c >= '0' && c <= '9'
установит в d
значение 1, если c
есть цифра, и 0 в противном случае. Однако
функции, подобные isdigit
, в случае истины могут выдавать любое ненулевое
значение. В местах проверок внутри if
, while
, for
и т.д. «истина» просто
означает «не нуль».
Неявные арифметические преобразования, как правило, осуществляются
естественным образом. В общем случае, когда оператор типа +
или *
с двумя
операндами (бинарный оператор) имеет разнотипные операнды, прежде чем
операция начнет выполняться, «младший» тип подтягивается к «старшему».
Результат будет иметь старший тип. В разд. 6 приложения A правила
преобразования сформулированы точно. Если же в выражении нет беззнаковых
операндов, можно удовлетвориться следующим набором неформальных правил:
long double
, то другой
приводится к long double
.
double
, то другой приводится к double
.
float
, то другой приводится к float
.
char
и short
приводятся к int
.
long
, то другой приводится к
long
.
Заметим, что операнды типа float
не приводятся автоматически к типу double
; в
этом данная версия языка отличается от первоначальной. Вообще говоря,
математические функции, аналогичные собранным в библиотеке <math.h>
,
базируются на вычислениях с двойной точностью. В основном float
используется
для экономии памяти на больших массивах и менее часто для убыстрения счета на
тех машинах, где арифметика двойной точности слишком дорога.
Правила преобразования усложняются с появлением unsigned
-операндов. Проблема
в том, что сравнения знаковых и беззнаковых значений зависят от размеров
целых типов, которые на разных машинах могут отличаться. Предположим, что
значение типа int
занимает 16 бит, а значение типа long
— 32 бита. Тогда -1L
< 1U
, поскольку 1U
принадлежит типу int
и подтягивается к типу signed long
.
Но -1L > 1UL
, так как -1L
подтягивается к типу unsigned long
и воспринимается
как большое положительное число.
Преобразования имеют место и при присваиваниях: значение правой части присваивания приводится к типу левой части, который и является типом результата.
Литера превращается в целое посредством размножения знака или другим описанным выше способом.
Длинные целые преобразуются в короткие целые или в значения типа char
с
помощью отбрасывания старших разрядов. Так, в
int i; char c; i = c; c = i;
значение c
не изменится. Это справедливо независимо от того, размножается
знак при переводе char
в int
или нет. Однако, если изменить порядок
присваиваний, возможна потеря информации.
Если x
принадлежит типу float
, а i
типу int
, то и x = i
, и i = x
вызовут
преобразования, причем перевод float
в int
сопровождается отбрасыванием
дробной части. Если double
переводится в float
, то значение либо округляется,
либо обрезается; это зависит от реализации.
Так как аргумент в вызове функции есть выражение, при передаче его функции
также возможно преобразование типа. При отсутствии прототипа функции
аргументы типа char
и short
переводятся в int
, а float
— в double
. Вот почему
мы объявляли аргументы типа int
или double
даже тогда, когда в вызове функции
использовали аргументы типа char
или float
.
И наконец, для любого выражения можно явно указать преобразование его типа, используя унарный оператор, называемый приведением. Конструкция вида
(имя_типа) выражение
приводит выражение к указанному в скобках типу по перечисленным выше
правилам. Смысл операции приведения можно представить себе так: выражение
как бы присваивается некоторой переменной указанного типа, и эта переменная
используется вместо всей конструкции. Например, библиотечная программа sqrt
рассчитана на аргумент типа double
и выдает чепуху, если ей подсунуть
что-нибудь другое. (sqrt
описана в <math.h>
.) Поэтому, если n
есть целое, мы
можем написать
sqrt((double) n);
и перед тем, как значение n
будет передано функции, оно будет переведено в
double
. Заметим, что операция приведения всего лишь вырабатывает значение n
указанного типа, но саму переменную n
не затрагивает. Приоритет оператора
приведения столь же высок, как и любого унарного оператора, что зафиксировано
в таблице, показанной в конце этой главы.
В том случае, когда аргументы описаны в прототипе функции, как тому и следует
быть, при вызове функции нужное преобразование включается автоматически. Так,
при наличии прототипа функции sqrt
:
double sqrt(double);
перед обращением к sqrt
в присваивании
root2 = sqrt(2);
целое 2 будет переведено в значение double
2.0 автоматически без явного
указания операции приведения.
Операцию приведения проиллюстрируем на переносимой версии генератора псевдослучайных чисел и функции, осуществляющей начальную «затравку», входящих в стандартную библиотеку.
unsigned long int next = 1; /* rand: получает псевдослучайное целое 0..32767 */ int rand(void) { next = next * 1103515245 + 12345; return (unsigned int) (next / 65536) % 32768; } /* srand: устанавливает "затравку" для rand() */ void srand(unsigned int seed) { next = seed; }
Напишите функцию htoi(s)
, которая преобразует
последовательность шестнадцатиричных цифр, начинающуюся с 0x
или 0X
в
соответствующее целое. Шестнадцатиричными цифрами являются литеры 0...9
,
a... f
, A...F
.
В Си есть два необычных оператора, предназначенных для увеличения и
уменьшения переменных. Инкрементный оператор ++
добавляет 1 к своему
операнду, а декрементный оператор --
вычитает 1. Мы уже неоднократно
использовали ++
для наращивания значения переменных, как, например, в
if (c == '\n') ++nl;
Необычность ++
и --
в том, что их можно использовать и как префиксные
операторы (помещая перед переменной, например, ++n
), и как постфиксные
операторы (помещая после переменной: n++
). В обоих случаях значение n
увеличивается на 1. Но выражение ++n
увеличивает n
до того, как его значение
будет использовано, а n++
— после того. Предположим, что n
содержит 5, тогда
x = n++;
установит в x
значение 5, а
x = ++n;
установит в x
значение 6. И в том и другом случае значение n
станет равным 6.
Инкрементные и декрементные операторы можно применять только к переменным.
Например, запись (i+j)++
не верна. В контексте, где требуется только
увеличить (или уменьшить) значение переменной, как в
if (c == '\n') nl++;
безразлично, какой выбрать оператор — префиксный или постфиксный. Но
существуют ситуации, когда требуется оператор вполне определенного типа.
Например, рассмотрим функцию squeeze(s, c)
, которая удаляет из стринга s
все
литеры, совпадающие с c
:
/* squeese: удаляет все c из s */ void squeese(char s[], int c) { int i, j; for (i = j = 0; s[i] != '\0'; i++) if (s[i] != c) s[j++] = s[i]; s[i] = '\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
достаточно пространства, чтобы в нем разместить суммарный стринг. Мы написали
strcat
так, что она не возвращает никакого результата. На самом деле
библиотечная strcat
возвращает ссылку на результирующий стринг.
/* strcat: помещает t в конец s; s достаточно большой */ void strcat(char s[], char t[]) { int i, j; i = j = 0; while (s[i] != '\0') /* находим конец s */ i++; while ((s[i++] = t[j++]) != '\0' /* копируем t */ ; }
При копировании очередной литеры из t
в s
постфиксный оператор ++
применяется
и к i
, и к j
, чтобы на каждом шаге цикла переменные i
и j
правильно
отслеживали позиции перемещаемой литеры.
Напишите версию функции squeese(s1, s2)
, которая удаляет из
s1
все литеры, встречающиеся в стринге s2
.
Напишите функцию any(s1, s2)
, которая возвращает либо ту
позицию в s1
, где стоит первая литера, совпавшая с любой из литер в s2
, либо
-1 (если ни одна литера s1
не совпадает с литерами из s2
). (Стандартная
библиотечная функция strpbrk
делает то же самое, но выдает указатель на
литеру, а не номер ее позиции.)
В Си имеются шесть операторов для манипулирования с битами. Их можно
применять только к целочисленным операндам, т.е. к операндам типов char
,
short
, int
и long
, знаковым и беззнаковым.
& |
побитовое И |
| |
побитовое ИЛИ |
^ |
побитовое исключающее ИЛИ |
<< |
сдвиг влево |
>> |
сдвиг вправо |
~ |
побитовое отрицание (унарный) |
Оператор &
(побитовое И) часто используется для обнуления некоторой группы
разрядов. Например,
n = n & 0177;
очищает в n
все разряды, кроме младших семи.
Оператор |
(побитовое ИЛИ) применяют для установки разрядов; так,
x = x | SET_ON;
устанавливает единицы в тех разрядах x
, которым соответствуют единицы в
SET_ON
.
Оператор ^
(побитовое исключающее ИЛИ) в каждом разряде установит 1, если
соответствующие разряды операндов имеют различные значения, и 0, когда они
совпадают.
Поразрядные операторы &
и |
следует отличать от логических операторов &&
и
||
, которые при вычислении слева направо дают значение истинности. Например,
если x
есть 1, а y
равно 2, то x & y
даст нуль, а x && y
— единицу.
Операторы <<
и >>
выполняют сдвиг, влево или вправо, своего левого операнда
на число битовых позиций, задаваемое правым операндом, которое должно быть
положительным. Так, x << 2
сдвигает значение x
влево на 2 позиции, заполняя
освобождающиеся биты нулями, что эквивалентно умножению x
на 4. Сдвиг вправо
беззнаковой величины всегда сопровождается заполнением освобождающихся
разрядов нулями. Сдвиг вправо знаковой величины на одних машинах происходит с
размножением знака («арифметический сдвиг»), на других — с заполнением
освобождающихся разрядов нулями («логический сдвиг»).
Унарный оператор ~
производит дополнение целого до единиц по всем разрядам,
т. е. превращает единичные биты в нулевые и наоборот. Например,
x = x & ~077
обнуляет в x
последние 6 разрядов. Заметим, что запись x & ~077
не зависит от
длины слова, и, следовательно, она лучше, чем x & 0177700
, поскольку
последняя подразумевает, что x
занимает 16 бит. Независимая от машины форма
записи ~077
не потребует дополнительных затрат при счете, так как ~077
—
константное выражение, которое будет вычислено во время компиляции.
Для иллюстрации некоторых побитовых операций рассмотрим функцию
getbits(x, p, n)
, которая формирует поле в n
бит, вырезанное из x
, начиная с
позиции p
, прижимая его к правому краю. Предполагается, что 0-й бит — крайний
правый бит, а n
и p
— разумные положительные числа. Например, getbits(x, 4, 3)
вернет в качестве результата 4, 3 и 2-й биты значения x
прижимая их к правому
краю. Вот эта функция:
/* getbits: получает n бит, начиная с p-й позиции */ unsigned getbits(unsigned x, int p, int n) { return (x >> (p + 1 - n)) & ~(~0 << n); }
Выражение x >> (p+1-n)
сдвигает нужное нам поле к правому краю. Константа ~0
состоит только из единиц, и ее сдвиг влево на n
бит (~0 << n)
приведет к
тому, что правый край этой константы займут n
нулевых разрядов. Еще одна
операция побитового отрицания, ~(~0 << n)
, позволяет получить справа n
единиц.
Напишите функцию setbits(x, p, n, y)
, возвращающую значение x
,
в котором n
бит, начиная с p
-й позиции, заменены на n
правых разрядов из y
(остальные биты не изменяются).
Напишите функцию invert(x, p, n)
, возвращающую значение x
с
инвертированными n
битами, начиная с позиции p
(остальные биты не
изменяются).
Напишите функцию rightrot(x, n)
, которая циклически сдвигает
(«вращает») вправо x
на n
разрядов.
Выражение типа
i = i + 2
в котором стоящая слева переменная повторяется и справа, можно написать в сжатом виде:
i += 2
Оператор +=
называется оператором присваивания.
Большинству бинарных операторов (аналогичных +
и имеющих левый и правый
операнды) соответствуют операторы присваивания op=
, где op
— один из
операторов
+ - * / % << >> & ^ |
Если выр1 и выр2 — выражения, то запись
выр1 op= выр2;
эквивалентна записи
выр1 = (выр1) op (выр2);
с той лишь разницей, что выр1 вычисляется только один раз. Обратите внимание на скобки вокруг выр2: запись
x *= y + 1;
эквивалентна записи
x = x * (y + 1);но не
x = x * y + 1;
В качестве примера приведем функцию bitcount
, подсчитывающую число единичных
битов в своем аргументе целого типа.
/* bitcount: подсчет 1 в x */ int bitcount(unsigned x) { int b; for (b = 0; x != 0; x >>= 1) if (x & 01) b++; return b; }
Независимо от машины, на которой будет работать эта программа, описание
аргумента x
как unsigned
гарантирует, что при правом сдвиге освобождающиеся
биты будут заполняться нулями, а не знаковым битом.
Помимо краткости операторы присваивания обладают тем преимуществом, что они
более соответствуют тому, как человек мыслит. Мы говорим «прибавить 2 к i
»
или «увеличить i
на 2», а не «взять i
, добавить 2 и затем вернуть результат в
i
», так что выражение i += 2
лучше, чем i = i + 2
. Кроме того, в сложных
выражениях вроде
yyval[yypv[p3+p4] + yypv[p1+p2]] += 2
благодаря оператору присваивания запись становится более легкой для понимания, так как читателю при такой записи не потребуется старательно сравнивать два длинных выражения или выяснять, почему они не совпадают. Следует иметь в виду и то, что оператор присваивания может помочь компилятору сгенерировать более эффективный код.
Мы уже видели, что присваивание вырабатывает значение и может применяться внутри выражения; во многих рядовых программах мы видим
while ((c = getchar()) != EOF) ...
В выражениях встречаются и другие операторы присваивания (+=
, -=
и т.д.),
хотя и реже.
Типом и значением любого выражения присваивания являются тип и значение его левого операнда после завершения присваивания.
Применительно к числам, в представлении которых использован
дополнительный код, выражение x &= (x-1)
уничтожает самую правую 1 в x
.
Объясните, почему. Используйте это наблюдение при написании более быстрого
варианта функции bitcount
.
Инструкции
if (a > b) z = a; else z = b;
пересылают в z
максимальное из двух значений, a
и b
. Условное выражение,
написанное с помощью тернарного оператора «?:
», представляет собой другой
способ записи этой и подобных ей конструкций. В выражении
выр1 ? выр2 : выр3
первым вычисляется выражение выр1. Если его значение не нуль (истина), то
вычисляется выражение выр2, и значение этого выражения становится значением
всего условного выражения. В противном случае вычисляется выражение выр3, и
его значение становится значением условного выражения. Следует отметить, что
из выражений выр2 и выр3 вычисляется только одно из них. Таким образом, чтобы
установить в z
наибольшее из a
и b
, можно написать
z = (a > b) ? a : b; /* z = max (a, b) */
Следует заметить, что условное выражение и в самом деле является выражением,
и его можно использовать в любом месте, где допускается выражение. Если выр2
и выр3 принадлежат разным типам, то тип результата определяется правилами
преобразования, о которых шла речь в этой главе ранее. Например, если f
имеет
тип float
, а n
— тип int
, то типом выражения
(n > 0) ? f : n
будет float
вне зависимости от того, положительно значение n
или нет.
Заключать в скобки первое выражение в условном выражении не обязательно, так как приоритет ?: очень низкий (более низкий приоритет имеет только присваивание), однако мы рекомендуем всегда это делать, поскольку благодаря обрамляющим скобкам условие в выражении лучше воспринимается.
Условное выражение часто позволяет сократить программу. В качестве примера
приведем цикл, обеспечивающий печать n
элементов массива по 10 на каждой
строке с одним пробелом между колонками; каждая строка цикла, включая
последнюю, заканчивается литерой новая_строка:
for (i = 0; i < n; i++) printf("%6d%c", a[i], (i % 10 == 9 || i == n-1) ? '\n' : ' ');
Литера новая_строка посылается после каждого десятого и после n
-го элемента.
За всеми другими элементами следует пробел. Эта программа выглядит довольно
замысловато, зато она более компактна, чем эквивалентная программа с
использованием if-else
. Вот еще один пример:
printf("Вы имеете %d элемент %s.\n'', n, (n % 10 == 1 && n != 11) ? "" : ((n < 10 || n > 20) && n % 10 = 2 && n % 10 <= 4) ? "а" : "ов");
Напишите функцию lower
, которая переводит большие буквы в
малые, используя условное выражение (вместо if-else
).
В табл. 2.1 показаны приоритеты и порядок вычислений всех операторов, включая
и те, которые мы еще не рассматривали. Операторы, перечисленные на одной
строке, имеют одинаковый приоритет; строки упорядочены по убыванию
приоритетов; так, например, *
, /
и %
имеют одинаковый приоритет, который
выше, чем приоритет бинарных +
и -
. «Оператор» ()
обозначает вызов функции.
Операторы ->
и .
(точка) обеспечивают доступ к элементам структур; о них
пойдет речь в гл. 6, там же будет рассмотрен и оператор sizeof
(размер
объекта). Операторы *
(адресация по указателю) и &
(получение адреса
объекта) обсуждаются в гл. 5. Оператор «запятая» будет рассмотрен в гл. 3.
Операторы | Выполняются |
---|---|
() [] -> . |
слева направо |
! ~ ++ -- + - * & (тип) sizeof |
справа налево |
* / % |
слева направо |
+ - |
слева направо |
<< >> |
слева направо |
< <= > >= |
слева направо |
== != |
слева направо |
& |
слева направо |
^ |
слева направо |
| |
слева направо |
&& |
слева направо |
|| |
слева направо |
?: |
справа налево |
= += -= *= /= %= &= ^= |= <<= >>= |
справа налево |
, |
слева направо |
Унарные операторы + , - и * имеют более высокий приоритет,чем те же операторы в бинарном варианте |
Заметим, что приоритеты побитовых операторов &
, ^
и |
ниже, чем приоритет ==
и !=
, из-за чего в побитовых проверках типа
if ((x & MASK) == 0)
чтобы получить правильный результат, приходится использовать скобки.
Си подобно многим языкам не фиксирует порядок вычисления операндов оператора
(за исключением &&
, ||
, ?:
и «,
»). Например, в инструкции вида
x = 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++;
возникает вопрос: массив a
индексируется старым или измененным значением i
?
Компиляторы могут по-разному генерировать программу, что проявится в
интерпретации данной записи. Стандарт сознательно устроен так, что
большинство подобных вопросов оставлено на усмотрение компиляторов, так как
лучший порядок вычислений определяется архитектурой машины. Стандартом только
гарантируется, что все побочные эффекты при вычислении аргументов проявятся
перед входом в функцию. Правда, в примере с printf
это нам не поможет.
Мораль такова: писать программы, которые зависят от порядка вычислений, — плохая практика, какой бы язык вы ни использовали. Естественно, надо знать, чего следует избегать, но если вы не знаете, как образуются побочные эффекты на вашей машине, то лучше и не рассчитывать на особенности реализации.