Арифметические операции: сложение, вычитание, умножение и деление

Практический курс по C/C++: https://stepik.org/course/193691

После изучения базовых типов данных, переменных и функций ввода/вывода пришло время познакомиться с арифметическими операциями языка Си. Начнем с самых очевидных:

Операция

Обозначение

Сложение

+

Вычитание

-

Умножение

*

Деление

/

Все эти операции можно использовать как бинарные, то есть, слева и справа от них записываются выражения, над которыми выполняется соответствующая операция:

<левый операнд> + <правый операнд>
<левый операнд> - <правый операнд>
<левый операнд> * <правый операнд>
<левый операнд> / <правый операнд>

Здесь левые и правые операнды могут быть любыми выражениями, над которыми можно выполнять соответствующую арифметическую операцию. В самом простом случае – это переменные и числовые литералы.

Обратите внимание, все рассматриваемые арифметические операции являются именно операциями, а не операторами. Это значит, они вычисляют значение и возвращают его. Именно поэтому мы можем совершенно свободно присвоить вычисленный результат какой-либо переменной следующим образом:

int result = 10 + 5;

Или так:

int result;
result = 10 - 5;

Здесь точка с запятой в конце превращает арифметическую операцию в завершенный оператор. В частности, это означает, что арифметические операции можно продолжать, пока не встретится символ точка с запятой:

int result = 10 + 5 - 7 * 3;

Но обо всем по порядку. Давайте непосредственно в программе посмотрим на работу операций сложения, вычитания и умножения:

#include <stdio.h>
 
int main(void)
{
    short a = -5;
    int b = 10;
    float c = 5.4f;
    double d = -6.5;
 
    int res_1 = a + b;
    short res_2 = 100 - b;
    float res_3 = 5.4 - c;
    double res_4 = d * 4;
 
    return 0;
}

Вначале идет объявление четырех переменных разных типов с определенными начальными значениями. Затем, эти переменные используются в арифметических операциях наряду с числовыми литералами. Наверное, первое, что бросается в глаза, возможность использования смешанных типов данных при арифметических вычислениях. Да, язык Си нас в этом не ограничивает, в отличие от некоторых других языков высокого уровня, где смешение типов не допускается. Но возникает вопрос, как это в деталях работает? Начнем с первой операции сложения двух целочисленных переменных a и b. Они имеют разные типы: short и int. Так вот, компилятор языка Си целочисленные значения по умолчанию приводит к единому типу int и только потом выполняет их сложение. То есть, переменная a будет приведена к типу int и мы получаем сложение двух значений одного типа.

Я думаю, вы понимаете, почему переменная a приводится именно к типу int? Как мы с вами уже говорили, тип int может превышать по размеру тип short, то есть, он является более общим и наиболее употребительным для представления целых числовых значений. Когда меньший тип приводится к большему, такая операция называется повышением типа.

Конечно, в общем случае, типа int может быть недостаточно, например, когда используются очень большие числовые значения. В этом случае компилятор повышает тип до long или long long так, чтобы не происходило потери данных при их представлении.

В следующей строчке программы записана операция вычитания:

short res_2 = 100 - b;

Здесь все работает аналогичным образом. Литерал 100 по умолчанию представляется типом int, переменная b также имеет тип int и результат тоже сохраняется в памяти как число типа int. А далее, полученное значение типа int присваивается переменной res_2 типа short. Перед присваиванием также происходит приведение типов, в данном случае значение int к типу short, т.к. тип переменной res_2 компилятор поменять самовольно не может. В результате получаем операцию понижения типа, которая может привести к потере данных, если присваиваемое значение не укладывается в меньший по размеру тип short. Вот на это всегда следует обращать внимание, при реализации арифметических операций. Как только встречается понижение типа данных, потенциально возможна потеря данных.

В следующей строчке:

float res_3 = 5.4 - c;

мы тоже видим понижение типа от double к float. Как мы помним, числовой литерал вещественного числа представляется типом double. Переменная c также приводится к типу double и результат вычитания сохраняется на уровне этого типа данных. После этого, значение double присваивается переменной res_3 меньшего типа float. Снова имеем понижение типов и потенциальную возможность потери данных. Поэтому, чаще всего на практике вещественные числа описываются типом double, чтобы избежать подобных преобразований.

Наконец, последняя строчка:

double res_4 = d * 4;

Здесь вещественное число d типа double умножается на целочисленное значение 4. Строго говоря, компьютер не умеет выполнять арифметические операции с вещественными и целыми числами. В нем реализована арифметика либо над целыми, либо над вещественными числами, не смешивая их. Поэтому здесь число 4 сначала будет приведено к более общему типу double, и только потом выполнена операция умножения над вещественными числами.

И у нас остается еще одна операция деления. Я думаю, вы теперь легко поймете принцип ее работы. Запишем ее в нескольких вариациях:

#include <stdio.h>
 
int main(void)
{
    short a = -5;
    int b = 10;
    float c = 5.4f;
    double d = -6.5;
 
    int res_1 = 7 / 2;      /* 3 */
    double res_2 = -9 / 2;     /* -4 */
    float res_3 = a / c;    /* -0.9259... */
    double res_4 = d / b;   /* -0.65 */
 
    return 0;
}

Смотрите, когда происходит деление двух целочисленных значений, то результат также получается целочисленным. Причем, в соответствии со стандартом C99, дробная часть просто отбрасывается. Именно так образуются целые значения. То есть, здесь нет округления по правилам математики, а просто отбрасывание дробной части, какой бы она ни была. Это следует запомнить. Если же один из операндов является вещественным значением, то все числа приводятся к типу double и после этого выполняется операция деления. Поэтому переменные res_3 и res_4 принимают дробные значения.

Операция приведения типов

Вот такой нюанс есть у операции деления в языке Си и начинающие программисты здесь очень часто совершают ошибки, когда делят два целых числа, ожидая получить вещественное значение. В действительности же происходит обычное целочисленное деление. И здесь может возникнуть вопрос: а как нам разделить две целочисленные переменные, чтобы получилось вещественное число? То есть, ситуация выглядит следующим образом:

#include <stdio.h>
 
int main(void)
{
    short a = -5;
    int b = 10;
 
    double res_1 = a / b;      /* 0 */
 
    return 0;
}

Для этого необходимо одну, а лучше обе переменные привести к вещественному типу double, используя операцию приведения типов:

double res_1 = (double)a / (double)b;      /* -0.5 */

То есть, перед выражением (в данном случае переменной) в круглых скобках указывается тип, к которому должны быть приведены данные. В нашем примере значения переменных a и b сначала приводятся к типу double, а затем, выполняется операция деления над вещественными числами. Как результат получаем вещественное значение -0,5.

Если же мы оперируем конкретными числовыми значениями, например:

double res_2 = 7 / 2;

то для получения вещественного значения их удобнее записать в виде вещественных чисел:

double res_2 = 7.0 / 2.0;

вместо того, чтобы приводить целые числа к вещественному типу.

Унарные операции - и +

Далее, операции + и - можно использовать не только как бинарные, но и как унарные, то есть, перед выражением ставить знаки минус и плюс следующим образом:

#include <stdio.h>
 
int main(void)
{
    short a = -5;       // -5
    int b = -a;         // 5
    int d = -(7 + a);   // -(7 + -5) = -2
 
    return 0;
}

В первом случае знак минус стоит перед числовым литералом 5 и определяет его как отрицательное число, которое, затем, присваивается переменной a. В следующей строчке минус указан перед переменной и инвертирует знак числового значения, которое присваивается переменной b. При этом значение в переменной a не меняется. Наконец, в третьей строчке унарный минус поставлен перед выражением (7 + a). Соответственно, сначала вычисляется операция внутри скобок, а затем, выполняется инвертирование знака.

По аналогии можно использовать и унарный плюс, хотя, на практике он почти никогда не используется, т.к. по умолчанию значения определяются как положительные и лишний раз подчеркивать этот факт не имеет особого смысла.

Приоритеты арифметических операций

Важно знать, что приоритет любой унарной операции в языке Си выше, чем бинарной. Например, в следующей строчке:

int res = -10 + 7;

сначала будет выполнен унарный минус и только после этого бинарная операция сложения. Или так:

int res = -10 * 7;

Здесь также сначала унарный минус будет применен к числу 10 и только после этого выполнится бинарная операция умножения.

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

short a = -5, b = 7, c = 4;
double D = b * b - 4 * a * c;

Сначала вычисляются умножения, а затем, бинарная операция вычитания. Причем, обратите внимание, порядок вычисления первого умножения b * b и второго 4 * a * c стандартом языка Си не определен. Разработчики компиляторов могут делать это в произвольном порядке с целью повышения скорости работы программы. Поэтому нет гарантии, что сначала вычислится b * b и только потом 4 * a * c. Порядок может быть другим. Это очень важный момент. А вот порядок вычисления подряд идущих арифметических операций с одинаковым приоритетом всегда выполняется слева-направо. То есть в выражении 4 * a * c сначала будет выполнено первое умножение и только потом – второе. В этом мы можем быть уверены. Поэтому запись вида:

double res = 5 / 3 * 2;

означает, что сначала 5 делится на 3 и результат умножается на 2. Это эквивалентно выражению (5 / 3) * 2. Иногда путаются и считают, что оно соответствует выражению 5 / (3 * 2). Но это неверно из-за строго порядка вычислений слева-направо.

Если нужно изменить порядок вычислений, то есть, приоритеты, то для этого используются круглые скобки, например, следующим образом:

int perimetr = 2 * (b + c);

Сначала будет вычислено выражение внутри скобок и только потом умножение на два.

В заключение этого занятия приведу простой пример программы, которая запрашивает два числа и на их основе вычисляет площадь треугольника:

#include <stdio.h>
 
int main(void)
{
    double height, a;
    
    printf("Enter the height and length of the triangle's base: ");
    int res = scanf("%lf %lf", &height, &a);
 
    if(res != 2) {      // проверка, что res не равна двум
        printf("Data entry error\n");
        return 0;       // завершение функции main и программы
    }
 
    double sq = height * a / 2.0;
    printf("The square of the triangle is: %.2f", sq);
 
    return 0;
}

Вначале с помощью функции scanf() осуществляется ввод высоты и основания треугольника, затем, проверка, что данные были прочитаны корректно. Для этого, забегая вперед, я воспользовался условным оператором if, в котором проверяю, что переменная res не равна двум. Если это так, значит, произошла ошибка ввода значений. В этом случае выводится сообщение об ошибке в консоль и программа завершается. Если же данные введены верно, то вычисляется площадь треугольника и это значение выводится в консоль. Как видите, все достаточно просто.

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

Практический курс по C/C++: https://stepik.org/course/193691

Видео по теме