Разработка класса комплексных чисел

Необходимо реализовать шаблон complex для работы с комплексными числами. Комплексное число имеет вид \(z = a + bi\), где \(a\) — действительная часть числа, \(b\) — мнимая часть числа. При операциях с комплексными числами предполагается, что \(i^2=-1\).

Тем самым комплексное число будет структурой, содержащей два поля: действительную и мнимую часть. Сам класс complex должен быть шаблоном.

template<typename T> complex

T может быть одним из действительных типов данных: float, double, long double. Кроме того, в задачах на конструкторы и ввод-вывод допускается использование целочисленых типов в качестве базового типа, но в большинстве задач будут рассматриваться только действительные типы.

Инкапсуляция

Вам необходимо будет реализовать операции сложения, вычитания, умножения, деление. Все эти операции могут работать как с комплексными, так и с действительными числами. Между тем при вычислениях с комплексными числами не бывает такой операции, как изменение отдельно действительной или мнимой части числа, все вычисления в программе должны работать с комплексным числом, как с единым целым (не должно быть возможности значению действительной части комплексного числа присвоить новое значение). Поэтому поля структуры, в которых хранятся значения действительной и мнимой части должны быть недоступны для прямого изменения. Это делается при помощи модификаторов доступа к полям и методам.

Модификаторы доступа бывают следующими:

Модификаторы доступа пишутся внутри определения класса, после модификатора доступа ставится двоеточие. Действие модификатора доступа распространяется на все поля и методы до конца объявления класса или до следующего модификатора. Пример:

class complex
{
 private:
    double m_re, m_im;

 public:
    complex();
    complex(double x);
    complex(double x, double y);
    double re() {return m_re;}
    double im() {return m_im;}
};

Если не указывать модификатор доступа, то для struct по умолчанию доступ public, а для class по умолчанию private.

Между тем бывает полезно получить доступ к private-полям не из методов класса. В этом случае пишутся специальные функции-обёртки, возвращающие значение закрытого поля, как методы re() и im() в примере выше. Такая технология называется инкапсуляцией. Можно также реализовать методы для изменения private-полей класса, если в этом есть необходимость.

По правилам оформления программ в тестирующей системе перед модификаторами доступа ставится отступ в один пробел.

Пользовательские литералы

Иногда в программах необходимо использовать значение числовой константы определенного типа. Например, если требуется производить вычисления c типом long long, то может возникнуть желание использовать целочисленные константы (например, 0, 1, 2,...) данного типа.

Это можно сделать при помощи явного преобразования типов (long long)1, но есть и более компактная запись с использованием суффиксов-литералов, записываемых сразу после числа, например, 1LL.

Для целых типов данных возможны следующие суффиксы:

Их можно комбинировать, например, 1ULL — это число типа unsigned long long.

Начиная со стандарта C++11 пользователи могут создавать собственные литералы. Например, тригонометрические функции математической библиотеки работают с углами в радианах. Но, возможно, захочется задавать углы в градусах, например, так:

long double alpha = 45.0_deg;
cout << sin(alpha) << endl;

В данном примере _deg — это определённый пользователем литерал, который применяется к предшествующей константе и может быть использован для перевода значения константы из градусов в радианы. Для этого нужно предварительно определить литерал следующим образом:

long double operator"" _deg(long double deg)
{
    return deg * M_PI / 180;
}

Параметр этой функции — константа, записанная перед литералом. При использовании в коде константы с литералом вместо значения константы будет использоваться значение, которое возвращает данная функция. Название функции является идентификатором литерала и должно начинаться с символа подчеркивания.

Пользователь может объявлять литералы, которые можно использовать для суффиксов типа unsigned long long int и long long double, а также для символьных и строковых констант.

Статические методы класса

У класса могут быть статические методы (а также поля) — методы, не привязанные ни к одному экземпляру класса, общие для всех экземпляров класса. Можно рассматривать статические методы, как функции, которые имеют какое-то отношение к классу, но мы не хотим вызывать их для конкретного экземпляра класса.

Пример статического поля в библиотеке STL: std::string::npos.

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

static complex<T> polar(T r, T angle);

Статический метод вызывается так: complex<double>::polar(r, phi).

Упражнения

На проверку в этих задачах сдаётся реализация шаблона структуры complex, если не оговорено иное.

Для отладки задач вам может помочь интерпретатор python или WolframAlpha.

A: Конструкторы и инкапсуляция

Реализуйте шаблон структуры complex и конструкторы со следующими параметрами:

Структура должна содержать инкапсулирующие методы re() и im(), возвращающие действительную и мнимую части числа.

B: Пользовательские литералы

Хочется писать в программах:

complex<double> z = 1 + 2_i;

Для этого сначала определим литерал _i, возвращающий значение типа complex<double>. Для этого необходимо реализовать два оператора: complex<double> operator"" _i(long double) и complex<double> operator"" _i(unsigned long long).

Правда, ещё нужно будет определить оператор “+”, но это будет сделано позже.

Помимо этого определите литералы _if, возвращающий значение типа complex<float> и _il, возвращающий значение типа complex<long double>.

C: Вывод

Реализуйте оператор вывода

template<typename T>
ostream & operator<<(ostream&, complex<T>);

Правила вывода такие:

D: Сопряжённое

Реализуйте метод conj():

complex<T> conj() const;

возвращающий сопряжённое к данному числу. Сопряжённое к числу \(z=a+bi\) это число \(\overline{z}=a-bi\).

Примеры использования метода:

complex<double> w = z.conj();
cout << z.conj() << endl;

E: Сложение

Реализуйте операцию “+” так, чтобы можно было складывать два комплексных числа, комплексное число и действительное число, действительное число и комплексное число. Тип действительного числа (float, double, long double) совпадает с базовым типом комплексного числа.

F: Вычитание

Аналогично определите операцию “-” для вычитания действительных и комплексных чисел.

G: Умножение

Аналогично определите операцию “*” для умножения действительных и комплексных чисел.

H: Деление

Аналогично определите операцию “/” для деления действительных и комплексных чисел.

I: Унарный минус и унарный плюс

Определите унарные операторы “+” и “-”. Для этого нужно определить функции (или методы) operator+ и operator-, работающие только с одним операндом.

J: Модуль и аргумент

Реализуйте методы abs() и arg(), возвращающие модуль и аргумент (полярный угол) комплексного числа. Метод arg() возвращает значение из интервала \((-\pi, \pi]\). Для нулевого комплексного числа метод arg() должен возвращать 0.

К: Число по тригонометрической форме

Необходимо уметь создавать число по тригонометрической форме. Для этого реализуйте статический метод класса.

static complex<T> polar(T r, T angle);

L: Квадратный корень

Необходимо реализовать статический метод класса sqrt, возвращающий квадратный корень из комплексного числа. Комплексное число (кроме нуля) имеет два различных комплексных корня, метод может вернуть любое из них.

Начиная с какого-то момента вы столкнётесь с тем, что определёнными вами функции имеют одинаковые названия со стандартными функциями из библиотеки cmath, что приводит компилятор в затрудение. Чтобы указать компилятору, что необходимо использовать функцию стандартной библиотеки, её нужно вызывать так: std::sqrt.

M: Квадратное уравнение

Дано квадратное уравнение с комплексными коэффициентами: \(az^2+bz+c=0\), где \(a\ne 0\). Найдите его корни.

Решение оформите в виде метода static void complex<T>::solve_quadratic_equation(complex<T> a, complex<T> b, complex<T> c, complex<T> & z1, complex<T> & z2);

Два найденных корня необходимо записать в переменные z1 и z2 в любом порядке.

N: Проверка на равенство и неравенство

Реализуйте операторы == и !=, возвращающие значение типа bool. Сравнение действительных и мнимых частей осуществляется на точное равенство. Необходимо реализовать сравнения двух комплексных чисел, комплексного и действительного числа, действительного и комплексного числа.

O: Сравнение и сортировка

Хоть это и неправильно, но давайте определим для комплексных чисел оператор сравнения <. Он должен сравнивать числа по модулю, при равном модуле — по аргументу, при этом аргумент должен браться на интервале \([0; 2\pi)\), то есть \(1 \lt i \lt -1 \lt -i\). При помощи этого оператора будет выполняться сортировка комплексных чисел функцией sort. Сравнивать можно между собой только два комплексных числа.

P: Возведение в целочисленную степень

Научитесь возводить комплексное число в целочисленную степень n при помощи алгоритма быстрого возведения в степень.

Для этого реализуйте статический метод класса.

static complex<T> pow(complex<T> z, int n);

Q: Считывание комплексного числа

Необходимо реализовать считывание комплексных чисел, определив

template<typename T>
istream & operator>>(istream&, complex<T> &);

Число подаётся на вход в том виде, в котором оно записано в примерах вывода. Если число записано со скобками, то внутри скобок допускаются пробелы, но знак “+” или “-” всегда пишется слитно с последующим символом.

Немного более формально. Запись комплексного числа на входе может иметь одну из следующих форм:

где RE := x, IM := xi | i | +i | -i, где x — действительное число (запись действительного числа может начинаться со знаков “+”, “-”, “.” или цифры). Внутри скобок и между RE и IM могут идти пробелы. В записи (RE IM) литерал IM обязательно начинается со знака “+” или “-”.

Во всех тестах запись комплексного числа будет корректной, то есть удовлетворяющей данному определению. Тестирование будет производиться при помощи цикла:

    while (cin >> z)
        cout << z << endl;

Полезные идеи, которые могут пригодится при решении этой задачи.

У объекта istream есть метод unget(), который возвращает назад в поток последний считанный символ.

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

#include<sstream>
...
string str;
...
istringstream is(str);
...
is >> a > > b >> c;

Успешность считывания можно проверять при помощи преобразования к типу bool:

if (is >> a > > b >> c;)

Для повторного считывания из объекта istringstream с самого начала нужно вызвать два метода:

is.clear();
is.seekg(0);

R: Возведение в действительную степень

Научитесь возводить комплексное число в действительную степень степень x при помощи формулы Муавра.

Для этого реализуйте статический метод класса.

static complex<T> pow(complex<T> z, T x);

S: Экспонента

Для данного комплексного \(z\) вычислите \(e^z\). Вам поможет формула Эйлера.

Для этого реализуйте статический метод класса.

static complex<T> exp(complex<T> z);

T: Синус

Теперь посчитайте синус.

U: Косинус

И косинус.

V: Логарифм

На очереди — натуральный логарифм. Он называется log.

Комплексный логарифм — многозначная функция (для каждого числа определено счётное число возможных значений логарифма). Функция должна возвращать главное значение логарифма, мнимая часть которого принадлежит интервалу \((-\pi; \pi]\).

W: Возведение в комплексную степень

Комплексная степень сводится к логарифму, поэтому тоже является многозначной функций. Необходимо использовать главное значение логарифма.

Z: Кубическое уравнение

Дано кубическое уравнение с комплексными коэффициентами: \(az^3+bz^2+cz+d=0\), где \(a\ne 0\). Найдите его корни.

Решение оформите в виде метода static void complex<T>::solve_cubic_equation(complex<T> a, complex<T> b, complex<T> c, complex<T> d, complex<T> & z1, complex<T> & z2, complex<T> & z3);

Три найденных корня необходимо записать в переменные z1, z2, z3.

Вам поможет статья в википедии формула Кардано и WolframAlpha, которая умеет решать всё, для отладки.

Для вычисления кубического корня в библиотеке cmath есть функция cbrt.