Б. Керриган, Д. Ритчи Язык программирования C. Б. Керниган, Д. зык программирования и . Издание 3е, исправленное Перевод с английского под редакцией Вс. С. Штаркмана СанктПетербург 2003
Скачать 31.48 Mb.
|
60 Глава 2. Типы, операторы и выражения Имена в различных перечислениях должны отличаться друг от друга. Значения внутри одного перечисления могут Средство обеспечивает удобный способ присвоить константам имена, причем в отличие от ine значения констант при этом способе могут генерироваться автоматически. Хотя разрешается объявлять пере- менные типа enum, однако компилятор не обязан контролировать, входят ли присваиваемые этим переменным значения в их тип. Но сама возмож- ность такой проверки часто делает enum лучше, чем ine. Кроме того, отладчик получает возможность печатать значения переменных типа в символьном виде. 2.4. Объявления Все переменные должны быть объявлены раньше, чем будут использо- при этом некоторые объявления могут быть получены неявно - из контекста. Объявление специфицирует тип и содержит список из од- ной или нескольких переменных этого типа, как, например, в int lower, upper, step; char с, line Переменные можно распределять по объявлениям произвольным обра- зом, что указанные выше списки можно записать и в следующем виде: int lower; int upper; int step; char c; char Последняя форма записи занимает больше места, тем не менее она луч- ше, поскольку позволяет добавлять к каждому объявлению комментарий. Кроме того, она более удобна для последующих модификаций. В своем объявлении переменная может быть инициализирована, как, например: char esc = int i = 0; int limit = float eps = Инициализация неавтоматической переменной осуществляется толь- ко один раз - перед тем, как программа начнет выполняться, при этом начальное значение должно быть константным выражением. Явно ини- 2.5. Арифметические операторы автоматическая переменная получает начальное значение каждый раз при входе в функцию или блок, ее начальным значением мо- жет быть любое выражение. Внешние и статические переменные по умол- чанию получают нулевые значения. Автоматические переменные, явным образом не инициализированные, содержат неопределенные значения ("мусор"). К любой переменной в объявлении может быть применен тор const для указания того, что ее значение далее не будет изменяться. const double e = const char = Применительно к массиву const указывает на то, что ни один из элементов не меняться. Указание const можно также применять к аргументу-массиву, чтобы сообщить, что функция не изме- няет этот массив: int strlen(const char[] ); Реакция на попытку изменить переменную, помеченную const, зависит от реализации компилятора. 2.5. Арифметические операторы Бинарными (т. е. с двумя арифметическими оператора- ми являются +, -, *, /, а также оператор деления по модулю %. Деление целых сопровождается отбрасыванием дробной части, какой бы она ни была. Выражение дает остаток от деления х на у и, следовательно, нуль, если х делится на у нацело. Например, год является високосным, если он делится на 4, но не де- лится на Кроме год является високосным, если он делится на 400. Следовательно, if % 4 == 0 year % 100 != О ! i year % 400 == 0) високосный year); else невисокосный year); Оператор % к операндам типов и double не применяется. В какую сторону (в сторону увеличения или уменьшения числа) будет усечена дробная часть при выполнении / и каким будет знак результата операции % с отрицательными операндами, зависит от машины. 62 Глава 2. Типы, операторы и выражения Бинарные операторы + и - имеют одинаковый приоритет, который ниже приоритета операторов / и %, который в свою очередь ниже приоритета унарных операторов + и -. Арифметические операции одного приоритет- ного уровня выполняются слева направо. В конце этой главы (параграф 2.12) приводится таблица 2.1, в которой представлены приоритеты всех операторов и очередность их выполнения. 2.6. Операторы отношения и логические операторы Операторами отношения являются Все они имеют одинаковый приоритет. Сразу за ними идет приоритет операторов сравнения на равенство: Операторы отношения имеют более низкий приоритет, чем арифмети- ческие, поэтому выражение вроде i < будет выполняться так же, как i < т. е. как мы и ожидаем. Более интересны логические операторы и Выражения, между ко- торыми стоят операторы && или ! вычисляются слева направо. Вы- числение прекращается, как только становится известна истинность или ложность результата. Многие Си-программы опираются на это свойство, как, например, цикл из функции getline, которую мы приводили в главе 1: for (i = 0; i < lim-1 && (с = != EOF && с != ++i) s[i] c; Прежде чем читать очередной нужно проверить, есть ли для него место в s, иначе говоря, сначала необходимо проверить соблюде- ние < lim-1. Если это условие не выполняется, мы не должны продолжать вычисление, в частности читать следующий символ. Так же было бы неправильным сравнивать с и EOF до обращения к следо- вательно, и вызов getchar, и присваивание должны выполняться перед указанной проверкой. Приоритет оператора && выше, чем таковой оператора однако их при- оритеты ниже, чем приоритет операторов отношения и равенства. Из ска- занного следует, что выражение вида i < lim-1 && (с = && с EOF не нуждается в дополнительных скобках. Но, так как приоритет ! = выше, 2.7. типов 63 чем приоритет присваивания, в (с ! = скобки необходимы, чтобы сначала выполнить присваивание, а затем срав- нение с ' По определению численным результатом вычисления выражения от- ношения или логического выражения является если оно истинно, и О, если оно ложно. Унарный оператор ! преобразует ненулевой операнд в 0, а нуль в 1. Обычно оператор ! используют в конструкциях вида if что эквивалентно if (valid == 0) Трудно сказать, какая из форм записи лучше. Конструкция вида ! valid хорошо читается ("если не но в более сложных выражениях может оказаться, что ее не так-то легко понять. Упражнение 2.2. Напишите эквивалентный приведенному выше f не пользуясь операторами && и 2.7. Преобразования типов Если операнды оператора принадлежат к разным типам, то они приво- дятся к некоторому общему типу. Приведение выполняется в соответствии с небольшим числом правил. Обычно автоматически производятся лишь те преобразования, которые без какой-либо потери информации превра- щают операнды с меньшим диапазоном значений в операнды с большим диапазоном, как, например, преобразование целого в число с плавающей точкой в выражении вроде f + i. Выражения, не имеющие смысла, напри- мер число с плавающей точкой в роли индекса, не допускаются. Выраже- ния, в которых могла бы теряться информация (скажем, при присваива- нии длинных целых переменным более коротких типов или при присваи- вании значений с плавающей точкой целым переменным), могут повлечь за собой предупреждение, но они Значения типа - это просто малые целые, и их можно свободно использовать в арифметических выражениях, что значительно облегчает всевозможные манипуляции с символами. В качестве примера приведем простенькую реализацию функции преобразующей последователь- ность цифр в ее числовой эквивалент. 64 Глава 2. Типы, и выражения /* atoi: преобразование s в целое */ int atoi(char { int i, n; n = 0; for (i = 0; s[i] >= && s[i] <= ++i) n = 10 * n + (s[i] - return n; Как мы уже говорили в главе выражение s[i] дает числовое значение символа, в так как значения ' и пр. образуют непрерывную возрастающую последовательность. Другой пример приведения к int связан с функцией кото- рая одиночный символ из набора ASCII, если он является заглавной бук- вой, превращает в строчную. Если же символ не является заглавной бук- вой, его не изменяет. /* lower: преобразование с в строчную; только для ASCII */ int lower(int с) { if (с >= && с <= return с + - else return с; } В случае ASCII эта программа будет работать правильно, потому что меж- ду одноименными буквами верхнего и нижнего регистров - одинаковое расстояние (если их рассматривать как числовые значения). Кроме того, латинский алфавит — плотный, т. е. между буквами А и Z расположены только буквы. Для набора EBCDIC последнее условие не выполняется, и поэтому наша программа в этом случае будет преобразовывать не толь- ко буквы. Стандартный заголовочный файл описанный в приложе- нии В, определяет семейство функций, которые позволяют проверять и преобразовывать символы независимо от символьного набора. Напри- мер, функция возвращает букву с в коде нижнего регистра, если она была в коде верхнего регистра, поэтому tolower - универсальная за- мена функции рассмотренной выше. Аналогично проверку с >= && с <= 2.7. Преобразования типов - 65 можно заменить на Далее мы будем пользоваться функциями из h>. Существует одна тонкость, касающаяся преобразования символов в целые числа: язык не определяет, являются ли переменные типа char знаковыми или беззнаковыми. При преобразовании char в int может ли когда-нибудь получиться отрицательное целое? На машинах с разной ар- хитектурой ответы могут отличаться. На некоторых машинах значение типа char с единичным старшим битом будет превращено в отрицатель- ное целое (посредством "распространения знака"). На других - преобра- зование char в int осуществляется добавлением нулей слева, и, таким образом, получаемое значение всегда положительно. Гарантируется, что любой символ из стандартного набора печатаемых символов никогда не будет отрицательным числом, поэтому в выражени- ях такие символы всегда являются положительными операндами. извольный восьмибитовый код в переменной типа на одних маши- нах может быть отрицательным числом, а на других - положительным. Для совместимости переменные в которых хранятся несимволь- ные данные, следует специфицировать явно как signed или unsigned. Отношения вроде i > j и логические выражения, перемежаемые опера- торами && и ! определяют выражение-условие, которое имеет значение если оно истинно, и 0, если ложно. Так, присваивание d = с >= && с <= установит d в значение если с есть цифра, и 0 в противном случае. Одна- ко функции, подобные isdigit, в качестве истины могут выдавать любое ненулевое значение. В местах проверок внутри while, for и пр. "исти- на" просто означает "не нуль". Неявные арифметические преобразования, как правило, осуществля- ются естественным образом. В общем случае, когда оператор вроде + или с двумя операндами (бинарный оператор) имеет разнотипные операн- ды, прежде чем операция начнет выполняться, "низший" тип повышается до "высшего". Результат будет иметь высший тип. В параграфе 6 приложе- ния А правила преобразования сформулированы точно. Если же в выра- жении нет беззнаковых операндов, можно удовлетвориться следующим набором неформальных правил: • Если какой-либо из операндов принадлежит типу long double, то и дру- гой приводится к long double. • В противном случае, если какой-либо из операндов принадлежит типу double, то и другой приводится к double. 1116 66 Глава 2. Типы, операторы и выражения • В противном если какой-либо из операндов принадлежит типу то и другой приводится к • В противном случае операнды типов char и s h o r t int. • И наконец, если один из операндов типа long, то и другой приводится к long. Заметим, что операнды типа не приводятся автоматически к типу double; в этом данная версия языка отличается от первоначальной. Вооб- ще говоря, математические функции, аналогичные собранным в теке h>, базируются на вычислениях с двойной точностью. В основ- ном используется для экономии памяти на больших массивах и не так часто - для ускорения счета на тех машинах, где арифметика с двой- ной точностью слишком дорога с точки зрения расхода времени и памяти. Правила преобразования усложняются с появлением операндов типа unsigned. Проблема в том, что сравнения знаковых и беззнаковых значе- ний зависят от размеров целочисленных типов, которые на разных маши- нах могут отличаться. Предположим, что значение типа int занимает 16 битов, а значение типа long - 32 бита. Тогда < поскольку при- надлежит типу unsigned int и повышается до типа signed long. Но > повышается типа unsigned long и воспринимается как боль- шое положительное число. Преобразования имеют место и при присвоениях: значение правой ча- сти присвоения приводится к типу левой части, который и является ти- пом результата. Тип превращается путем распространения знака или другим описанным выше способом. Тип long i n t преобразуются в short int или в значения типа char пу- тем отбрасывания старших разрядов. Так, в int i; char с; i = с; с = i; значение с не изменится. Это справедливо независимо от того, распрост- раняется знак при переводе c h a r в int или нет. Однако, если изменить очередность присваиваний, потеря информации. Если х принадлежит типу a i - типу int, то и х = i, и i = x вызовут преобразования, причем перевод float в int сопровождается отбрасыва- нием дробной части. Если double переводится во то значение либо округляется, либо обрезается; это зависит от реализации. Так как аргумент в вызове функции есть выражение, при передаче его функции также возможно преобразование типа. При отсутствии прото- 2.7. Преобразования типов 67 типа функции аргументы типа c h a r и short переводятся в int, a - в double. Вот почему мы объявляли аргументы типа int или double даже тогда, когда в вызове функции использовали аргументы типа char или float. И наконец, для любого выражения можно явно ("насильно") указать преобразование его типа, используя унарный оператор, называемый при- ведением. Конструкция вида выражение приводит выражение к указанному в скобках типу по перечисленным выше правилам. Смысл операции приведения можно представить себе так: выражение как бы присваивается некоторой переменной указанного и эта переменная используется вместо всей конструкции. Например, би- блиотечная функция рассчитана на аргумент типа double и выдает чепуху, если ей подсунуть что-нибудь другое описана в Поэтому, если п имеет целочисленный тип, мы можем написать sqrt((double) n) и перед тем, как значение п будет передано функции, оно будет переведе- но в double. Заметим, что операция приведения всего лишь вырабатывает значение п указанного типа, но саму переменную п не затрагивает. При- оритет оператора приведения столь же высок, как и любого унарного опе- ратора, что зафиксировано в таблице, помещенной в конце этой главы. В том случае, когда аргументы описаны в прототипе функции, тому и следует быть, при вызове функции нужное преобразование выполняет- ся автоматически. Так, при наличии прототипа функции sq double sqrt(double); перед обращением к sq rt в присваивании root2 = sqrt(2); целое 2 будет переведено в значение автоматически без явного указания операции приведения. Операцию приведения проиллюстрируем на переносимой версии ге- нератора псевдослучайных чисел и функции, инициализирующей "семя". И генератор, и функция входят в стандартную библиотеку. unsigned long int next = 1; /* rand: возвращает псевдослучайное целое */ int rand(void) { next = next * 1103515245 + 12345; 68 _ Глава 2. Типы, операторы и выражения return (unsigned int)(next/65536) % 32768; /* srand: устанавливает "семя" для rand() */ void srand(unsigned int seed) < next = seed; } Упражнение 2.З. Напишите функцию h t o l ( s ) , которая преобразует последовательность цифр, начинающуюся с Ох или ОХ, в соответствующее целое. цифрами являются символы f, F. 2.8. Операторы инкремента и декремента В Си есть два необычных оператора, предназначенных для увеличения и уменьшения переменных. Оператор инкремента ++ добавляет 1 к своему операнду, а оператор декремента — вычитает 1. Мы уже неоднократно ис- пользовали ++ для наращивания значения переменных, как, например, в if (с == Необычность операторов ++ и — в том, что их можно использовать и как префиксные (помещая перед переменной: и как постфиксные (помещая после переменной: п++) В обоих случаях значение п увеличивается на 1, но выражение ++п увеличивает п до как его зна- чение будет использовано, а п++ - после того. Предположим, что п со- держит 5, тогда х = п++; - установит х в значение 5, а х = ++п; установит х в значение 6. И в том и другом случае п станет равным 6. Опе- раторы инкремента и декремента можно применять только к переменным. Выражения вроде )++ недопустимы. Если требуется только увеличить или уменьшить значение перемен- ной (но не получить ее значение), как например (С == 2.8. Операторы инкремента и декремента _ _ 69 то безразлично, какой оператор выбрать - префиксный или постфикс- ный. Но существуют ситуации, когда требуется оператор вполне опреде- ленного типа. Например, рассмотрим функцию с), которая уда- ляет из строки s все символы, совпадающие с с: /* squeeze: удаляет все с из s */ void int с) { int i, j; for (i = j = 0; s[i] != if (s[i] != c) Каждый раз, когда встречается символ, отличный от с, он копируется в текущую j -ю позицию, и только после этого переменная j увеличива- ется на подготавливаясь таким образом к приему следующего символа. Это в точности совпадает со следующими действиями: if != с) { Другой пример - функция getline, которая нам известна по главе 1. Приведенную там запись if (с == { s[i] = с; можно переписать более компактно: if (с == s[i++] = с; В качестве третьего примера рассмотрим стандартную функцию st rcat которая строку t помещает в конец строки s. что в s достаточно места, чтобы разместить там суммарную строку. Мы написали st так, что она не возвращает никакого результата. На самом деле би- блиотечная st возвращает указатель на результирующую строку. /* strcat: помещает t в конец s; s достаточно велика */ strcat (char s[], char 70 _ Глава 2. Типы, операторы и выражения i = j = 0; while (s[i] != /* находим конец s */ i++; while = != /* копируем t */ При копировании очередного символа из t в s постфиксный оператор ++ применяется и к i, и к j, чтобы на каждом шаге цикла переменные i и j правильно отслеживали позиции перемещаемого символа. Упражнение 2.4. Напишите версию функции которая удаляет из s1 все символы, встречающиеся в строке s2. Упражнение 2.5. Напишите функцию которая возвращает либо ту позицию в где стоит первый символ, совпавший с любым из символов в s2, либо -1 (если ни один символ из s1 не совпадает с символами из s2). (Стандартная библиотечная функция делает то же самое, но выдает не номер позиции символа, а указатель на символ.) |