Язык программирования Си Брайан Керниган, Деннис Ритчи 3е издание Версия 1 Table of Contents
Скачать 2.33 Mb.
|
2.4. Объявления Все переменные должны быть объявлены раньше, чем будут использоваться, при этом некоторые объявления могут быть получены неявно — из контекста. Объявление специфицирует тип и содержит список из одной или нескольких переменных этого типа, как, например, в int lower, upper, step; char с, 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; Инициализация неавтоматической переменной осуществляется только один раз — перед тем, как программа начнет выполняться, при этом начальное значение должно быть константным выражением. Явно инициализируемая автоматическая переменная получает начальное значение каждый раз при входе в функцию или блок, ее начальным значением может быть любое выражение. Внешние и статические переменные по умолчанию получают нулевые значения. Автоматические переменные, явным образом не инициализированные, содержат неопределенные значения ("мусор"). 5 От английского слова enumeration — перечисление. — Примеч. ред. К любой переменной в объявлении может быть применен квалификатор const для указания того, что ее значение далее не будет изменяться. const double e = 2.71828182845905; const char msg[] = "предупреждение: "; Применительно к массиву квалификатор const указывает на то, что ни один из его элементов не будет меняться. Указание const можно также применять к аргументу-массиву, чтобы сообщить, что функция не изменяет этот массив: int strlen(const char[] ); Реакция на попытку изменить переменную, помеченную квалификатором const , зависит от реализации компилятора. 2.5. Арифметические операторы Бинарными (т. е. с двумя операндами) арифметическими операторами являются + , - , * , / , а также оператор деления по модулю % . Деление целых сопровождается отбрасыванием дробной части, какой бы она ни была. Выражение дает остаток от деления х на y и, следовательно, нуль, если х делится на 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.12) приводится таблица 2.1, в которой представлены приоритеты всех операторов и очередность их выполнения. 2.6. Операторы отношения и логические операторы Операторами отношения являются > >= < <= Все они имеют одинаковый приоритет. Сразу за ними идет приоритет операторов сравнения на равенство: == != Операторы отношения имеют более низкий приоритет, чем арифметические, поэтому выражение вроде i < lim-1 будет выполняться так же, как i < (lim-1) , т. е. как мы и ожидаем. Более интересны логические операторы && и || . Выражения, между которыми стоят операторы && или || , вычисляются слева направо. Вычисление прекращается, как только становится известна истинность или ложность результата. Многие Си-программы опираются на это свойство, как, например, цикл из функции getline , которую мы приводили в главе 1: for (i = 0; i < lim-1 && (с = getchar()) != EOF && с != '\n'; ++i) s[i] = c; Прежде чем читать очередной символ, нужно проверить, есть ли для него место в массиве s , иначе говоря, сначала необходимо проверить соблюдение условия i < lim-1 . Если это условие не выполняется, мы не должны продолжать вычисление, в частности читать следующий символ. Так же было бы неправильным сравнивать с и EOF до обращения к getchar ; следовательно, и вызов getchar , и присваивание должны выполняться перед указанной проверкой. Приоритет оператора && выше, чем таковой оператора || , однако их приоритеты ниже, чем приоритет операторов отношения и равенства. Из сказанного следует, что выражение вида i < lim-1 && (с = getchar()) != '\n' && с != EOF не нуждается в дополнительных скобках. Но, так как приоритет != выше, чем приоритет присваивания, в (с = getchar()) ! = '\n' скобки необходимы, чтобы сначала выполнить присваивание, а затем сравнение с '\n' По определению численным результатом вычисления выражения отношения или логического выражения является 1, если оно истинно, и 0, если оно ложно. Унарный оператор ! преобразует ненулевой операнд в 0, а нуль в 1. Обычно оператор ! используют в конструкциях вида if (!valid) что эквивалентно if (valid == 0) Трудно сказать, какая из форм записи лучше. Конструкция вида !valid хорошо читается ("если не правильно"), но в более сложных выражениях может оказаться, что ее не так-то легко понять. Упражнение 2.2. Напишите цикл, эквивалентный приведенному выше fоr -циклу, не пользуясь операторами && и || 2.7. Преобразования типов Если операнды оператора принадлежат к разным типам, то они приводятся к некоторому общему типу. Приведение выполняется в соответствии с небольшим числом правил. Обычно автоматически производятся лишь те преобразования, которые без какой-либо потери информации превращают операнды с меньшим диапазоном значений в операнды с большим диапазоном, как, например, преобразование целого в число с плавающей точкой в выражении вроде 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: преобразование с в строчную; только для ASCII */ int lower(int с) { if (с >= 'А' && с <= 'Z' ) return с + 'а' - 'А'; else return с; } В случае ASCII эта программа будет работать правильно, потому что между одноименными буквами верхнего и нижнего регистров — одинаковое расстояние (если их рассматривать как числовые значения). Кроме того, латинский алфавит — плотный, т. е. между буквами А и Z расположены только буквы. Для набора EBCDIC последнее условие не выполняется, и поэтому наша программа в этом случае будет преобразовывать не только буквы. Стандартный заголовочный файл , описанный в приложении B, определяет семейство функций, которые позволяют проверять и преобразовывать символы независимо от символьного набора. Например, функция tolower(c) возвращает букву c в коде нижнего регистра, если она была в коде верхнего регистра, поэтому tolower — универсальная замена функции lower , рассмотренной выше. Аналогично проверку с >= '0' && с <= '9' можно заменить на isdigit(c) Далее мы будем пользоваться функциями из Существует одна тонкость, касающаяся преобразования символов в целые числа: язык не определяет, являются ли переменные типа char знаковыми или беззнаковыми. При преобразовании char в int может ли когда-нибудь получиться отрицательное целое? На машинах с разной архитектурой ответы могут отличаться. На некоторых машинах значение типа char с единичным старшим битом будет превращено в отрицательное целое (посредством "распространения знака"). На других преобразование char в int осуществляется добавлением нулей слева, и, таким образом, получаемое значение всегда положительно. Гарантируется, что любой символ из стандартного набора печатаемых символов никогда не будет отрицательным числом, поэтому в выражениях такие символы всегда являются положительными операндами. Непроизвольный восьмибитовый код в переменной типа char на одних машинах может быть отрицательным числом, а на других — положительным. Для совместимости переменные типа char , в которых хранятся несимвольные данные, следует специфицировать явно как signed или unsigned Отношения вроде i > j и логические выражения, перемежаемые операторами && и || , определяют выражение-условие, которое имеет значение 1, если оно истинно, и 0, если ложно. Так, присваивание d = с >= '0' && с <= '9' установит d в значение 1, если с есть цифра, и 0 в противном случае. Однако функции, подобные isdigit , в качестве истины могут выдавать любое ненулевое значение. В местах проверок внутри if , while , for и пр. "истина" просто означает "не нуль". Неявные арифметические преобразования, как правило, осуществляются естественным образом. В общем случае, когда оператор вроде + или * с двумя операндами (бинарный оператор) имеет разнотипные операнды, прежде чем операция начнет выполняться, "низший" тип повышается до "высшего". Результат будет иметь высший тип. В параграфе 6 приложения А правила преобразования сформулированы точно. Если же в выражении нет беззнаковых операндов, можно удовлетвориться следующим набором неформальных правил: Если какой-либо из операндов принадлежит типу long double , то и другой приводится к long double В противном случае, если какой-либо из операндов принадлежит типу double , то и другой приводится к double В противном случае, если какой-либо из операндов принадлежит типу float , то и другой приводится к float В противном случае операнды типов char и short приводятся к int И наконец, если один из операндов типа long , то и другой приводится к long Заметим, что операнды типа float не приводятся автоматически к типу double ; в этом данная версия языка отличается от первоначальной. Вообще говоря, математические функции, аналогичные собранным в библиотеке , базируются на вычислениях с двойной точностью. В основном float используется для экономии памяти на больших массивах и не так часто — для ускорения счета на тех машинах, где арифметика с двойной точностью слишком дорога с точки зрения расхода времени и памяти. Правила преобразования усложняются с появлением операндов типа unsigned . Проблема в том, что сравнения знаковых и беззнаковых значений зависят от размеров целочисленных типов, которые на разных машинах могут отличаться. Предположим, что значение типа int занимает 16 битов, а значение типа long — 32 бита. Тогда -1L < 1U , поскольку 1U принадлежит типу unsigned int и повышается до типа signed long . Но -1L > 1UL , так как -1L повышается до типа unsigned long и воспринимается как большое положительное число. Преобразования имеют место и при присвоениях: значение правой части присвоения приводится к типу левой части, который и является типом результата. Тип char превращается в int путем распространения знака или другим описанным выше способом. Тип long int преобразуются в short int или в значения типа char путем отбрасывания старших разрядов. Так, в int i; char с; i = с; с = i; значение с не изменится. Это справедливо независимо от того, распространяется знак при переводе char в int или нет. Однако, если изменить очередность присваиваний, возможна потеря информации. Если х принадлежит типу float , a i — типу int , то и х = i , и i = x вызовут преобразования, причем перевод float в int сопровождается отбрасыванием дробной части. Если double переводится во float , то значение либо округляется, либо обрезается; это зависит от реализации. Так как аргумент в вызове функции есть выражение, при передаче его функции также возможно преобразование типа. При отсутствии прототипа функции аргументы типа char и short переводятся в int , a float — в double . Вот почему мы объявляли аргументы типа int или double даже тогда, когда в вызове функции использовали аргументы типа char или float И наконец, для любого выражения можно явно ("насильно") указать преобразование его типа, используя унарный оператор, называемый приведением. Конструкция вида (имя-типа) выражение приводит выражение к указанному в скобках типу по перечисленным выше правилам. Смысл операции приведения можно представить себе так: выражение как бы присваивается некоторой переменной указанного типа, и эта переменная используется вместо всей конструкции. Например, библиотечная функция sqrt рассчитана на аргумент типа double и выдает чепуху, если ей подсунуть что-нибудь другое ( sqrt описана в ). Поэтому, если 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; } Упражнение 2.З. Напишите функцию htol(s) , которая преобразует последовательность шестнадцатеричных цифр, начинающуюся с 0х или 0Х, в соответствующее целое. Шестнадцатеричными цифрами являются символы 0…9, а…f, А…F. 2.8. Операторы инкремента и декремента В Си есть два необычных оператора, предназначенных для увеличения и уменьшения переменных. Оператор инкремента ++ добавляет 1 к своему операнду, а оператор декремента -- вычитает 1. Мы уже неоднократно использовали ++ для наращивания значения переменных, как, например, в if (с == '\n') ++nl; Необычность операторов ++ и -- в том, что их можно использовать и как префиксные (помещая перед переменной: ++n ), и как постфиксные (помещая после переменной: n++ ) операторы. В обоих случаях значение n увеличивается на 1, но выражение ++n увеличивает n до того, как его значение будет использовано, а n++ — после того. Предположим, что n содержит 5, тогда x = n++; установит x в значение 5, а x = ++n; установит х в значение 6. И в том и другом случае n станет равным 6. Операторы инкремента и декремента можно применять только к переменным. Выражения вроде (i+j)++ недопустимы. Если требуется только увеличить или уменьшить значение переменной (но не получить ее значение), как например if (c == '\n') nl++; то безразлично, какой оператор выбрать — префиксный или постфиксный. Но существуют ситуации, когда требуется оператор вполне определенного типа. Например, рассмотрим функцию squeeze(s, с) , которая удаляет из строки s все символы, совпадающие с c : /* squeeze: удаляет все с из s */ void squeeze(char s[], int с) { int i, j; for (i = j = 0; s[i] != '\0'; i++) if (s[i] != c) s[j++] = s[i]; s[i] = '\0'; } Каждый раз, когда встречается символ, отличный от с , он копируется в текущую j -ю позицию, и только после этого переменная j увеличивается на 1, подготавливаясь таким образом к приему следующего символа. Это в точности совпадает со следующими действиями: if (s[i] != с) { s[j] = s[i]; j++; } Другой пример — функция getline , которая нам известна по главе 1. Приведенную там запись if (с == '\n' ) { s[i] = с; ++i; } можно переписать более компактно: if (с == '\n' ) s[i++] = с; В качестве третьего примера рассмотрим стандартную функцию 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 правильно отслеживали позиции перемещаемого символа. |