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