Содержание
Перегрузка операторов
Перегрузка операторов - это возможность определять собственное поведение операторов для пользовательских типов. Это позволяет работать с объектами классов так же интуитивно, как со встроенными типами.
Основные принципы перегрузки операторов
Можно перегружать большинство операторов C++ (кроме ::
, .*
, .
, ?:
)
Перегруженные операторы сохраняют приоритет и ассоциативность
Нельзя создавать новые операторы
Перегрузка должна быть интуитивно понятной
Перегрузка операторов как членов класса
Пример с классом комплексных чисел:
#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;
}
Пояснение:
Объекты таких классов называют функторами
Позволяют использовать объекты как функции
Могут хранить состояние (в отличие от обычных функций)
Перегрузка операторов позволяет:
Сделать код более читаемым и интуитивно понятным
Обеспечить естественный синтаксис работы с пользовательскими типами
Реализовать поддержку стандартных операций для своих классов
Важные правила:
Сохраняйте естественную семантику операторов
Соблюдайте принцип наименьшего удивления
Перегружайте операторы только тогда, когда это действительно необходимо
Для симметричных операторов (как +
) используйте внешние функции
Операторы, изменяющие объект, лучше делать методами класса