В данной лабораторной работе Вам предстоит познакомиться с перегрузкой операторов С++ и применить ее в своем проекте.
-
- Перегрузка оператора сложения и вычитания
- Перегрузка оператора !=
- Перегрузка оператора <<
Начнем с того, что дадим определение оператору: оператор – это символ, который работает со значением или переменной. Например, =
, +
, -
, <
, >>
- все это является операторами. Но, если уйти немного глубже, то можно сделать вывод, что оператор - это не просто символ, а функция (метод).
К примеру, оператор присваивания
a = b;
a.equal(b); // Эквивалентная запись оператора присваивания
Имеет эквивалентную запись a.присвоить(b). У объекта а вызывается метод присвоить
и параметром передается значение b.
Перегрузка операторов — это возможность применять встроенные операторы языка к разным типам, в том числе и пользовательским. Другими словами, мы переопределяем поведение оператора вручную.
Перегруженные операторы — это просто функции (но особого типа) со специальным ключевым словом operator
, за которым следует символ перегружаемого оператора.
Например, функция оператора сложения выглядит так:
return_type operator+(params..){}
// Функция оператора сложения может иметь разный тип, в зависимости от того,
// какие объекты мы складываем. Например для целых чисел:
int operator+(params..){}
В данном примере, params
- это параметры, которые передаются в функцию оператора сложения. Но что конкретно это за параметры? Давайте разберем на примере простой реализации класса комплексного числа.
Небольшая справка: комплексное число имеет реальную и мнимую часть и представимо в виде Z = x + iy, где x - действительная часть, а y - мнимая часть.
Ниже приведен класс для работы с комплексными числами:
.h
class Complex {
int real, imag;
public:
Complex(int re,int im):real(re),imag(im){}
Complex() {
real = 0; // Действительная часть
imag = 0; // Мнимая часть
}
void display() const;
};
.cpp
void Complex::display() {
// Функция печати комплексного числа на экран
if(imag < 0)
std::cout << real << "-" << imag << "i" << std::endl;
else
std::cout << real << '+' << imag << "i" <<' \n';
}
Cоздадим два объекта комплексного числа:
// main.cpp
Complex c1(2,2);
Complex c2(2,2);
И попробуем их сложить:
Complex c3 = c1+c2;
Получаем ошибку, что закономерно. Оператор +
не понимает, как ему работать с типом данных Complex
. Если это были бы числа или строки, к примеру:
int a = 4;
int b = 3;
int c = a + b; // Оператор `+` арифметически суммирует для int
std::string str1 = "Hello, ";
std::string str2 = "World!";
std::string str3 = str1 + str2 // "Hello, world!" - оператор + для строк конкатенирует их
Нам нужно перегрузить оператор сложения конкретно для логики комплексных чисел. Перегрузка выглядит следующим образом:
Complex operator+(const Complex c1){
// Перегрузка оператора сложения для комплесных чисел
Complex temp;
temp.real = real + c1.real;
temp.imag = imag + c1.imag;
return temp;
}
Теперь, если мы попробуем сложить комплексные числа, то получим следующее
Complex c1(2,2);
Complex c2(2,2);
Complex c3 = c1+c2;
c3.display(); // 4 + 4i
Что именно произошло и почему суммирование работает именно так? Внимательно посмотрите на то, как мы переопределили работу оператора +
в классе Сomplex:
Complex operator+(const Complex c1) {
...
}
Возвращаемый тип функции operator+
- Complex, а входным параметром является объект этого же типа. Внутри функции сначала происходит создание промежуточного объекта, который и будет по итогу возвращен из функции. Реальная часть этого объекта будет состоять из реальной части объекта, по отношению к которому был применен этот оператор и реальной части объекта, переданного параметром.
То есть, запись c1 + c2, по сути, эквивалентна следующему: c1.operator+
(c2), operator+ - это просто метод, который применяется к объекту класса c1.
Аналогичным образом, можно перегрузить и вычитание, просто поменяв знак, а метод будет называться operator-
.
В отличие от оператора +
, -
и других арифметических операций, данный оператор, как и ==
, <
, >
как правило используют не для того, чтобы получить новый объект, а для того чтобы получить булевое значение true
или false
.
Вернемся к нашему классу комплексных чисел. Перегрузим оператор != так, чтобы он проверял два числа на неравенство (действительные части не равны, мнимые части не равны):
bool Complex::operator!=(const Complex c1){
if(real != c1.real || real != c1.imag)
return true;
else
return false;
}
Логика работы такая же, как и у оператора +
:
Complex c1(2,2);
Complex c2(2,2);
std::cout << (c1 != c2) << std::endl; // false
А вот тут уже поинтереснее. Сделаем так, чтобы можно было печатать комплексное число, не обращаясь к его атрибутам напрямую (imag, real), т.к во первых это нарушает принцип инкапсуляции, а во вторых - некрасиво. Добьемся того, чтобы это можно было сделать через cout:
Complex c1(2, 2);
std::cout << c1;
Сделать это можно следующим образом:
friend ostream& operator<<(ostream& os,Complex c1) {
if (c1.imag < 0)
os << c1.real << "-" << c1.imag<< "i"; // os - объект потокового вывода, например, сout
else
os << c1.real << "+" << c1.imag << "i";
return os;
}
Перегрузка такого типа внешне отличается от раннее рассмотренных (унарных операторов). Во первых, возвращаемый тип - ostream
. Он является одним из производных класса fstream
, связанный через наследование. Оператор принимает на вход 2 параметра: объект класса ostream и объект класса, для которого мы хотим определить логику вывода. То есть, оператор "двухсторонний":
cout << complex_number; // cout - левая часть - объект ostream, справа - объект класса Complex
Такой тип оператора называется бинарным.
В данном примере, оператор возвращает объект типа ostream, что позволяет каскадировать несколько операторов <<
, например:
std::cout << a << b << c << std::endl
// Сначала выполняется cout << a, снова возвращается объект ostream, выполняется
(std::cout << a) << b ... // И т.д
После перегрузки оператора <<
для нашего класса комплексных чисел получаем:
Complex c1(2,2);
std::cout << c1 << std::endl; // 4 + 4i
Как вы могли заметить, метод объявлен как дружественный. Причина тому - этот метод не вызывается объектом класса напрямую, а, по сути, находится вне привязки к классу. Он принимает 2 параметра, первым объект класса ostream, а вторым - какой-либо объект).
- Перегрузить оператор сравнения < (меньше) для сравнения двух объектов структуры по Вашему варианту. Например, для варианта с играми, оператор сравнения может возвращать результат сравнения фильмов по годам:
Game minecraft; // 2009 года
Game doom_3; // 2004 года
std::cout << Minecraft > Doom3 << std::endl // True
- Перегрузить оператор == (оператор равенства). Например, для варианта с фильмами, оператор сравнения может возвращать результат сравнения названия фильмов:
Film cheburashka // 2023
Film john_wick // 2023
if (cheburashka == john_wick) { // True
...
}
- Перегрузить оператор потокового вывода << для ofstream внутри вашего класса (игр, фильмов и т.д), для того чтобы можно было записывать данные в файл при помощи следущей записи:
// Функция writeAll класса CSVWriter:
for (auto &obj: games) {
fout << obj;
}
- Для того, чтобы в TextBrowser можно было писать так же, как в консоль через cout, перегрузите оператор << для QtextBrowser, чтобы им можно было пользоваться следующим образом:
ui->Browser << text1; // Работает аналогично ui->Browser->append(text1);
- Что такое перегрузка операторов?
- Что такое бинарный оператор и какие у него особенности?
- Что такое унарный оператор и какие у него особенности?
- Какие операторы можно перегрузить?