Skip to content

Latest commit

 

History

History
231 lines (178 loc) · 12.7 KB

File metadata and controls

231 lines (178 loc) · 12.7 KB

Объектно-ориентированное программирование

Лабораторная работа 6. Перегрузка операторов

В данной лабораторной работе Вам предстоит познакомиться с перегрузкой операторов С++ и применить ее в своем проекте.

Теоретическая справка

Начнем с того, что дадим определение оператору: оператор – это символ, который работает со значением или переменной. Например, =, +, -, <, >> - все это является операторами. Но, если уйти немного глубже, то можно сделать вывод, что оператор - это не просто символ, а функция (метод).

К примеру, оператор присваивания

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

Перегрузка оператора get from (<<)

А вот тут уже поинтереснее. Сделаем так, чтобы можно было печатать комплексное число, не обращаясь к его атрибутам напрямую (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, а вторым - какой-либо объект).

Задание

  1. Перегрузить оператор сравнения < (меньше) для сравнения двух объектов структуры по Вашему варианту. Например, для варианта с играми, оператор сравнения может возвращать результат сравнения фильмов по годам:
Game minecraft; // 2009 года
Game doom_3; // 2004 года
std::cout << Minecraft > Doom3 << std::endl // True
  1. Перегрузить оператор == (оператор равенства). Например, для варианта с фильмами, оператор сравнения может возвращать результат сравнения названия фильмов:
Film cheburashka // 2023
Film john_wick // 2023
if (cheburashka == john_wick) { // True
...
}
  1. Перегрузить оператор потокового вывода << для ofstream внутри вашего класса (игр, фильмов и т.д), для того чтобы можно было записывать данные в файл при помощи следущей записи:
// Функция writeAll класса CSVWriter:
for (auto &obj: games) {
 fout << obj;
}
  1. Для того, чтобы в TextBrowser можно было писать так же, как в консоль через cout, перегрузите оператор << для QtextBrowser, чтобы им можно было пользоваться следующим образом:
ui->Browser << text1; // Работает аналогично ui->Browser->append(text1);

Контрольные вопросы

  1. Что такое перегрузка операторов?
  2. Что такое бинарный оператор и какие у него особенности?
  3. Что такое унарный оператор и какие у него особенности?
  4. Какие операторы можно перегрузить?