Привет, читатель! Пошаманим на с++?
Сегодня поговорим о небольшой олимпиадной задачке с информатики. Суть довольно проста: написать класс, который может работать с дробями, т.е. нужно реализовать все 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(); }
Удачи в проектах.