Практический курс по C/C++: https://stepik.org/course/193691
После изучения базовых типов данных, переменных и функций ввода/вывода
пришло время познакомиться с арифметическими операциями языка Си. Начнем с
самых очевидных:
Операция
|
Обозначение
|
Сложение
|
+
|
Вычитание
|
-
|
Умножение
|
*
|
Деление
|
/
|
Все эти операции
можно использовать как бинарные, то есть, слева и справа от них
записываются выражения, над которыми выполняется соответствующая операция:
<левый операнд> + <правый операнд>
<левый операнд> - <правый операнд>
<левый операнд> * <правый операнд>
<левый
операнд> / <правый операнд>
Здесь левые и
правые операнды могут быть любыми выражениями, над которыми можно выполнять
соответствующую арифметическую операцию. В самом простом случае – это переменные
и числовые литералы.
Обратите внимание,
все рассматриваемые арифметические операции являются именно операциями, а не
операторами. Это значит, они вычисляют значение и возвращают его. Именно
поэтому мы можем совершенно свободно присвоить вычисленный результат какой-либо
переменной следующим образом:
Или так:
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 так, чтобы не
происходило потери данных при их представлении.
В следующей
строчке программы записана операция вычитания:
Здесь все
работает аналогичным образом. Литерал 100 по умолчанию представляется типом int, переменная b также имеет тип int и результат
тоже сохраняется в памяти как число типа int. А далее,
полученное значение типа int присваивается
переменной res_2 типа short. Перед
присваиванием также происходит приведение типов, в данном случае значение int к типу short, т.к. тип
переменной res_2 компилятор
поменять самовольно не может. В результате получаем операцию понижения типа,
которая может привести к потере данных, если присваиваемое значение не
укладывается в меньший по размеру тип short. Вот на это
всегда следует обращать внимание, при реализации арифметических операций. Как
только встречается понижение типа данных, потенциально возможна потеря данных.
В следующей
строчке:
мы тоже видим
понижение типа от double к float. Как мы помним,
числовой литерал вещественного числа представляется типом double. Переменная c также
приводится к типу double и результат вычитания сохраняется на
уровне этого типа данных. После этого, значение double присваивается
переменной res_3 меньшего типа
float. Снова имеем
понижение типов и потенциальную возможность потери данных. Поэтому, чаще всего
на практике вещественные числа описываются типом double, чтобы избежать
подобных преобразований.
Наконец,
последняя строчка:
Здесь
вещественное число 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.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).
Соответственно, сначала вычисляется операция внутри скобок, а затем,
выполняется инвертирование знака.
По аналогии
можно использовать и унарный плюс, хотя, на практике он почти никогда не
используется, т.к. по умолчанию значения определяются как положительные и
лишний раз подчеркивать этот факт не имеет особого смысла.
Приоритеты арифметических операций
Важно знать, что
приоритет любой унарной операции в языке Си выше, чем бинарной. Например, в следующей
строчке:
сначала будет
выполнен унарный минус и только после этого бинарная операция сложения. Или
так:
Здесь также
сначала унарный минус будет применен к числу 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 сначала будет
выполнено первое умножение и только потом – второе. В этом мы можем быть
уверены. Поэтому запись вида:
означает, что
сначала 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