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

Просмотр 0 веток ответов
  • Автор
    Сообщения
    • #6483
      @admin

      Решаемая задача:

      1. Создать класс для моделирования обработки комплексных чисел, имеющий:
        • конструкторы: без параметров, копирования, задания действительной и мнимой частей;
        • методы установки и получения действительной и мнимой частей;
        • перегруженные операции +, -, *, /, для:
          • операций над комплексными числами;
          • случая когда первый операнд комплексный, а второй — действительный.
      2. Добоавить глобальные перегрузки опеторов +, -, *, / для случая, когда первый операнд действительный, а второй — комплесный.
      3. Разработать программу, содержащую меню, позволяющее проверить функциональность класса;
      4. Разработать программу, позволяющую создавать динамический массив комплексных чисел с использованием стандартного шаблонного контейнера vector и осуществлять его упорядочивание по заданным критериям:
        1. возрастанию или убыванию модуля комплексного числа;
        2. возрастанию или убыванию действительной части;
        3. возрастанию или убыванию мнимой части.
      5. Исходный код решения вы найдете в репозитории — первая часть задания в первом коммите, вторая — во втором.

        1.1 Заготовка

        Создадим простой класс, содержащий два поля для хранения целой и мнимой частей, конструкторы и функции получения/установки значений полей:

        class ComplexNumber {
        public:
          ComplexNumber();
          ComplexNumber(const ComplexNumber& number);
          ComplexNumber(double real, double imaginary);
          double real() const;
          double imaginary() const;
          void real(double real);
          void imaginary(double imaginary)
        private:
          double m_real, m_imaginary;
        };

        Эти методы имеют очень простую реализацию, единственное, что тут стоит отметить — методы, возвращающие значения полей помечены спецификатором const, так как в дальнейшем их нужно применять к константным объектам. Этот спецификатор для функции означает, что функция не изменяет объект по указателю this.

        ComplexNumber::ComplexNumber()
          : m_real(0), m_imaginary(0) {
        }
        
        ComplexNumber::ComplexNumber(const ComplexNumber& other)
          : m_real(other.m_real), m_imaginary(other.m_imaginary) {
        }
        
        ComplexNumber::ComplexNumber(double real, double imaginary)
          : m_real(real), m_imaginary(imaginary) {
        }
        
        double ComplexNumber::real() const {
          return m_real;
        }
        
        void ComplexNumber::real(double real) {
          m_real = real;
        }
        
        double ComplexNumber::imaginary() const {
          return m_imaginary;
        }
        
        void ComplexNumber::imaginary(double imaginary) {
          m_imaginary = imaginary;
        }

        1.2 Операторы ComplexNumber op ComplexNumber

        Добавим в класс первые 4 оператора:

        class ComplexNumber {
        public:
          // ...
          ComplexNumber operator+(const ComplexNumber& rhs) const;
          ComplexNumber operator-(const ComplexNumber& rhs) const;
          ComplexNumber operator*(const ComplexNumber& rhs) const;
          СomplexNumber operator/(const ComplexNumber& rhs) const;
          // ...
        };

        Каждый из этих операторов вызывается когда слева и справа от знака оператора стоит ComplexNumber, например:

        ComplexNumber a(5,5), b(3,4);
        a+b;

        Эти операторы принимают по одному аргументу, т. к. вторым является this, соответствующий объекту, стоящему слева от знака. Реализация этих операторов также не вызывает затруднений:

        ComplexNumber ComplexNumber::operator+(const ComplexNumber& rhs) const {
          return ComplexNumber(real() + rhs.real(), imaginary() + rhs.imaginary());
        }
        
        ComplexNumber ComplexNumber::operator-(const ComplexNumber& rhs) const {
          return ComplexNumber(real() - rhs.real(), imaginary() - rhs.imaginary());
        }
        
        // (a + bi)*(c + di) = (a*с - b*d) + (a*d + b*c)i.
        ComplexNumber ComplexNumber::operator*(const ComplexNumber& rhs) const {
          double realPart = real()*rhs.real() - imaginary()*rhs.imaginary();
          double imaginaryPart = real()*rhs.imaginary() + imaginary()*rhs.real();
          return ComplexNumber(realPart, imaginaryPart);
        }
        
        // (a + bi)/(c + di) = (a*с + b*d)/(c*c + d*d) + (b*c - a*d)/(c*c + d*d)i.
        ComplexNumber ComplexNumber::operator/(const ComplexNumber& rhs) const {
          double demoninator = rhs.real()*rhs.real() + rhs.imaginary()*rhs.imaginary();
          if (std::abs(demoninator) < 0.00001) {
            throw std::overflow_error("division by zero");
          }
          double realPart = (real()*rhs.real() + imaginary()*rhs.imaginary())/demoninator;
          double imaginaryPart = (imaginary()*rhs.real() - real()*rhs.imaginary())/demoninator;
          return ComplexNumber(realPart, imaginaryPart);
        }

        В функции деления может возникать деление на ноль, приводящее к переполнению — поэтому эта функция вырабатывает исключение типа std::overflow_error в таких случаях.

        1.3 Операторы ComplexNumber op real

        Операторы, обрабатывающие ситуацию, когда справа от знака оператора стоит обычное число (double) реализуются точно также как и описанные в части 1.2. Нам достаточно перегрузить дополнительные 4 функции-члена:

        class ComplexNumber {
        public:
          // ...
          ComplexNumber operator+(const double& rhs) const;
          ComplexNumber operator-(const double& rhs) const;
          ComplexNumber operator*(const double& rhs) const;
          ComplexNumber operator/(const double& rhs) const;
          // ...
        }

        Во время реализации таких операторов целесообразно вызывать операторы, описанные выше, а не дублировать код — для этого на базе реального числа конструируется

        ComplexNumber:
        ComplexNumber ComplexNumber::operator+(const double& rhs) const {
          return *this + ComplexNumber(rhs, 0);
        }
        
        ComplexNumber ComplexNumber::operator-(const double& rhs) const {
          return *this - ComplexNumber(rhs, 0);
        }
        
        ComplexNumber ComplexNumber::operator*(const double& rhs) const {
          return *this * ComplexNumber(rhs, 0);
        }
        
        ComplexNumber ComplexNumber::operator/(const double& rhs) const {
          return *this / ComplexNumber(rhs, 0);
        }

        1.4 Операторы real op ComplexNumber

        Операторы, слева от которых стоит обычное (не комплексное) число не могут быть реализованы как функции-члены класса ComplexNumber. Они должны описываться как функции от двух аргументов:

        ComplexNumber operator+(const double& lhs, const ComplexNumber& rhs);
        ComplexNumber operator-(const double& lhs, const ComplexNumber& rhs);
        ComplexNumber operator*(const double& lhs, const ComplexNumber& rhs);
        ComplexNumber operator/(const double& lhs, const ComplexNumber& rhs);

        Можно объявить их внутри класса ComplexNumber дружественными, однако, не требуется доступ к деталям реализации класса (закрытым полям или методам) — их можно реализовать, используя методы получения мнимой и действительной частей. Дружба — является сильным отношением, т. к. его нельзя изменить во время работы программы (без перекомпиляции), а ценностью в программировании является ослабление зависимостей [1], поэтому эти операторы обяъвлены вне класса.

        Релизованы операторы подобно тому, как мы поступали в разделе 1.3:

        ComplexNumber operator+(const double& lhs, const ComplexNumber& rhs) {
          return ComplexNumber(lhs, 0) + rhs;
        }
        
        ComplexNumber operator-(const double& lhs, const ComplexNumber& rhs) {
          return ComplexNumber(lhs, 0) - rhs;
        }
        
        ComplexNumber operator*(const double& lhs, const ComplexNumber& rhs) {
          return ComplexNumber(lhs, 0) * rhs;
        }
        
        ComplexNumber operator/(const double& lhs, const ComplexNumber& rhs) {
          return ComplexNumber(lhs, 0) / rhs;
        }

        1.5 Оператор потокового вывода

        В дальнейшем нам потребуется выводить числа на экран (в поток cout). Для этого целесообразно определить оператор потокового вывода:

        std::ostream& operator<<(std::ostream& ost, const ComplexNumber& rhs);

        И реализовать его:

        std::ostream& operator<<(std::ostream& ost, const ComplexNumber& rhs) {
          ost << '(' << rhs.real();
          if (rhs.imaginary() < 0) {
            ost << " - ";
          }
          else {
            ost << " + ";
          }
          ost << rhs.imaginary() << "i)";
          return ost;
        }

        Этот оператор принимает и возвращает поток ostream, частным случае которого являются cout или ofstream — за счет этого он может применяться как для вывода на экран, так и в файл.

        1.6 Демонстрационная программа

        Теперь напишем демонстрационную программу, вызывающую все наши операторы. Пусть программа запрашивает у пользователя действие — ввод чисел или выполнение одной из трех групп операций (из разделов 1.2-1.4). В случае операций — запрашивает один из 4 типов оператора, выполняет вычисления и выводит результат на экран. Нам потребуется ряд вспомогательных функций — ввод числа с входного потока, сопровождающийся выводом в выходной поток подсказки для ввода:

        ComplexNumber readComplex(std::istream& ist, std::ostream& ost) {
          double real, imaginary;
          ost << "enter complex as <real part> <space> <imaginary part>: ";
          ist >> real >> imaginary;
          return ComplexNumber(real, imaginary);
        }

        Функця ввода оператора (+, -, / или *), в случает некорректного ввода выбрасывает исключение:

        char readOperator(std::istream& ist, std::ostream& ost) {
          char op;
          ost << "enter operator +, -, * or / : ";
          ist >> op;
          if (op != '+' && op != '-' && op != '/' && op != '*') {
            throw  std::range_error("wrong operator symbol");
          }
          return op;
        }

        Напишем теперь функцию main():

        int main() {
          ComplexNumber a, b, result;
          double real;
          while (true) {
            int menuPoint = 0;
            char op;
            cout << "enter: " << endl
                 << "\t0 - exit;" << endl
                 << "\t1 - input A;" << endl
                 << "\t2 - input B;" << endl
                 << "\t3 - calculate A op B;" << endl
                 << "\t4 - calculate A op real;" << endl
                 << "\t5 - calculate real op B;" << endl
                 << ": ";
            cin >> menuPoint;
            if (menuPoint == 0)
              break;
            switch (menuPoint) {
            case 1:
              a = readComplex(cin, cout);
              break;
            case 2:
              b = readComplex(cin, cout);
              break;
            case 3:
              op = readOperator(cin, cout);
              cout << endl << a << op << b << " = " << calc(a, op, b) << endl;
              break;
            case 4:
              cout << "real number: ";
              cin >> real;
              op = readOperator(cin, cout);
              cout << endl << a << op << ' ' << real << " = " << calc(a, op, real) << endl;
              break;
            case 5:
              cout << "real number: ";
              cin >> real;
              op = readOperator(cin, cout);
              cout << endl << real << ' ' << op << b << " = " << calc(real, op, b) << endl;
              break;
            }
          }
        }

        Тут для выполнения операции op с аргументами a, b или real используется функция calc. Эта функция должна уметь обрабатывать следующие типы входных параметров:

        ComplexNumber calc(const ComplexNumber& a, char op, const ComplexNumber&b);
        ComplexNumber calc(const ComplexNumber& a, char op, const double&b);
        ComplexNumber calc(const double& a, char op, const ComplexNumber&b);

        Добиться этого можно реализовав все три приведенные тут перегрузки, однако такая реализация будет содержать много дублирующегося кода, что не хорошо — более правильное решение — использование механизма шаблонов С++:

        template <typename T1, typename T2>
        ComplexNumber calc(const T1& a, char op, const T2&b) {
          switch (op) {
          case '+':
            return a+b;
          case '-':
            return a-b;
          case '/':
            return a/b;
          case '*':
            return a*b;
          default:
            throw  std::range_error("wrong operator symbol");
          }
        }

        Полученная программа исправно работает, что подтверждается снимками экрана, приведенными на рисунках 1-4. На рсиунке 2 значение B равно нулю, так как объект B создан конструктором по умолчанию.

        Рисунок 1 — Вывод начального меню:

        Рисунок 2 — Ввод числа А, вычисление 12+34i + 0:

        Рисунок 3 — Ввод числа B, вычисление (12+34i) * (99+99i):

        Рисунок 4 — Вычисление 88 / (99+99i):

        Использованием чисел в качестве элементов вектора

        Создадим новую функцию main, на этот раз объявим в ней вектор комплексных чисел. Функция будет выводить меню, позволяющее добавить элемент в конец вектора и отсортировтаь его по заданному параметру, после каждого действия содержимое вектора будем выводить на экран:

        int main() {
          vector<ComplexNumber> numbers;
          while (true) {
            int menuPoint = 0;
            char op;
            cout << "enter: " << endl
                 << "\t0 - exit;" << endl
                 << "\t1 - add number;" << endl
                 << "\t2 - sort by less module;" << endl
                 << "\t3 - sort by greater module;" << endl
                 << "\t4 - sort by less real part;" << endl
                 << "\t5 - sort by greater real part;" << endl
                 << "\t6 - sort by less imaginary part;" << endl
                 << "\t7 - sort by greater imaginary part;" << endl
                 << ": ";
            cin >> menuPoint;
            if (menuPoint == 0)
              break;
            switch (menuPoint) {
            case 1:
              numbers.push_back(readComplex(cin, cout));
              break;
            case 2:
              sort(numbers.begin(), numbers.end(), lessByModule);
              break;
            case 3:
              sort(numbers.begin(), numbers.end(), greaterByModule);
              break;
            case 4:
              sort(numbers.begin(), numbers.end(), lessByReal);
              break;
            case 5:
              sort(numbers.begin(), numbers.end(), greaterByReal);
              break;
            case 6:
              sort(numbers.begin(), numbers.end(), lessByImaginary);
              break;
            case 7:
              sort(numbers.begin(), numbers.end(), greaterByImaginary);
              break;
            }
            cout << endl << "numbers: " << endl;
            for (auto& number : numbers) {
              cout << number << endl;
            }
            cout << endl;
          }
        }

        Видно, что функция использует стандартный алгоритм std::sort. Функция sort принимает в качестве первых двух параметров — диапазон сортируемых элементов, задаваемый итераторами. А третьим (не обязательным) параметром — функцию сравнения (компаратор). Третий параметр можно не задавать если нужно упорядосить объекты по убыванию и для них перегружен:

        bool operator<(const Type& const Type&);

        В нашем случае сравнение нужно выполнять по разным полям, поэтому перегружать оператор нет смысла. Все 6 функций сравнения можно реализовать так:

        bool lessByModule(const ComplexNumber& lhs, const ComplexNumber& rhs) {
          return lhs.module() < rhs.module();
        }
        
        bool lessByReal(const ComplexNumber& lhs, const ComplexNumber& rhs) {
          return lhs.real() < rhs.real();
        }
        
        bool lessByImaginary(const ComplexNumber& lhs, const ComplexNumber& rhs) {
          return lhs.imaginary() < rhs.imaginary();
        }
        
        bool greaterByModule(const ComplexNumber& lhs, const ComplexNumber& rhs) {
          return lhs.module() > rhs.module();
        }
        
        bool greaterByReal(const ComplexNumber& lhs, const ComplexNumber& rhs) {
          return lhs.real() > rhs.real();
        }
        
        bool greaterByImaginary(const ComplexNumber& lhs, const ComplexNumber& rhs) {
          return lhs.imaginary() > rhs.imaginary();
        }

        Для сравнения чисел по модулю было нужно вычислять модуль комплексного числа, для этого в класс был добавлен метод module:

        double ComplexNumber::module() const {
          return sqrt(real()*real() + imaginary()*imaginary());
        }

        Запустим программу, добавим в нее числа. На рисунке 5 приведено окно программы после добавления четырех чисел:

        Далее можно сортировать массив по различным критериям…

        Инструкция по сборке

        В ходе работы использолась среда разработки Qt Creator и компилятор gcc с поддержкой С++11, поэтому к исходному коду прриложены два файла проекта (для первой и второй частей работы соответственно). Для сборки проекта необходимо установить эти инструменты, открыть файл проекта (.pro-файл) и выполнить сборку в IDE (нажатием F5). Однако, программа должна без проблем собираться в других IDE — например Mycrosoft Visual Studio.

        Литература по теме

        1. Учебник: объектно-ориентированное программирование. Раздел 1.3 [Электронный ресурс]. – режим доступа: https://pro-prof.com/archives/5355. Дата обращения: 15.01.2020.
        2. Спрвавка по std::sort [Электронный ресурс]. – режим доступа: http://www.cplusplus.com/reference/algorithm/sort/ Дата обращения: 15.01.2020.

Просмотр 0 веток ответов
  • Для ответа в этой теме необходимо авторизоваться.
×