1.4. Обработка данных. Операции. Выражения

    Следующий слой выразительных средств языка – средства обработки данных. Это – набор элементарных действий (операций) и их сочетаний (выражений), который позволяет изменять значения переменных. К ним относятся:

    • традиционные операции над переменными (например, арифметические, логические, поразрядные);

    • присваивание;

    • ввод-вывод;

    • вызов процедур и функций.

    Перечисленные средства объединены по назначению в программе. Синтаксически они могут относиться к различным уровням. Например, в большинстве языков программирования присваивание является оператором, т.е. элементом более высокого синтаксического уровня, чем выражения. Аналогичным образом иногда обстоят дела с вызовом функций и вводом-выводом. В Си в этом смысле повезло. Всё перечисленное является операциями и объединено в один уровень – выражений. (Вызов функций в выражениях, а также библиотека функций ввода-вывода будут рассмотрены в 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=0xFFFFFF84
    
    unsigned char 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;
    
    unsigned short 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
    unsigned short 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) ;

    • явноe преобразованиe типа ( (тип) );

    • последовательность ( ","-запятая), условная ( ?: ).

    Арифметические операции

    Арифметические операции имеют в Си меньше всего специфики. Единственное, на что следует обращать внимание при их выполнении, - это размерность используемых целых переменных и переменных с плавающей точкой, неявные преобразования типов данных в выражениях и связанные со всем этим возможные потери значащих цифр (значимости) результата.

    Операция % вычисляет остаток от деления первого операнда на второй. Она имеет также другой, содержательный смысл: второй операнд-константа выступает ограничителем возможных изменений первого операнда и называется модулем. Название такой операции звучит как "... по модулю ...":

    C
    a = (a + 1) % 16; // a присвоить a+1 по модулю 16

    Операции сравнения и логические операции

    В Си отсутствует особый базовый тип данных для представления логических значений «истина» и «ложь». Для этой цели используются значения целой переменной. Значение 0 всегда является "ложью". Значение 1 -"истиной". Такие значения дают операции сравнения и логические операции. Вообще, в широком смысле любое ненулевое значение является истинным. В такой интерпретации проверяются условия в операторах программы. Поэтому можно записать:

    C
    if (1) { A } else { B } // Всегда выполнять B
    
    while (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)

    Трудно обнаруживаемые ошибки возникают при неявных преобразованиях типов в операциях, особенно при сочетании знаковой и беззнаковой форм представления:

    C
    char c[80];
    #define CODE 193
    if (c[i] == CODE) // Эквивалентно (int)c[i] == 193

    В данном примере идентификатором CODE обозначена целая константа, которая имеет смысл кода символа, на наличие которого затем проверяются элементы массива символов. Но дело в том, что такая операция будет давать значение «ложь» всегда. Тип char представляет символы как знаковые байты (целые минимальной длины), поэтому этому коду в данной форме представления соответствует отрицательное значение -63. Так как любая операция преобразует операнды char к int, то получится интересное сочетание "-63 == 193", имеющее значение «ложь» вместо планируемого «истина». В таких случаях, когда разрядности переменных меняются, лучше не смешивать знаковую и беззнаковую формы. В данном случае исправить ошибку можно несколькими способами

    C
    #define CODE -63 // Непонятно
    #define CODE (char)193 // Приемлемо
    #define CODE '\301'
    unsigned char 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 < c
        else {...} // a < b && a < c && b >=c
      else if (b < c)  {...} // a < b && a >=c && b < c
      else {...} // a < b && a >=c && b >=c
    else ...

    Можно воспользоваться тем, что операция сравнения дает целый результат (1 или 0) и сформировать переменную, принимающую уникальное значение для каждой комбинации сравнений. Тогда программа примет хотя и менее понятный, но зато более регулярный вид:

    C
    int n; 
    n = (a < b)*4 + (a < c)*2 + (b < c);
    switch(n) {
      case 0:... break; // a >=b && a >=c && b >=c
      case 7:... break; // a < b && a < c && b < c
    }

    Роль символа «точка с запятой»

    Символ ";" (точка с запятой), поставленный в конце выражения, превращает его в конструкцию более высокого уровня - оператор. Для обозначения его роли лучше всего подходит слово «ограничитель» - он ограничивает текущую синтаксическую конструкцию. То же самое он делает в других местах программы, например, в определениях переменных. Поэтому транслятор, обнаружив начало выражения или определения, продолжает его обработку, пока не встретит ";". Если программист забыл ограничить конструкцию этим символом, то транслятор «не заметит» окончания выражения и по инерции будет продолжать анализ последующей части программы как часть последнего. Это может привести к появлению ошибок трансляции, которых на самом деле нет в программе.

    C
    a = b + c - 5 // Здесь пропущен символ ";"
    if (a < b) {} // Здесь транслятор обнаружит ошибку
                  // в выражении, которое с его точки
                  // зрения еще не закончилось
    else {} // В этой части программы транслятор может
            // обнаружить «наведенную» ошибку
            // Эту часть программы транслятор пропустит

    Конечно, здесь много зависит от особенностей транслятора, но чтобы не проверять его на «сообразительность», лучше приучить себя вовремя ставить этот ограничитель.

    Примечание: в Паскале символ «;» называется разделителем - он разделяет два оператора в простой последовательности. Эта тонкость в терминологии приводит к тому, что программы на Паскале и Си с точки зрения расстановки этого символа существенно различаются.

    Вопросы без ответов

    Определить значения переменных после выполнения действий.

    C
    //------------------------------------------------- 1
    
    int i1 = 0xFFFFFFFF; i1 ++;
    
    //------------------------------------------------- 2
    
    unsigned u1,u2,u; u1 = 5; u2 = -1; u=0;
    
    if (u1 > u2) u++;
    
    //------------------------------------------------- 3
    
    int i1 = 0x01FF; char c; c = i1; i1 = c;
    
    //------------------------------------------------- 4
    
    int i1 = 0x01FF;
    
    unsigned char c; c = i1; i1 = c;
    
    //------------------------------------------------- 5
    
    double d1,d2,d3;
      d1 = 2.56; 
      d2 = (int)d1 + 1.5;
      d3 = (int)(d1 + 1.5);
    
    //------------------------------------------------- 6
    
    double 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;