Главная страница
Навигация по странице:

  • 2.5. Арифметические операторы

  • 2.6. Операторы отношения и логические операторы

  • Упражнение 2.2.

  • Упражнение 2.З.

  • 2.8. Операторы инкремента и декремента

  • Язык программирования Си Брайан Керниган, Деннис Ритчи 3е издание Версия 1 Table of Contents


    Скачать 2.33 Mb.
    НазваниеЯзык программирования Си Брайан Керниган, Деннис Ритчи 3е издание Версия 1 Table of Contents
    Дата18.09.2022
    Размер2.33 Mb.
    Формат файлаpdf
    Имя файлаBrian_Kernighan_Dennis_Ritchie-The_C_Programming_Language-RU.pdf
    ТипДокументы
    #683263
    страница7 из 31
    1   2   3   4   5   6   7   8   9   10   ...   31
    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
    правильно отслеживали позиции перемещаемого символа.
    1   2   3   4   5   6   7   8   9   10   ...   31


    написать администратору сайта