Класс работы с дробями

Привет, читатель! Пошаманим на с++?

Сегодня поговорим о небольшой олимпиадной задачке с информатики. Суть довольно проста: написать класс, который может работать с дробями, т.е. нужно реализовать все 4 базовые действия над ними.

Для этого создадим 1 большой класс Fraction. Вся работа сводится до: парсим дробь из строки, сокращаем по возможности и переопределяем операторы + — / *.

Итак, сегодня мы с вами научимся переопределять операторы, писать классы, парсить строки, думать, считать 🙂 Для начала, напишем вспомогательные функции: поиск наибольшего общего делителя и кратного (НОД/НОК). Реализовать это достаточно просто за алгоритмом Евклида:

НОД(a, b) = НОД(a, a mod b);
НОК(a, b) = (a * b) / НОД(a, b);
// Наибольший общий делитель
// (англ.) greatest common divisor
int gcd(int a, int b) {
    while (b > 0) {
        int c = a % b;
        a = b;
        b = c;
    }
    return a;
}

// Наименьшее общее кратное
// (англ.) least common multiple
int lcm(int a, int b) {
    return gcd(a, b) * a * b;
}

Это задача для начинающего программиста, поэтому останавливаться не будем. Следующий пункт — создадим скелет нашего класса.

// Файл fraction.h

using std::string;

class Fraction {
private:
    // Числитель
    int _numerator;
    // Знаменатель
    int _denominator;

    // Функция нужна для сокращения дроби
    void reduce();
public:
    // Конструктор принимает значения числителя и знаменателя
    Fraction(int numerator, int denominator);
    Fraction(cons string &string);

    // Возвращаем дробь в виде строки
    string toString();

    // Геттеры
    int getNumerator();
    int getDenominator();
    
    // Перегружаем операторы основных операций
    Fraction& operator+(const Fraction &fraction);
    Fraction& operator-(const Fraction &fraction);
    Fraction& operator*(const Fraction &fraction);
    Fraction& operator/(const Fraction &fraction);
}

Создаем два поля — для числителя и знаменателя. Не забываем про инкапсуляцию — делаем эти поля приватными.

Так же создаем два конструктора. В первом числитель и знаменатель явно заданы. А во втором — будем получать их из строки.

Ну и напоследок — переопределение операторов сложения, вычитания, умножения, деления. Это самая интересная часть, которую мы будем реализовывать.

Перейдём к самому простому — первый конструктор:

#include "fraction.h"

Fraction::Fraction(int numerator = 1; int denominator = 1)
    : _numerator(numerator), _denominator(denominator) {
    // Выбрасываем исключение, если знаменатель равен нулю
    assert(denominator == 0);
}

Здесь нам достаточно проверить то, что знаменатель не равен нулю.

Во втором конструкторе нам нужно проверить наличие знака ‘/’. Если он есть — то левая часть от него — числитель, а правая — знаменатель. А если же знака нет, то вся строка — числитель, а знаменатель равен единице.

Fraction::Fraction(const string &string) {
    // Выбрасываем исключение, если не задана строка
    assert(string == "")
    // Ищем знак '/'
    int pos = string.find("/");
    
    // Если символ не найден - то вся строка является числом 
    if (pos == string::npos) {
        _numerator = stoi(string);
        _denominator = 1;
    }else {
        // Числитель - левая часть
        _numerator   = stoi(string.substr(0, pos));
        // Знаменатель - правая часть
        _denominator = stoi(string.substr(pos, string.length));

        // Знаменатель не должен быть равен нулю
        assert(_denominator == 0);
    }
}

Переходим к сокращению дроби:

void Fraction::reduce() {
    // Находим НОД
    int gcd = gcd(abs(_numerator), _denominator);
    if (gcd != 1) {
        _numerator = _numerator / gcd;
        _denominator = _denominator / gcd;
    }
}

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

И ещё один простой метод — форматированный вывод:

string Fraction::toString() {
    string fraction = "";
    if (_numerator == 0) {
        fraction.append("0");
        return fraction;
    }

    fraction.append(_numerator);
    if (_denominator != 1) {
        fraction.append("/");
        fraction.append(_denominator);
    }
    return fraction;
}

Здесь всё просто. Если числитель равен нулю — возвращаем ноль. Если знаменатель равен единице — возвращаем только числитель. Иначе возвращаем числитель/знаменатель

А теперь самое вкусное — реализация действий между дробями. Начнём с умножения:

Fraction& Fraction::operator*(const Fraction &fraction) {
    _numerator = _numerator * fraction.getNumerator();
    _denominator = _denominator * fraction.getDenominator();   
    reduce();
    return *this;
}

Всё просто до невозможности, перемножаем числители и знаменатели, а потом сокращаем дробь и возвращаем текущий объект.

Деление:

Fraction& Fraction::operator/(const Fraction &fraction) {
    _numerator = _numerator * fraction.getDenominator();
    _denominator = _denominator * fraction.getNumerator();
    reduce();
    return *this;
}

Не забываем, что деление дробей это умножение первой на оборотную вторую.

Вычитание:

// Ищем НОК для знаменателей. Умножаем оба числителя на него
Fraction& Fraction::operator-(const Fraction &fraction) {
    int relNumerator = _numerator * fraction.getDenominator();
    _numerator = _numerator * fraction.getDenominator() - _denominator * fraction.getNumerator();
    _denominator = gcd(_denominator, fraction.getDenominator());
    reduce();
    return *this;
}

Сложение:

Fraction& Fraction::operator+(const Fraction &fraction) {
    int unionDenominator = lcm(_denominator, fraction.getDenominator());
    int relNumerator = _numerator * unionDenominator;
    int mulNumerator = fraction.numerator * unionDenominator;
    _numerator = relNumerator * mulNumerator;
    _denominator = unionDenominator;
    reduce();
    return *this;
}

Вот и всё. Напишем ещё небольшой пример использование нашего класса: int main()

void main() {
    Fraction first("-2/3");
    Fraction second("4/10");
    
    Fraction result = a * b;
    cout << result.toString();
}

Удачи в проектах.

Этот пост не был отмечен тегами

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *