Операции и операторы

Вспомним, что мы говорили о типах данных. Тип данных в первую очередь определяет то, какие операции мы можем совершать над данными.

А операции над данными - это одна из самых базовых (в плане простых и в плане необходимых) вещей в программировании в целом.

Давайте посмотрим на то, какие операции в принципе есть в С++.

Операции и операторы

Операция - это конкретное действие, которые производится над данными. Сложение, умножение, и так далее.

Операторы - это же специальные символы, с помощью которых мы создаём операции.

Например, оператор + задаёт для целых чисел операцию сложения.

Обратите внимание: каждая операция в C++ определена для конкретных типов, а не для всех сразу.

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

У каждой операции есть возвращаемый тип данных

Стоит помнить, что каждая операция в C++ возвращает какое-то значение, привязанное к конкретному типу данных.

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

Присваивание

Самая базовая операция.

int a = 1;
a = 8;

Выполняется при помощи оператора присваивания =.

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

int a; 
int b;

a = b = 4;

Здесь сначала выполнится b = 4, вернёт 4, и потом выполнится a = 4.

Базовая арифметика

Здесь всё просто. С целыми и дробными числами мы можем выполнять базовые арифметические операции:

int a = 9 + 2;
int b = 9 - 2;
int c = 9 / 2; 
int d = 9 * 2;

Это сложение, вычитание, деление и умножение.

Порой нам нужно к уже имеющейся переменной добавить число, или поделить переменную на число, или сделать с ней ещё какую-то базовую арифметическую операцию.

Писать это через операции сложения, деления, и т.д. напрямую достаточно неудобно:

int a = a + 1;

Поэтому, для таких случаев в C++ есть специальные операторы:

a += 9; // a = a + 9;
b -= 9; // a = b - 9;
c /= 9; // c = c / 9;
d *= 9; // d = d * 9;

Остаток от деления

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

int a = 5 % 2;

Что такое остаток от деления, я надеюсь, вы знаете из школьного курса - грубо говоря, это тот остаток, который у нас остаётся от числа, когда мы пытаемся его нацело поделить в столбик.

Остаток от деления зачастую используется, например, для проверки того, что одно число делится на другое - в случае, если число a делится на число b, остаток от деления a на b (a % b) должен равняться 0, и какому-то числу от 0 До b в противном случае.

Для остатка от деления также есть оператор со знаком равенства:

a %= 5;

Префиксное увеличение

Для того, чтобы добавить к числу 1, можно использовать оператор префиксного увеличения. Выглядит это примерно так:

++a;

Префиксное - потому что оператор ++ записывается перед увеличиваемым значением.

Постфиксное увеличение

Также в C++ доступно и постфиксное увеличение переменной:

a++;

В этом случае к переменной a тоже будет добавлена единица

Различие префиксного и постфиксного увеличения

Как конечный итог выполнения, и a++ и ++a прибавляют к a единицу. Но всё таки у этих операторов есть чёткое различие, и оно заключается в возвращаемом значении.

Если выполнить следующий код:

int a = 1;
std::cout << a++ << std::endl;

...на экран выведется 1. То есть, при обращении к a++, мы получаем число до прибавления 1.

Если же поменять a++ на ++a, на экран выведется 2.

Префиксное и постфиксное уменьшение

Здесь всё абсолютно аналогично увеличению, но вместо прибавления единицы, происходит вычитание единицы, а операнд ++ заменяется на операнд --.

Операции сравнения

Здесь, в принципе, тоже нет ничего нового.

Основные операции сравнения в C++ записываются как: >, >=, <, <=.

Единственные особенности касаются операций проверки равенства двух чисел (==) и неравенства (!=).

Все эти операторы возвращают значение типа bool.

С оператором сравнения чисел == существует классическая ошибка, которую нам нужно обязательно рассмотреть:

int b;
std::cin >> b;

if (b = 5)
{
	std::cout << "You guessed the number!\n";
}

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

Причём этот код будет успешно скомпилирован и запущен, но условие if будет выполняться для любых введённых значений b. Почему?

Как уже говорилось ранее, каждая операция возвращает какое-то значение. Операция присваивания не исключение - она будет возвращать присвоенное значение.

В данном случае, операция b = 5 вернёт 5, которое потом преобразуется в bool по цепочке преобразований (см. урок о приведении типов). Из-за этого тело условного оператора будет выполняться всегда, и порой такую ошибку бывает очень сложно заметить.

Логические операции

Для составления логических условий так же используются логические операции. Все они возвращают значение типа bool.

Логические операторы:

  • || - или. Принимает на вход два аргумента типа bool. Возвращает true, если хотя бы один из аргументов true.
  • && - и. Принимает на вход для аргумента типа bool. Возвращает true только если оба аргумента тоже true.
  • ! - не. Принимает на вход один аргумент и возвращает его противоположное значение.

Простой пример:

int age = 17;
if (age > 16 && age < 18)
{
	std::cout << "Вам можно только ездить на мопеде.\n";
}

Что здесь происходит:

  1. Сначала вычисляется age > 16 - операция сравнения возвращает булевое значение
  2. Аналогично справа
  3. Потом оператор && подхватывает эти два значения и как-то обрабатывает

Другой пример - проверка того, что число делится на 3 или на 5:

int number = 18;
if (!(number % 3) || !(number % 5))
{
	std::cout << "Делится на 3 или на 5.\n";
}

Что происходит здесь?

  1. Компилятор видит выражение с или, и начинает разбирать левую его часть. Для начала вычисляется выражение в скобках. Как "промежуточный" результат получаем:
if (!0  || !(number %5))
  1. Оператор ! умеет работать только с булевыми значениями, поэтому компилятор выполняет цепочку преобразований для целого значения 0, и мы получаем:
if (!false || !(number % 5))
  1. Применяем отрицание:
if (true || !(number % 5))
  1. И здесь наступает очень важный момент. Оператор || видит, что справа от него уже стоит значение true. Зачем ему тогда проверять значение справа, если результат или уже гарантированно будет true? На этом шаге оператор сразу возвращает true, без вычисления !(number % 5)

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

#include <iostream>
int f()
{
	std::cout << "f()\n";
	return 5;
}

int main()
{
	if (!(false && f()))
	{
		std::cout << "Obviously false statement is false.\n";
	}
}

Если запустить этот код, в терминал не выведется строка f(), что значит, что функция f() даже не вызвалась, так как левый аргумент оператора && уже был false.

Бинарные операции

x & y // бинарное И
x | y // бинарное ИЛИ
x ^ y // бинарное НЕ-ИЛИ (XOR)
~x // бинарное НЕ

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

Про то, что такое в теории бинарные операции, рассказано в отдельной статье.

Здесь очень важно понимать различие x & y и x | y от x && y и x || y.

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

Бинарные же операторы принимают на вход два числа (или 1 в случае отрицания) и возвращают как результат своих действий такое же число - результат применения ко входным данным конкретной бинарной операции. В этом случае вне зависимости от значений аргументов при любых обстоятельствах будут происходится просто вычисления над битами.

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

Частично мы уже затронули унарные операции - это операции, которые принимают на вход только один аргумент. К ним можно также добавить:

  • - - возвращает число * (-1)
  • + - используется для того, чтобы намеренно указать на положительность какого-то числа. Используется не так часто, так что пока можно сильно в смысл унарного + не вникать.

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

Над строками тоже можно делать базовые операции. Бегло рассмотрим два примера.


Конкатенация - объединение двух строк:

std::cout << "Hello " + "world\n" << std::endl;

В данном случае оператор + вернёт строку Hello world\n.

Обратите внимание, что в этом случае мы не говорим об операции сложения - мы говорим именно об операции конкатенации, потому что объединение двух строк и сложение двух чисел - это совершенно разные операции как на уровне идеи, так и на уровне реализации.


Добавление в конец

std::string greetings = "Hello";
greetings += '!';

С помощью оператора += можно добавить в конец строки символ или строку.