Перегрузка операторов

Перегрузка операторов - это возможность определять собственное поведение операторов для пользовательских типов. Это позволяет работать с объектами классов так же интуитивно, как со встроенными типами.

Основные принципы перегрузки операторов
  1. Можно перегружать большинство операторов C++ (кроме ::, .*, ., ?:)

  2. Перегруженные операторы сохраняют приоритет и ассоциативность

  3. Нельзя создавать новые операторы

  4. Перегрузка должна быть интуитивно понятной


Перегрузка операторов как членов класса

Пример с классом комплексных чисел:
#include <iostream> class Complex { private: double real; double imag; public: Complex(double r = 0.0, double i = 0.0) : real(r), imag(i) {} // Перегрузка оператора + (член класса) Complex operator+(const Complex& other) const { return Complex(real + other.real, imag + other.imag); } // Перегрузка оператора - (член класса) Complex operator-(const Complex& other) const { return Complex(real - other.real, imag - other.imag); } // Перегрузка оператора += (член класса) Complex& operator+=(const Complex& other) { real += other.real; imag += other.imag; return *this; } // Перегрузка оператора вывода << (должна быть дружественной функцией) friend std::ostream& operator<<(std::ostream& os, const Complex& c); }; std::ostream& operator<<(std::ostream& os, const Complex& c) { os << "(" << c.real << ", " << c.imag << ")"; return os; } int main() { Complex a(1.0, 2.0); Complex b(3.0, 4.0); Complex c = a + b; // Используем перегруженный + Complex d = a - b; // Используем перегруженный - a += b; // Используем перегруженный += std::cout << "a = " << a << std::endl; std::cout << "b = " << b << std::endl; std::cout << "c = a + b = " << c << std::endl; std::cout << "d = a - b = " << d << std::endl; return 0; }
Пояснение:
  • Операторы +, - и += перегружены как методы класса

  • Оператор << перегружен как дружественная функция, так как левый операнд - ostream

  • Операторы + и - возвращают новый объект, а += возвращает ссылку на текущий объект


Перегрузка операторов как внешних функций

Пример с классом строки:
#include <iostream> #include <cstring> class MyString { private: char* str; public: MyString(const char* s = "") { str = new char[strlen(s) + 1]; strcpy(str, s); } ~MyString() { delete[] str; } // Перегрузка оператора [] (член класса) char& operator[](size_t index) { return str[index]; } // Дружественные функции для перегрузки операторов friend MyString operator+(const MyString& lhs, const MyString& rhs); friend bool operator==(const MyString& lhs, const MyString& rhs); friend std::ostream& operator<<(std::ostream& os, const MyString& s); }; // Перегрузка оператора + (внешняя функция) MyString operator+(const MyString& lhs, const MyString& rhs) { char* newStr = new char[strlen(lhs.str) + strlen(rhs.str) + 1]; strcpy(newStr, lhs.str); strcat(newStr, rhs.str); MyString result(newStr); delete[] newStr; return result; } // Перегрузка оператора == (внешняя функция) bool operator==(const MyString& lhs, const MyString& rhs) { return strcmp(lhs.str, rhs.str) == 0; } // Перегрузка оператора вывода << std::ostream& operator<<(std::ostream& os, const MyString& s) { os << s.str; return os; } int main() { MyString s1("Hello"); MyString s2("World"); MyString s3 = s1 + " " + s2; // Используем перегруженный + std::cout << "s1: " << s1 << std::endl; std::cout << "s2: " << s2 << std::endl; std::cout << "s3: " << s3 << std::endl; s1[0] = 'h'; // Используем перегруженный [] std::cout << "s1 after modification: " << s1 << std::endl; if (s1 == "hello") { std::cout << "s1 equals \"hello\"" << std::endl; } return 0; }
Пояснение:
  • Оператор [] перегружен как метод класса, так как требует доступа к приватным данным

  • Операторы + и == перегружены как внешние дружественные функции

  • Оператор + создает новую строку и возвращает ее по значению

  • Оператор == возвращает bool результат сравнения


Перегрузка операторов инкремента и декремента

Пример с классом-счетчиком:
#include <iostream> class Counter { private: int count; public: Counter(int c = 0) : count(c) {} // Префиксный инкремент (++counter) Counter& operator++() { ++count; return *this; } // Постфиксный инкремент (counter++) Counter operator++(int) { Counter temp = *this; ++count; return temp; } // Префиксный декремент (--counter) Counter& operator--() { --count; return *this; } // Постфиксный декремент (counter--) Counter operator--(int) { Counter temp = *this; --count; return temp; } friend std::ostream& operator<<(std::ostream& os, const Counter& c); }; std::ostream& operator<<(std::ostream& os, const Counter& c) { os << c.count; return os; } int main() { Counter c(5); std::cout << "Initial: " << c << std::endl; std::cout << "Prefix ++: " << ++c << std::endl; std::cout << "After prefix: " << c << std::endl; std::cout << "Postfix ++: " << c++ << std::endl; std::cout << "After postfix: " << c << std::endl; std::cout << "Prefix --: " << --c << std::endl; std::cout << "After prefix: " << c << std::endl; std::cout << "Postfix --: " << c-- << std::endl; std::cout << "After postfix: " << c << std::endl; return 0; }
Пояснение:
  • Префиксные версии возвращают ссылку на измененный объект

  • Постфиксные версии принимают фиктивный параметр int и возвращают временную копию

  • Постфиксные операторы менее эффективны из-за создания временного объекта


Перегрузка операторов ввода/вывода

Пример с классом точки:
#include <iostream> class Point { private: int x; int y; public: Point(int x = 0, int y = 0) : x(x), y(y) {} // Дружественные функции для операторов ввода/вывода friend std::ostream& operator<<(std::ostream& os, const Point& p); friend std::istream& operator>>(std::istream& is, Point& p); }; std::ostream& operator<<(std::ostream& os, const Point& p) { os << "Point(" << p.x << ", " << p.y << ")"; return os; } std::istream& operator>>(std::istream& is, Point& p) { std::cout << "Enter x and y coordinates: "; is >> p.x >> p.y; return is; } int main() { Point p1(3, 4); std::cout << p1 << std::endl; Point p2; std::cin >> p2; std::cout << "You entered: " << p2 << std::endl; return 0; }
Пояснение:
  • Оператор << должен быть дружественной функцией, так как левый операнд - ostream

  • Оператор >> должен принимать неконстантную ссылку на объект для модификации

  • Оба оператора возвращают ссылку на поток для поддержки цепочки вызовов


Перегрузка операторов сравнения

Пример с классом даты:
#include <iostream> #include <tuple> // для std::tie class Date { private: int day; int month; int year; public: Date(int d, int m, int y) : day(d), month(m), year(y) {} // Перегрузка операторов сравнения bool operator==(const Date& other) const { return day == other.day && month == other.month && year == other.year; } bool operator!=(const Date& other) const { return !(*this == other); } bool operator<(const Date& other) const { return std::tie(year, month, day) < std::tie(other.year, other.month, other.day); } bool operator>(const Date& other) const { return other < *this; } bool operator<=(const Date& other) const { return !(*this > other); } bool operator>=(const Date& other) const { return !(*this < other); } friend std::ostream& operator<<(std::ostream& os, const Date& d); }; std::ostream& operator<<(std::ostream& os, const Date& d) { os << d.day << "/" << d.month << "/" << d.year; return os; } int main() { Date d1(15, 6, 2023); Date d2(20, 6, 2023); Date d3(15, 6, 2023); std::cout << "d1: " << d1 << std::endl; std::cout << "d2: " << d2 << std::endl; std::cout << "d3: " << d3 << std::endl; std::cout << "d1 == d2: " << (d1 == d2) << std::endl; std::cout << "d1 == d3: " << (d1 == d3) << std::endl; std::cout << "d1 != d2: " << (d1 != d2) << std::endl; std::cout << "d1 < d2: " << (d1 < d2) << std::endl; std::cout << "d1 > d2: " << (d1 > d2) << std::endl; return 0; }
Пояснение:
  • Используется std::tie для удобного сравнения нескольких полей

  • Некоторые операторы выражаются через другие (например, != через ==)

  • Все операторы объявлены как const, так как не изменяют объект


Перегрузка оператора вызова функции ()

Пример с классом-функтором:
#include <iostream> class Multiplier { private: int factor; public: Multiplier(int f) : factor(f) {} // Перегрузка оператора () int operator()(int x) const { return x * factor; } }; int main() { Multiplier times2(2); Multiplier times5(5); std::cout << "times2(10) = " << times2(10) << std::endl; std::cout << "times5(10) = " << times5(10) << std::endl; std::cout << "times2(times5(3)) = " << times2(times5(3)) << std::endl; return 0; }
Пояснение:
  • Объекты таких классов называют функторами

  • Позволяют использовать объекты как функции

  • Могут хранить состояние (в отличие от обычных функций)


Перегрузка операторов позволяет:

  1. Сделать код более читаемым и интуитивно понятным

  2. Обеспечить естественный синтаксис работы с пользовательскими типами

  3. Реализовать поддержку стандартных операций для своих классов

Важные правила:

  1. Сохраняйте естественную семантику операторов

  2. Соблюдайте принцип наименьшего удивления

  3. Перегружайте операторы только тогда, когда это действительно необходимо

  4. Для симметричных операторов (как +) используйте внешние функции

  5. Операторы, изменяющие объект, лучше делать методами класса


Комментарии

Добавить комментарий

Чтобы оставить комменатрий необходимо Авторизоваться