Арифметические операции деления по модулю, инкремента и декремента

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

Смотреть материал на YouTube | RuTube

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

Операция

Обозначение

Деление по модулю

%

Инкремент

++

Декремент

--

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

#include <stdio.h>
 
int main(void)
{
    int res = 10 % 3;
    printf("res = %d\n", res);
 
    return 0;
}

После запуска программы увидим значение 1 переменной res. Как это работает?

Смотрите, значение 3 трижды умещается в число 10 и остается еще единица. Эта единица и есть результат вычисления оператора деления по модулю. Аналогично, если взять два других целых числа, например:

int res = 10 % 4;

то увидим значение 2. И так для любых двух целых положительных чисел.

Ситуация кардинально не меняется и при использовании отрицательных целых чисел. Имеем здесь следующие комбинации и результаты вычислений в соответствии со стандартом C99:

int res_1 = -10 % 4;    // -2
int res_2 = 10 % -4;    // 2
int res_3 = -10 % -4;   // -2

Запомнить просто: если левый операнд отрицателен, то и результат отрицательный, иначе получаем положительное значение вне зависимости от знака правого операнда.

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

int res_4 = 10 - 10 / 4 * 4;    // 2

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

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

#include <stdio.h>
 
int main(void)
{
    int digit;
    scanf("%d", &digit);
 
    int res = digit % 2;
    printf("res = %d\n", res);
 
    return 0;
}

Сначала в переменную digit вводится некоторое целое значение. После этого вычисляется остаток от деления на 2 и результат сохраняется в переменной res. Очевидно, если переменная res будет равна 1 или -1, то число в digit нечетное, а если res равна 0, то четное.

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

#include <stdio.h>
 
int main(void)
{
    int digit;
    scanf("%d", &digit);
 
    int range = 10;
    int length = digit % range;     // [0; range-1]
    printf("length = %d\n", length);
 
    return 0;
}

В итоге переменная length будет принимать значения, не выходящие за диапазон [0; range-1].

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

часы : минуты : секунды

Сделать это можно следующим образом:

#include <stdio.h>
 
int main(void)
{
    unsigned int time = 4*3600 + 32*60 + 18;
 
    unsigned int sec = time % 60;
    unsigned int min = (time / 60) % 60;
    unsigned int hour = time / 3600;
 
    printf("%02d:%02d:%02d\n", hour, min, sec);
 
    return 0;
}

Как видите, арифметическая операция деления по модулю бывает весьма полезной при разработке программ на языке Си.

Операции инкремента и декремента

Следующая группа арифметический операций – это инкремент и декремент. Работают они схожим образом, только операция инкремента увеличивает значение на единицу, а декремента – уменьшает на единицу. Начнем с инкремента.

Формально, инкремент определяется оператором ++, который можно прописывать у любого изменяемого выражения (как говорят, l-value). Часто это обычные переменные. Например, следующим образом:

#include <stdio.h>
 
int main(void)
{
    int count = 0;
 
    printf("count = %d\n", count);
    count++;
    printf("count = %d\n", count);
 
    return 0;
}

Вначале значение переменной count равно нулю, а затем, с помощью операции инкремент, ее значение увеличивается на единицу.

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

count = count + 1;

Суть от этого не поменялась бы. Но между этими двумя записями есть существенные отличия. Ранние компиляторы языка Си (без оптимизаторов) переводили операцию инкремента в виде одной машинной команды, а вторую аналогичную конструкцию – в набор из нескольких команд. В результате, программы с использованием записи вида «count = count + 1» работали несколько дольше. Хотя в наше время компиляторы достаточно «сообразительные» чтобы такие команды эффективно транслировать в машинный код. Но, теме не менее, вариант инкремента более предпочтителен и, как мы далее увидим, обладает дополнительными возможностями.

Второй момент отличия этих конструкций заключается в том, что при использовании оператора инкремента переменная count фигурирует в операторе только один раз, тогда как в команде «count = count + 1» дважды. Сейчас вам это может показаться несущественным, но в действительности, если вместо переменной используется некоторое вычисляемое выражение и оно может меняться при каждой записи, то вариант «count = count + 1» к нему будет просто неприменим.

Итак, на данный момент вы должны просто запомнить, что для увеличения или уменьшения значения на единицу лучше использовать операции инкремента и декремента соответственно.

Далее, эти операции можно записывать в двух формах: префиксной и постфиксной:

count++;    // постфиксная форма
++count;    // префиксная форма

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

#include <stdio.h>
 
int main(void)
{
    int count = 0, size = 5;
 
    int current = count++;
    int width = ++size;
 
    printf("count = %d, size = %d, current = %d, width = %d\n", 
                                    count, size, current, width);
 
    return 0;
}

Вначале определены две переменные count и size с начальными значениями 0 и 5. Затем, объявляется переменная current, которой присваивается значение переменной count, записанной с операцией инкремента в постфиксной форме. Следом объявляется еще одна переменная width, которой присваивается значение переменной size с операцией инкремента в префиксной форме. После запуска программы на экране увидим следующую строку:

count = 1, size = 6, current = 0, width = 6

Во-первых, обе переменные count и size были увеличены на единицу, что и должно было произойти. А вот дальше видим отличия: переменная current принимает значение 0, а переменная width – значение 6. Почему так произошло? Очевидно, что операций инкремента, записанная в постфиксной форме, срабатывает после использования переменной count. То есть, сначала была выполнена операция присваивания нулевого значения переменной current и только после этого в переменной count значение было увеличено на единицу. Именно так работает инкремент в постфиксной форме записи. Во втором случае инкремент записан перед переменной и срабатывает до ее использования в арифметических и других операциях. Поэтому, сначала было увеличено значение переменной size на единицу и только после этого число 6 было присвоено переменной width. Вот так работает инкремент в префиксной и постфиксной формах записи. По аналогии отрабатывает и операция декремента.

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

int p = 2 * size++;
int r = 3 * --width;

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

int p = width * size++;

то здесь также сначала будет выполнено умножение и только потом увеличение переменной size на единицу. А вот если заключить переменные в круглые скобки:

int p = (width * size)++;

то при компиляции программы появится ошибка недопустимого леводопустимого (l-value) выражения. Дело в том, что операции инкремента и декремента могут быть применены лишь к изменяемой области памяти, например, переменным. Тогда как произведение (width * size) следует воспринимать как промежуточное константное значение, которое нельзя изменить с помощью операции инкремента. Это все равно, что записать инкремент у числового литерала:

int p = 100++;

также увидим ошибку при компиляции.

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

int width = 5;
int size = width * width + 2 * (10 + width--);

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

int res = ++width * width + 2 * (10 + width--);

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

int size = width * width + 2 * (10 + width);
width--;

Заключение

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

Операция

Обозначение

Приоритет

Сложение

+

1

Вычитание

-

1

Умножение

*

2

Деление

/

2

Деление по модулю

%

2

Инкремент

++

3

Декремент

--

3

Здесь условно приоритет обозначен числами: чем выше число – тем выше приоритет.

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

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

Видео по теме