Если рассматриваемая переменная является внешней или статической, то инициализация
проводится только один раз, согласно концепции до начала выполнения программы. Инициали-
зируемым явно автоматическим переменным начальные значения присваиваются при каждом
обращении к функции, в которой они описаны. Автоматические переменные, не инициализируемые
яв- но, имеют неопределенные значения, (т.е. мусор). Внешние и статические переменные
по умолчанию инициализируются нулем, но, тем не менее, их явная инициализация является
признаком хорошего стиля.
Мы продолжим обсуждение вопросов инициализации, когда будем описывать новые типы
данных.
2.5. Арифметические операции
Бинарными арифметическими операциями являются +, -, *, / и операция деления по
модулю %. Имеется унарная операция -, но не существует унарной операции +.
При делении целых дробная часть отбрасывается. Выражение
X % Y
дает остаток от деления X на Y и, следовательно, равно нулю, когда х делится
на Y точно. Например, год является високос- ным, если он делится на 4, но не делится
на 100, исключая то, что делящиеся на 400 годы тоже являются високосными. По- этому
IF(YEAR % 4 == 0 && YEAR % 100 != 0 \!\! YEAR % 400 == 0)
год високосный ELSE
год невисокосный
Операцию % нельзя использовать с типами FLOAT или DOUBLE.
Операции + и - имеют одинаковое старшинство, которое младше одинакового уровня
старшинства операций *, / и %, ко- торые в свою очередь младше унарного минуса.
Арифметические операции группируются слева направо. (Сведения о старшинстве и ассоциативности
всех операций собраны в таблице в конце этой главы). Порядок выполнения ассоциативных
и коммутатив- ных операций типа + и - не фиксируется; компилятор может пе- регруппировывать
даже заключенные в круглые скобки выраже- ния, связанные такими операциями. таким
образом, а+(B+C) мо- жет быть вычислено как (A+B)+C. Это редко приводит к како-
му-либо расхождению, но если необходимо обеспечить строго определенный порядок,
то нужно использовать явные промежу- точные переменные.
Действия, предпринимаемые при переполнении и антипере- полнении (т.е. При получении
слишком маленького по абсолют- ной величине числа), зависят от используемой машины.
2.6. Операции отношения и логические операции
Операциями отношения являются
=> > =< <
все они имеют одинаковое старшинство. Непосредственно за ни- ми по уровню старшинства
следуют операции равенства и нера- венства:
== !=
которые тоже имеют одинаковое старшинство. операции отноше- ния младше арифметических
операций, так что выражения типа 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
: прежде чем проверять символ, его нужно считать.
Старшинство операции && выше, чем у \!\!, и обе они младше операций отношения
и равенства. Поэтому такие выраже- ния, как
I<LIM-1 && (C = GETCHAR()) != '\N' && C != EOF
не нуждаются в дополнительных круглых скобках. Но так как операция != старше
операции присваивания, то для достижения правильного результата в выражении
(C = GETCHAR()) != '\N'
кобки необходимы.
Унарная операция отрицания ! Преобразует ненулевой или истинный операнд в 0,
а нулевой или ложный операнд в 1. Обычное использование операции ! Заключается в
записи
IF( ! INWORD )
Вместо
IF( INWORD == 0 )
Tрудно сказать, какая форма лучше. Конструкции типа ! INWORD Читаются довольно
удобно ("если не в слове"). Но в более сложных случаях они могут оказаться трудными
для понимания.
Упражнение 2-1
Напишите оператор цикла, эквивалентный приведенному выше оператору FOR, не используя
операции &&.
2.7. Преобразование типов
Если в выражениях встречаются операнды различных типов, то они преобразуются
к общему типу в соответствии с неболь- шим набором правил. В общем, автоматически
производятся только преобразования, имеющие смысл, такие как, например, преобразование
целого в плавающее в выражениях типа F+I. Вы- ражения же, лишенные смысла, такие
как использование пере- менной типа FLOAT в качестве индекса, запрещены.
Во-первых, типы CHAR и INT могут свободно смешиваться в арифметических выражениях:
каждая переменная типа CHAR авто- матически преобразуется в INT. Это обеспечивает
значительную гибкость при проведении определенных преобразований симво- лов. Примером
может служить функция ATOI, которая ставит в соответствие строке цифр ее численный
эквивалент.
ATOI(S) /* CONVERT S TO 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);
}
KAK Уже обсуждалось в главе 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, потому что численные значения,
соответствующие в этом коде прописным и строчным буквам, отличаются на постоянную
величину, а каждый алфавит является сплошным - между а и Z нет ничего, кроме букв.
Это последнее замечание для набора символов EBCDIC систем IBM 360/370 оказывается
несправедливым, в силу чего эта программа на таких системах работает неправильно
- она преобразует не только буквы.
При преобразовании символьных переменных в целые возни- кает один тонкий момент.
Дело в том, что сам язык не указы- вает, должны ли переменным типа CHAR соответствовать
числен- ные значения со знаком или без знака. Может ли при преобра- зовании CHAR
в INT получиться отрицательное целое? К сожале- нию, ответ на этот вопрос меняется
от машины к машине, отра- жая расхождения в их архитектуре. На некоторых машинах
(PDP-11, например) переменная типа CHAR, крайний левый бит которой содержит 1, преобразуется
в отрицательное целое ("знаковое расширение"). На других машинах такое преобразо-
вание сопровождается добавлением нулей с левого края, в ре- зультате чего всегда
получается положительное число.
Определение языка "C" гарантирует, что любой символ из стандартного набора символов
машины никогда не даст отрица- тельного числа, так что эти символы можно свободно
использо- вать в выражениях как положительные величины. Но произволь- ные комбинации
двоичных знаков, хранящиеся как символьные переменные на некоторых машинах, могут
дать отрицательные значения, а на других положительные.
Наиболее типичным примером возникновения такой ситуации является сучай, когда
значение 1 используется в качестве EOF. Рассмотрим программу
CHAR C;
C = GETCHAR();
IF ( C == EOF )
...
На машине, которая не осуществляет знакового расширения, переменная 'с' всегда
положительна, поскольку она описана как 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;
в "C" вся плавающая арифметика выполня- ется с двойной точностью.
Преобразования возникают и при присваиваниях; значение правой части преобразуется
к типу левой, который и является типом результата. Символьные переменные преобразуются
в це- лые либо со знаковым расширением ,либо без него, как описано выше. Обратное
преобразование INT в CHAR ведет себя хорошо - лишние биты высокого порядка просто
отбрасываются. Таким об- разом
INT I; CHAR C;
I = C; C = I;
значение 'с' не изменяется. Это верно независимо от того, вовлекается ли знаковое
расширение или нет.
Если х типа FLOAT, а I типа INT, то как
х = I; так и
I = х;
приводят к преобразованиям; при этом 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.
2.8. Операции увеличения и уменьшения
В языке "C" предусмотрены две необычные операции для увеличения и уменьшения
значений переменных. Операция увели- чения ++ добавляет 1 к своему операнду, а операция
уменьше- ния -- вычитает 1. Мы часто использовали операцию ++ для увеличения переменных,
как, например, в
IF(C == '\N')
++I;
Необычный аспект заключается в том, что ++ и -- можно использовать либо как префиксные
операции (перед переменной, как в ++N), либо как постфиксные (после переменной:
N++). Эффект в обоих случаях состоит в увеличении N. Но выражение ++N увеличивает
переменную N до использования ее значения, в то время как N++ увеличивает переменную
N после того, как ее значение было использовано. Это означает, что в контексте,
где используется значение переменной, а не только эффект увеличения, использование
++N и N++ приводит к разным ре- зультатам. Если N = 5, то
х = N++;
устанавливает х равным 5, а
х = ++N;
полагает х равным 6. В обоих случаях N становится равным 6. Операции увеличения
и уменьшения можно применять только к переменным; выражения типа х=(I+J)++ являются
незаконными.