Skip to the content.

Основные операторы C++:

Арифметические
Унарные (принимают 1 аргумент) Бинарные (принимают 2 аргумента)
Префиксные (p = ++a - вначале инкремент, потом присваивание) Постфиксные (p = a-- - вначале присваивание, потом декремент) +, -, *, /, %, +=, -=, *=, /=, %=
+, -, ++, -- ++, --
Битовые
Унарные Бинарные
~ (побитовое отрицание: 1001 -> 0110) & (AND), | (OR), ^ (XOR), &=, |=, ^=, сдвиги: >>, <<
Логические (bool)
Унарные Бинарные Сравнения
! &&, ||, x != y (XOR) ==, !=, <, >, <=, >=

Другие операторы:

Оператор “,” гарантирует что левый операнд будет вычислен раньше правого:

Оператор -> применим только к указателям и равнозначен двум операциям: разыменование указателя, а потом обращение к методу через .

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;:

Если бы мы ничего не возвращали (void), то такие “цепочки” стали бы невозможными, можно было бы вывести только один раз:

Перегрузка оператора ввода аналогична, 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) {} // так нельзя