Следующий слой выразительных средств языка – средства обработки данных. Это – набор элементарных действий (операций) и их сочетаний (выражений), который позволяет изменять значения переменных. К ним относятся:
традиционные операции над переменными (например, арифметические, логические, поразрядные);
присваивание;
ввод-вывод;
вызов процедур и функций.
Перечисленные средства объединены по назначению в программе. Синтаксически они могут относиться к различным уровням. Например, в большинстве языков программирования присваивание является оператором, т.е. элементом более высокого синтаксического уровня, чем выражения. Аналогичным образом иногда обстоят дела с вызовом функций и вводом-выводом. В Си в этом смысле повезло. Всё перечисленное является операциями и объединено в один уровень – выражений. (Вызов функций в выражениях, а также библиотека функций ввода-вывода будут рассмотрены в 1.6).
Операции, выражения #
Для начала – немного терминологии:
операция – элементарное действие по обработке данных (по работе с переменными);
операнд - переменная, константа, выражение, участвующие в операции;
унарная операция - операция с одним операндом;
бинарная операция - операция с двумя операндами;
выражение – элемент синтаксиса с описанием последовательности выполнения операций и их операндов, в котором результат одной операции является операндом другой.
Если посмотреть полный перечень операций в Си, то первое, что бросается в глаза, это их многочисленность. Второе (уже упомянутое), что к операциям относятся такие действия, которые в других языках программирования считаются операторами (например, присваивание). И третье, что все они имеют очень «компактный» синтаксис, т.е. при пропуске или добавлении лишнего символа одна операция превращается в другую. Все это требует внимательного и осознанного отношения к операциям в Си. Перечислим их особенности:
операции разбиты на 16 групп по приоритетам их выполнения;
внутри каждой группы задается направление выполнения операции для последовательности операций одного приоритета. Для большинства из них имеет место естественное направление слева направо. Однако для операций присваивания, условной операции и унарных операций, у которых знак (символ) операции находится перед операндом (слева), направление операций противоположное - справа налево;
большинство операций совместимо по результатам. Это значит, что результат любой операции может быть операндом любой другой операции, то есть их можно комбинировать между собой как угодно, даже в самых «диких» сочетаниях;
в Си отсутствует понятие логического (булевого) типа «истина-ложь». Для его представления используются значения целой переменной: 0 –«ложь», 1 (или любое отличное от нуля значение) – «истина»;
в некоторых операциях возможно изменение значений участвующих в ней операндов;
Действие и результат #
«Борьбу с пьянством прекратить, ибо это – не борьба, и это - не результат».
Из миниатюр М.Жванецкого
Обычно результат операции представляет собой новое, самостоятельное значение, которое затем может использоваться в качестве операнда в последующих операциях. Входные операнды при этом остаются без изменения. В Си схема может быть сложнее - операнды некоторых операций могут изменяться в процессе ее выполнения. Самый простой пример - операция присваивания:
C
a = b;// Действие над операндом: переменная a получает значение переменной b// Результат: значение переменной a после присваивания
Наличие в операции результата позволяет использовать ее в контексте (окружении) других операций, например:
C
c =(a = b)+5;// Эквивалентно a = b; c = a + 5;
Более интересный случай представляют собой операции инкремента и декремента, в которых действие не совпадает с результатом, например:
C
a++;// Действие над операндом: переменная a увеличивается нa 1// Результат: значение переменной до ее увеличения
c = A[i++];// Эквивалентно c = A[i]; i = i + 1;
Преобразование типов операндов #
В выражениях в качестве операндов могут присутствовать переменные и константы разных типов (здесь и далее мы ограничимся пока только известными нам базовыми типами данных). Результат каждой операции также имеет свой определенный тип, который зависит от типов операндов. Если в бинарных операциях типы данных обоих операндов совпадают, то результат будет иметь тот же самый тип. Если нет, то транслятор должен включить в код программы неявные операции, которые преобразуют тип операндов, то есть выполнить приведение типов. Но прежде, чем изучать подробности таких преобразований, ответим на вопрос: «Где они могут происходить»?:
при выполнении операции присваивания, когда значение переменной или выражения из правой части запоминается в переменной в левой части;
при прямом указании на необходимость изменения типа данных переменной или выражения, для чего используется операция явного преобразования типа;
при выполнении бинарных операций над операндами различных типов, когда более «длинный» операнд превалирует над более «коротким», вещественное - над целым, а беззнаковое над знаковым.
Преобразование типов может неявно включать в себя следующие действия:
преобразование целой переменной в переменную вещественную (с плавающей точкой) и наоборот;
увеличение или уменьшение разрядности машинного слова, то есть «усечение» или «растягивание» целой переменной;
преобразование знаковой формы представления целого в беззнаковую и наоборот.
Возможные ошибки, возникающие при таких преобразованиях, никоим образом не фиксируются ни аппаратными средствами, ни исполнительной системой языка, т.е. остаются незамеченными. Поэтому здесь нужно быть особенно внимательным и при необходимости учитывать особенности представления данных во внутренней форме (см. 1.3).
Уменьшение разрядности машинного слова всегда происходит путем отсечения старших разрядов числа. Заметим, что это может привести к ошибкам потери значащих цифр и разрядов:
C
int n =1045;// Во внутреннем представлении n=0x00000415 (1024+16+5)char c; c = n;// Потеря значащих цифр (0x15)
Увеличение разрядности приводит к появлению дополнительных старших разрядов числа. При этом способ их заполнения зависит от формы представления целого:
для беззнаковых целых заполнение производится нулями;
для целых со знаком они заполняются значением знакового (старшего) разряда.
Таким образом, при увеличении размерности целого его значение сохраняется (с учетом формы представления):
C
int n;unsigned u;char c =0x84; n = c;// Значение n=0xFFFFFF84unsignedchar uc =0x84; u = uc;// Значение u=0x00000084
При преобразовании вещественного к целому происходит потеря дробной части, при этом возможны случаи возникновения ошибок переполнения и потери значащих цифр, когда полученное целое имеет слишком большое значение:
C
double d1 =855.666, d2 =0.5E16;int n; n = d1;// Отбрасывание дробной части
n = d2;// Потеря значимости
Преобразование знаковой формы к беззнаковой не сопровождается изменением значения целого числа и вообще не приводит к выполнению каких-либо действий в программе. В таких случаях транслятор «запоминает», что форма представления целого изменилась:
C
int n =-1;unsigned d;
d = n;// Значение d=0xFFFFFFFF (-1)
При выполнении бинарных операций принят следующий порядок:
короткие типы char, short и битовые поля удлиняются до int, float до double;
если один из операндов – длинный вещественный (long double), то второй также приводится к этому типу;
если один из операндов – длинное целое (long), то второй также приводится к этому типу;
если один из операндов – беззнаковый (unsigned), то второй также приводится к этому типу;
в конце концов остаются целые операнды, сохраняющие свой тип.
Таким образом, короткие типы данных (знаковые и беззнаковые) удлиняются до int и double, а выполнение любой бинарной операции с одним long double, double, long, unsigned ведет к преобразованию другого операнда к тому же типу. Это может сопровождаться перечисленными выше действиями: увеличение разрядности операнда путем его «удлинения», преобразование в форму с плавающей точкой и изменение беззнаковой формы представления на знаковую и наоборот.
Следует обратить внимание на одну тонкость: если в процессе преобразования требуется увеличение разрядности переменной, то на способ ее «удлинения» влияет только наличие или отсутствие знака у самой переменной. Второй операнд, к типу которого осуществляется приведение, на этот процесс не влияет (принятые в примерах размерности short – 16 разрядов, int – 32 разряда):
C
int l =0x0031;unsignedshort d =0xFF000;
l + d ...// 0x00000031 + 0xFF00 = 0x00000031 + 0x0000FF00 = 0x0000FF31
В данном случае производится преобразование укороченного целого без знака (unsigned) к целому со знаком (int). В процессе преобразования «удлинение» переменной d производится как беззнаковое (разряды заполняются нулями), хотя второй операнд и имеет знак. Рассмотрим еще несколько примеров.
C
int i; i =0xFFFFFFFF;
Целая переменная со знаком получает значение FFFFFFFF, что соответствует -1 для знаковой формы в дополнительном коде. Изменение формы представления с беззнаковой на знаковую не сопровождается никакими действиями.
C
short i =0xFFFF;int l; l = i;
Преобразование short в int сопровождается «удлинением» переменной, что с учетом представления i со знаком дает FFFFFFFF, то есть целое со значением -1.
C
unsignedshort n =0xFF00;int l; l = n;
Переменная n «удлиняется» как целое без знака, то есть переменная l получит значение 0000FF00.
C
int i;unsigned u;
i = u =0xFFFFFFFF;if(i >5)...// «Ложь»if(u >5)...// «Истина»
Значения переменных без знака и со знаком равны FFFFFFFF или -1. Но результаты сравнения противоположны, так как во втором случае сравнение проводится для беззнаковых целых по их абсолютной величине, а в первом случае - путем проверки знака результата вычитания, то есть с учетом знаковой формы представления чисел.
И последнее, очень важное свойство: преобразование выполняется для каждой пары операндов независимо, т.е. абсолютно не важно, какие типы операндов имеют место в последующих операциях. «Забывчивость» в этом вопросе приводит к ошибкам:
C
int a =5, b =2;double dd; dd = a / b;// результат dd=2.0, деление int/int
Здесь производится целочисленное деление, несмотря на запоминание результата в вещественной форме.
Классификация операций #
Классификацию операций будем проводить не по приоритетам (как это делается в справочниках), а по их назначению. Именно такими группами они будут встречаться нам в различных областях программирования:
арифметические ( +,-,*,/,% ) (см. 4.2);
логические ( &&, ||, ! );
сравнения ( <,>,>=,<=,==,!=);
поразрядные (машинно-ориентированные) ( &,|,^,~,<<,>> ) (см. 9.1);
присваиваниe (=,++,--,+=,-=,*-,/= и т.п.);
работa с указателями и памятью (*,&,sizeof) (см. 5.2, 9.2);
выделение составляющего типа данных ( (),*,[], . , -> ) (см. 5.4) ;
Арифметические операции имеют в Си меньше всего специфики. Единственное, на что следует обращать внимание при их выполнении, - это размерность используемых целых переменных и переменных с плавающей точкой, неявные преобразования типов данных в выражениях и связанные со всем этим возможные потери значащих цифр (значимости) результата.
Операция % вычисляет остаток от деления первого операнда на второй. Она имеет также другой, содержательный смысл: второй операнд-константа выступает ограничителем возможных изменений первого операнда и называется модулем. Название такой операции звучит как "... по модулю ...":
C
a =(a +1)%16;// a присвоить a+1 по модулю 16
Операции сравнения и логические операции #
В Си отсутствует особый базовый тип данных для представления логических значений «истина» и «ложь». Для этой цели используются значения целой переменной. Значение 0 всегда является "ложью". Значение 1 -"истиной". Такие значения дают операции сравнения и логические операции. Вообще, в широком смысле любое ненулевое значение является истинным. В такой интерпретации проверяются условия в операторах программы. Поэтому можно записать:
C
if(1){ A }else{ B }// Всегда выполнять Bwhile(1){...}// «Вечный» циклif(k){ A }else{ B }// Эквивалентно if(k !=0)
Все операции сравнения дают в качестве результата значения 1 или 0. Следовательно, их можно использовать совместно с арифметическими и другими операциями:
C
a = b > c;// Запомнить результат сравнения
a =(b > c)\*2;// Принимает значения 0 или 2
Логические операции И (&&) , ИЛИ (||) и НЕ (!) едины для всех языков программирования и соответствуют логическим функциям И, ИЛИ и НЕ для логических (булевых) переменных. Операция И имеет результатом значение «истина» тогда и только тогда, когда оба ее операнда истинны, то есть по отношению к операндам - утверждениям звучит как «одновременно оба». Операция ИЛИ имеет результатом значение «истина», когда хотя бы один из операндов истинен, то есть характеризуется фразой «хотя бы один»:
C
if(a < b && b < c)// если ОДНОВРЕМЕННО ОБА a < b и b < c, то...if(a ==0|| b >0)// если ХОТЯ БЫ ОДИН a==0 или b > 0, то...
Логические операции И и ИЛИ имеют в Си еще одно свойство. Если в операции И первый операнд имеет значение «ложь», а в операции ИЛИ – «истина», то вычисление выражения прекращается, потому что значение его уже становится известным («ложь» -для И, «истина» -для ИЛИ). Поэтому возможны выражения, где в первом операнде операции И проверяется корректность некоторой переменной, а во втором - она же используется с учетом этой корректности:
C
if(a >=0&&sin(sqrt(a))>0)...
В данном примере второй операнд, включающий в себя функцию вычисления квадратного корня, не вычисляется, если первый операнд – «ложь».
Особо следует отметить операцию логической инверсии (отрицания) -"!". Значение «истина» она превращает в «ложь» и наоборот. Если считать значением «истина» любое ненулевое значение целой переменной, то эту операцию для целых следует понимать как проверку на 0:
C
while(!k){...}// эквивалентно while(k==0) {...}
Операции присваивания #
К операциям присваивания относятся все операции, которые меняют значение одного из операндов. В Си их целых три группы:
обычное присваивание (=);
присваивание, соединенное с одной их бинарных операций (+=, -=, *=, /=, %=, <<=, >>=, &=, |=, ^=);
операции инкремента и декремента (увеличения и уменьшения на 1).
Операция присваивания "=" сохраняет значение выражения, стоящего в левой части, в переменной, а точнее, в адресном выражении, стоящем а правой части. Термин адресное выражение (или l-value) используется для обозначения тех выражений, которым соответствуют исходные объекты (переменные) в памяти программы. На данном уровне знакомства со структурами данных – это простые переменные и элементы массивов.
Различная интерпретация левой и правой части соответствует дуализму понятия «имя переменной»: в левой части под ним понимается ссылка (адрес этой переменной в памяти), в правой части – ее значение.
Разница между ссылкой и значением такая же, как между стаканом и его содержимым.
При выполнении операции присваивания тип выражения в правой части преобразуется к типу адресного выражения в левой. Результатом операции является значение левой части после присваивания, соответственно, тип результата - это тип левой части. Кроме того, присваивание - одна из немногих операций с направлением выполнения «справа налево». Из сказанного следует возможность многократного присваивания «справа налево», в котором результат каждого из них используется как правая часть последующего:
C
long a;char b;int c;
a = b = c;// эквивалентно b = c; a = b;
В данном случае при первом (правом) присваивании тип int преобразуется к char, а результатом операции является значение переменной b типа char после выполнения этого присваивания. Затем аналогичным образом происходит присваивание b в a.
Операция присваивания, соединенная с одной из бинарных операций, - это частный случай, когда результат бинарной операции сохраняется (присваивается) в первом операнде:
C
a +=b;// эквивалентно a = a + b;
Приведенный выше эквивалент этой операции верен лишь в первом приближении, потому что в этих операциях левый операнд, если он является адресным выражением, вычисляется один, а не два раза. Следующий пример показывает это:
C
A[i++]+=b;// эквивалентно A[i] = A[i] + b; i++;
Операции инкремента и декремента увеличивают или уменьшают значение единственного операнда до или после использования его значения в выражении:
C
int a;// Эквивалент Интерпретация
a++;// Rez=a; a=a+1; Увеличить на 1 после использования++a;// a=a+1; Rez=a; Увеличить на 1 до использования
a--;// Rez=a; a=a-1; Уменьшить на 1 после использования--a;// a=a-1; Rez=a; Уменьшить на 1 до использования
Явное преобразование типа #
В тех случаях, когда программиста не устраивает принятый порядок неявного преобразования типов, он может сам преобразовать результат к такому типу, какой ему необходим. Это можно сделать, в частности, путем присваивания результата дополнительной переменной, во время которого требуемое преобразование будет произведено. Но он может сделать то же самое внутри выражения «на лету» с помощью специальной операции. Она представляет собой имя типа, к которому осуществляется приведение, заключенное в круглые скобки и стоящее перед операндом. В качестве примера рассмотрим получение дробной части числа:
C
double x,d;// double x,d; int n;
d = x -(int)x;// n = x; d = x - d;
Операции выделения составляющего типа данных #
Особенность этих операций, которая служит основанием для их объединения, состоит в том, что операций не изменяют данные, а осуществляют переход от одной формы представления к другой - от производного типа к его составляющей и наоборот (см. 5.5). Эти операции касаются, прежде всего, работы с различными типами данных и их конструирования в программе.
Условная операция и операция «запятая» #
Эти операции являются частью алгоритмического уровня языка, они позволяют «встроить» внутрь выражения небольшую условную конструкцию или последовательность независимых действий.
C
int a;double b;
c = x + a > b ? a : b;// Условие ? Выражение для «истина» : Выражение для «ложь»
Операция использует три операнда и два знака операции (?:). Первым операндом является условие. Если оно истинно, то результатом становится значение второго операнда, если ложно - то третьего. В данном примере вычисляется максимальное значение переменных a,b. Тип результата операции определяется по правилам неявного преобразования типов для второго и третьего операндов. Он будет всегда один и тот же, независимо от выполнения условия. В данном случае - всегда double, так как переменная a будет приведена к этому типу.
Операция "," позволяет соединить в одно выражение несколько выражений, не связанных между собой результатами, то есть просто перечислить их. Необходимость ее возникает, когда программисту нужно «втиснуть» в то место программы, где по синтаксису стоит одно выражение, несколько независимых друг от друга. Типичный пример - работа в цикле с использованием двух индексов массива, перемещающихся от концов к середине:
C
for(i=0; i<n; i++){…A[i]…}// Обычный циклfor(i=0, j=n-1; i<j; i++, j--)// Цикл с двумя индексами{...A[i]...A[j]...}
Результатом операции является значение последнего выражения. Поэтому если в группе выражений, соединенных запятыми, есть условие, которое надлежит проверить, то оно должно быть последним. При этом тип результата также равен типу последнего выражения. Например, в заголовке цикла в начале каждого шага проверяется условие, перед которым выполняется присваивание:
C
while( a=b, a <0){....}
Подводные камни и маленькие хитрости #
Компактный синтаксис операций, а также их совместимость по результатам служат источником значительного количества ошибок такого плана: при пропуске или, наоборот, дублировании знака операции может получиться другая операция, которая при принятой в Си «свободе нравов» будет синтаксически корректна, выполнима, но даст совершенно незапланированный результат. «Сообразительный» транслятор может сопроводить такую ошибку предупреждением (warning). Приведем примеры таких ошибок:
C
if(a=b)// вместо if (a==b)while(a <<3)// вместо while (a < 3)if(a &&0x10)// вместо if (a & 0x10)
Трудно обнаруживаемые ошибки возникают при неявных преобразованиях типов в операциях, особенно при сочетании знаковой и беззнаковой форм представления:
В данном примере идентификатором CODE обозначена целая константа, которая имеет смысл кода символа, на наличие которого затем проверяются элементы массива символов. Но дело в том, что такая операция будет давать значение «ложь» всегда. Тип char представляет символы как знаковые байты (целые минимальной длины), поэтому этому коду в данной форме представления соответствует отрицательное значение -63. Так как любая операция преобразует операнды char к int, то получится интересное сочетание "-63 == 193", имеющее значение «ложь» вместо планируемого «истина». В таких случаях, когда разрядности переменных меняются, лучше не смешивать знаковую и беззнаковую формы. В данном случае исправить ошибку можно несколькими способами
C
#defineCODE-63// Непонятно#defineCODE(char)193// Приемлемо#defineCODE'\301'unsignedchar c[80];// Лучше всего для символов с кодами >128
При выполнении операций с переменными различной разрядности нужно помнить, что последовательность преобразования разрядностей (типов) связана с последовательностью и приоритетами выполнения операций. Поэтому само по себе наличие в выражении операнда большей разрядности еще не гарантирует правильности вычислений для больших значений. Это видно на примере, где используется переменная типа long для хранения произведения переменных типа int. Ошибка состоит в том, что операция умножения все равно будет производиться с целыми размерности int, что может привести к потере значащих цифр произведения:
C
Int a,b;long c;
c = a \* b;// Неправильно
c =(long)a \* b;// Правильно
Операция присваивания, операция «запятая» и условная операция позволяют выполнять многие действия «на лету», не выходя за пределы синтaксиса выражения в условиях, проверяемых в оперaторах if, while, например:
C
while((c =getchar())!='\*'){...c...}
Здесь в переменной c запоминается результат функции, вызванной во время проверки условия в операторе while, с целью дальнейшего его использования в теле оператора.
C
while(x0 = x1, x0 >0){... x1 =f(x0)...}
Присваивание выполняется во время проверки условия в операторе цикла.
C
for(...; d >0? a > b : b >= a;...){...}
В зависимости от значения переменной d меняется условие продолжения цикла for.
При наличии в программе нескольких вариантов выбора по группе условий программа становится «сильно ветвистой», например:
C
if(a < b)if(a < c)if(b < c){...}// a < b && a < c && b < celse{...}// a < b && a < c && b >=celseif(b < c){...}// a < b && a >=c && b < celse{...}// a < b && a >=c && b >=celse...
Можно воспользоваться тем, что операция сравнения дает целый результат (1 или 0) и сформировать переменную, принимающую уникальное значение для каждой комбинации сравнений. Тогда программа примет хотя и менее понятный, но зато более регулярный вид:
C
int n;
n =(a < b)*4+(a < c)*2+(b < c);switch(n){case0:...break;// a >=b && a >=c && b >=ccase7:...break;// a < b && a < c && b < c}
Роль символа «точка с запятой» #
Символ ";" (точка с запятой), поставленный в конце выражения, превращает его в конструкцию более высокого уровня - оператор. Для обозначения его роли лучше всего подходит слово «ограничитель» - он ограничивает текущую синтаксическую конструкцию. То же самое он делает в других местах программы, например, в определениях переменных. Поэтому транслятор, обнаружив начало выражения или определения, продолжает его обработку, пока не встретит ";". Если программист забыл ограничить конструкцию этим символом, то транслятор «не заметит» окончания выражения и по инерции будет продолжать анализ последующей части программы как часть последнего. Это может привести к появлению ошибок трансляции, которых на самом деле нет в программе.
C
a = b + c -5// Здесь пропущен символ ";"if(a < b){}// Здесь транслятор обнаружит ошибку// в выражении, которое с его точки// зрения еще не закончилосьelse{}// В этой части программы транслятор может// обнаружить «наведенную» ошибку// Эту часть программы транслятор пропустит
Конечно, здесь много зависит от особенностей транслятора, но чтобы не проверять его на «сообразительность», лучше приучить себя вовремя ставить этот ограничитель.
Примечание: в Паскале символ «;» называется разделителем - он разделяет два оператора в простой последовательности. Эта тонкость в терминологии приводит к тому, что программы на Паскале и Си с точки зрения расстановки этого символа существенно различаются.
Вопросы без ответов #
Определить значения переменных после выполнения действий.
C
//------------------------------------------------- 1int i1 =0xFFFFFFFF; i1 ++;//------------------------------------------------- 2unsigned u1,u2,u; u1 =5; u2 =-1; u=0;if(u1 > u2) u++;//------------------------------------------------- 3int i1 =0x01FF;char c; c = i1; i1 = c;//------------------------------------------------- 4int i1 =0x01FF;unsignedchar c; c = i1; i1 = c;//------------------------------------------------- 5double d1,d2,d3;
d1 =2.56;
d2 =(int)d1 +1.5;
d3 =(int)(d1 +1.5);//------------------------------------------------- 6double d1 =2.56;int i;
i =(d1 -(int)d1) \*10;//------------------------------------------------- 7
i=0;if(i++) i++;//------------------------------------------------- 8
i=0;if(++i) i++;//------------------------------------------------- 9
m = a > b ? a : b;//------------------------------------------------ 10
m =(a \* b)>0;//------------------------------------------------ 11
m = a >0? a :-a;