Основные операторы C++:
Арифметические | |||
---|---|---|---|
Унарные (принимают 1 аргумент) | Бинарные (принимают 2 аргумента) | ||
Префиксные (p = ++a - вначале инкремент, потом присваивание) | Постфиксные (p = a-- - вначале присваивание, потом декремент) | +, -, *, /, %, +=, -=, *=, /=, %= | |
+, -, ++, -- | ++, -- | ||
Битовые | |||
Унарные | Бинарные | ||
~ (побитовое отрицание: 1001 -> 0110) | & (AND), | (OR), ^ (XOR), &=, |=, ^=, сдвиги: >>, << | ||
Логические (bool) | |||
Унарные | Бинарные | Сравнения | |
! | &&, ||, x != y (XOR) | ==, !=, <, >, <=, >= |
Другие операторы:
- Оператор присваивания: =
- Специальные:
- указатели: * &
- постфиксные: -> ->*
- особые: , . ::
- Скобки: [] ()
- Оператор приведения: (type)
- Тернарный: x ? y : z
- Работа с динамической памятью: new new[] delete delete[]
Оператор “,” гарантирует что левый операнд будет вычислен раньше правого:
Оператор -> применим только к указателям и равнозначен двум операциям: разыменование указателя, а потом обращение к методу через .
foo(), bar() // foo() раньше чем bar()
a = (foo(), bar()) // a = bar()
Нельзя перегружать операторы (все остальные можно перегружать): . :: и тернарный оператор
Перегрузка операторов
При помощи внешних функций
class Vector
{...};
// унарный минус
Vector operator-(Vector const& v) { return Vector(-v.x, -v.y); }
// оператор сложения
Vector operator+(Vector const& v1, Vector const& v2)
{
return Vector(v1.x + v2.x, v1.y + v2.y);
}
// умножение вектора на число
Vector operator*(Vector const& v, double d)
{
return Vector(v.x * d, v.y * d);
}
// умножение числа на вектор
Vector operator*(double d, Vector const& v)
{
return v * d; // пользуемся умножением вектора на число
}
Внутри классов
struct Vector
{
// унарный минус
Vector operator-() const { return Vector(-x, -y); }
// бинарный минус
Vector operator-(Vector const& other) const { return Vector(x - other.x, y - other.y); }
Vector & operator *=(double d)
{
x *= d;
y *= d;
return *this;
}
double operator[](size_t i) const
{
return (i==0) ? x : y;
}
// оператор () в отличии от оператора [] может принимать произвольное число значений
bool operator()(double d) const {...}
void operator()(double a, double b) {...}
double x, y;
};
Некоторые операторы можно определять только внутри класса, как методы: (type), [], (), ->, ->*, =
Перегрузка логических операторов отменяет ленивое вычисление (значения всех “скобок” будут посчитаны заранее) и вообще лучше избегать их перегрузки.
Перегрузка инкремента и декремента
Для того что бы различать постфиксный и префиксный операторы, в постфиксный оператор добавляют специальный параметр-заглушку:
class My_int
{
private:
int value;
public:
// prefix: ++a
My_int & operator++()
{
this->value += 1;
return *this; // если не возвращать ссылку, то нельзя будет присваивать
}
//postfix: a++
My_int operator++(int)
{
My_int tmp(*this); // сохраняем старое значение в временную переменную
this->value += 1;
return tmp;
}
};
My_int x = 5;
My_int y = ++x;
std::cout << y.get_value() << " " << x.get_value() << "\n"; // 6 6
My_int x = 5;
My_int y = x++;
std::cout << y.get_value() << " " << x.get_value() << "\n"; // 5 6
Кстати, отсюда видно что вызов постфиксного оператора более трудоемок (копирование старого значения), по этой причине в циклах чаще всего используют префиксный.
Перегрузка операторов ввода-вывода
Допустим у нас есть класс с множеством полей, стоит задача о выводе множества полей этого класса:
class Point
{
public:
Point(double x, double y, double z) : x(x), y(y), z(z) {}
double get_x() const { return x; }
double get_y() const { return y; }
double get_z() const { return z; }
private:
double x;
double y;
double z;
};
Для вывода приходиться все время обращаться к геттерам:
Point p(1, 3, -2);
std::cout << p.get_x() << " " << p.get_y() << " " << p.get_z() << "\n";
Либо, что более красиво, написать метод для вывода:
void print()
{
std::cout << x << " " << y << " " << z << "\n";
}
Однако есть ньюансы, так как метод типа void, то и вызвать его во время вывода нельзя, вывод приходиться разбивать на несколько строк:
std::cout << "My point is: \n";
p.print();
Решить проблему можно перегрузив оператор вывода.
Стоит понять следующую строку: std::cout << p;
. Если « - это бинарный оператор, то левая и правая его части это операнды: объект класса std::cout и объект нашего класса. Перегрузка выглядит так потому, что std::cout являеться объектом класса std::ostream:
std::ostream & operator<<(std::ostream &out, Point const & point)
{
out << point.get_x() << " " << point.get_y() << " " << point.get_z() << "\n";
return out;
}
Что позволяет нам теперь делать такой вывод:
std::cout << "My point is: \n" << p;
Возвращать нужно обязательно ссылку на std::ostream, только потому, что std::ostream запрещает свое копирование!
Вот так работает “цепочка” std::cout << x << y;
:
- Вначале отработает
(std::cout << x)
: выведет х и вернёт нам out - ссылку на std::cout - Потом отработает
std::cout << y
Если бы мы ничего не возвращали (void), то такие “цепочки” стали бы невозможными, можно было бы вывести только один раз:
- Отработает
(std::cout << x)
: выведет x и вернёт void void << y
- не имеет смысла
Перегрузка оператора ввода аналогична, std::cin объект std::istream:
class Point
{
public:
double x;
double y;
double z;
};
std::istream & operator>>(std::istream & in, Point & point)
{
in >> point.x >> point.y >> point.z; // либо через private + friend
return in;
}
Point p;
std::cin >> p;
Перегружать операторы ввода-вывода можно только как внешние функции, т.к. что бы перегрузить их как методы надо изменять соответствующие классы istream и ostream, которые описаны где-то внутри стандартной библиотеки.
Перегрузка оператора преобразования
class Dollars
{
private:
int dollars;
public:
// Перегрузка операции преобразования значений типа Dollars в значения типа int
operator int() { return dollars; }
};
Правила хорошего тона
Бинарные операторы с короткой формой
Перегружать бинарные операторы у которых есть короткая форма (+ и += например) стоит так: короткая форма - как метод класса, полная форма - как внешняя функция:
struct String
{
// конструктор работает в том числе как преобразование C-style строк в наши строки String
String (char const * c_style_str) {...}
String & operator+=(String const & other)
{
...
return *this;
}
};
// Стоит обратить внимание, что s1 локально копируется и не изменяется!
String operator+(String s1, String const & s2)
{
return s1 += s2;
}
String s1("world");
String s2 = "Hello " + s1;
Если бы оператор + был перегружен как метод, то одним из условий его вызова являлось бы существование левого операнда (ведь мы вызываем оператор у экземпляра - левого операнда), а у нас “Hello” на момент вызова оператора еще не существует, а значит String s2 = "Hello " + s1;
было бы невозможным, не будет работать неявное приведение (конструктором) левого аргумента. Однако, если определить оператор + как функицю, то "Hello "
при передаче в функцию будет неявно преобразовано конструктором в String.
Стоит отметить, что String s2 = s1 + " Hello";
будет работать и для функции и для метода.
Так же напомню, что передача аргумента по ссылке эффективнее, чем по значению поскольку не происходит копирования в локальную переменную
Операторы сравнения (== != > < >= <=)
Во-первых, если был перегружен оператор ==
, то нужно перегрузить вместе с ним !=
. Если был перегружен один из: > < >= <=
, то нужно перегрузить и все остальные.
Во-вторых, достаточно перегрузить всего два оператора, а все остальные реализуються через них (== и <
):
// реализуем как внешние функции
bool operator==(String const &a , String const &b)
{ return ... // уникальный код }
bool operator!=(String const &a , String const &b)
{ return !(a == b); } // используем уже готовый ==
bool operator<(String const &a , String const &b)
{ return ... // уникальный код }
// Используем уже готовый <
bool operator>(String const &a , String const &b)
{ return b < a; } // переставили местами
bool operator<=(String const &a , String const &b)
{ return !(b < a); } // меньше либо равно -> не больше
bool operator>=(String const &a , String const &b)
{ return !(a < b); }
Можно даже еще короче, обойтись только оператором <
, а ==
реализовать так: !(b < a) && !(a < b)
. Однако, это не всегда эффективно, ведь оператор <
в данном случае вызывается дважды.
Остальное
При перегрузке приоритет операторов остается прежним
Можно перегружать операторы только для пользовательских типов при этом хотя бы один аргумент оператора должен быть пользовательским:
void operator*(double d, int i) {} // так нельзя