Мир состоит из объектов и сам — объект. Исследуя Мир, принято объекты классифицировать по каким-либо признакам. Выделяют классы животных и растений, планет и звезд, атомов и молекул, машин и механизмов, и многие, многие другие. Эта идея положена в основу объектно-ориентированного программирования.
Объектно-ориентированное программирование(ООП) подразумевает описание информационной модели взаимодействия объектов, которые могут содержать данные и алгоритмы. Данные представляются как поля (свойства, атрибуты), а алгоритмы — как процедуры или функции (методы).
Многие из используемых языков программирования (C++, Java, Python, Ruby и т. д.) в большей или меньшей степени поддерживают объектно-ориентированное программирование. Наиболее полно концепция ООП представлена в языке C++. Поэтому на нем написаны все примеры, приведенные ниже.
Классы
Любой объект принадлежит определенному классу. С точки зрения языка C++ класс — это тип, объект — это переменная.
Описать класс на языке C++ можно так:
class ClassName
{
public:
…
protected:
…
private:
…
};
Например, класс обыкновенных дробей может быть описан так:
class CFract
{
public:
CFract(){};
CFract(int, int);
CFract(const CFract&);
~CFract(){};
CFract& operator=(const CFract&);
CFract& operator=(std::string);
CFract operator*(const CFract&);
CFract operator/(const CFract&);
CFract operator+(const CFract&);
CFract operator-(const CFract&);
std::string GetStr();
private:
int num{0};
int den{1};
protected:
int SetDen(int);
void Swap(int*, int*);
int Nod(int, int);
void Reduce();
};
В секции public: объявляются свойства и методы доступные в самом классе, его потомках. Кроме того, можно обратиться к ним через объект класса, используя точечную запись: имяОбъекта.свойтво/метод. Объявление конструкторов, деструктора, переопределение операций должно располагаться в этой секции.В приведенном примере определены:
три конструктора:
CFract(){}; - конструктор без параметров;
CFract(int, int); - конструктор с двумя параметрами: целые числа;
CFract(const CFract&); - копирующий конструктор.
две операции присваивания(перегрузка операций - тема отдельной статьи):
CFract& operator=(const CFract&);
CFract& operator=(std::string);
деструктор:
~CFract(){};
методы-арифметические операции
CFract operator*(const CFract&);
CFract operator/(const CFract&);
CFract operator+(const CFract&);
CFract operator-(const CFract&);
Обратите внимание, что при объявлении этих методов указаны только типы параметров. Имена понадобятся в определении методов.
Секция private: содержит свойства и методы доступные только в определении методов этого класса. В этой секции объявлены два поля: num(числитель) и den(знаменатель)
Секция protected: содержит свойства и методы доступные в классе и его потомках. В примере здесь объявлены вспомогательные функции.
Определение методов возможна внутри описания класса или после. Первый вариант неудобен для дальнейшей разработки и сопровождения класса. Поэтому будем определять методы после определения всего класса.
Конструктор без параметров определен внутри класса. там не предусмотрено ни каких действий.
Конструктор с параметрами:
CFract::CFract(int sourceNum, int sourceDen)
{
num = sourceNum;
den = sourceDen;
}
Может случится, что будет передан знаменатель меньше или равный нулю. Эту неприятность нужно обработать. В случае если передано отрицательное число, то нужно поменять знак и у числителя и у знаменателя, если передан 0, то вывести сообщение об ошибке. Для этого в protected объявим метод int SetDen(int).
int
CFract::SetDen(int sourceDen)
{
if(sourceDen > 0)
return den = sourceDen;
else{
if(sourceDen == 0){
std::cout<<"Недопустимый знаменатель."<<std::endl;
exit(1);
}
else{
num = -num;
return den = -sourceDen;
}
}
}
Тогда конструктор с параметрами примет вид:
CFract::CFract(int sourceNum, int sourceDen)
{
num = sourceNum;
SetDen(sourceDen);
}
Кроме метода проверки знаменателя нужен метод сокращения дроби Reduce. Добавим его объявление в секцию protected: , а определение выглядит так:
void
CFract::Reduce()
{
int tmp = Nod(num, den);
num = num / tmp;
SetDen(den / tmp);
}
Для сокращения дроби нужно найти наибольший общий делитель числителя и знаменателя. Добавим метод Nod в секцию protected, а ее определение выглядит так:
int
CFract::Nod(int sa, int sb)
{
int n, d;
d = sb;
n = (sa<0)? -sa : sa;
if(n < d) Swap(&n, &d);
while(d){
n = n - d;
if(n < d) Swap(&n, &d);
}
return n;
}
Еще один метод, который необходим в дальнейшем. Он также используется в методе Nod - обмен значениями переменных Swap. Добавим его объявление в секцию protected и определим:
void
CFract::Swap(int *a, int *b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
Следующий важный метод - операция присваивания:
CFract&
CFract::operator=(const CFract& source)
{
if(this == &source) return *this;
num = source.num;
SetDen(source.den);
return *this;
}
Ключевое слово operator указывает на то, что это не просто метод, а операция, которую в дальнейшем можно использовать в выражениях. Например:
...
CFract a(1,2);
CFract b;
b = a;
...
Хорошо бы перегрузить операцию присваивания в классе CFract, чтобы иметь возможность читать дробь из строки, то есть чтобы была допустима запись:
...
CFract a = "2/3"
...
Примерная реализация этого метода (не совсем очевидна; пожалуй это тема отдельной статьи):
CFract&
CFract::operator=(std::string source)
{
int a, b; //числитель, знаменатель, счетчик
std::string tmp; //временная переменная для числа
bool flA; //флаг введен или нет числитель
tmp = "";
b = 1;
flA = false;
for(auto s: source){
if((s >= '0') && (s <= '9')) tmp += s;
if(s == '/') {
//прехват исключения "неверный формат числа"
try{
a = std::stoi(tmp);
}
catch(...){
std::cout << "Неверный формат числа.";
}
tmp = "";
flA = true;
}
}
try{
b = ((tmp != "") && (flA)) ? std::stoi(tmp) : 1;
a = (!flA) ? std::stoi(tmp): 0;
}
catch(...){
std::cout << "Неверный формат числа.";
}
num = a;
SetDen(b);
return *this;
}
Определение арифметических операций
Наиболее сложная из всех - операция сложения. Нужно найти наименьший общий знаменатель, дополнительные множители для дробей. Затем сложить произведения числителей на соответствующие дополнительные множители. Примерное определение операции сложения:
CFract
CFract::operator+(const CFract& source)
{
CFract tmp;
int tmpNod, tmpDen;
tmpNod = Nod(den, source.den);
tmpDen = (den * source.den) / tmpNod;
tmp.num = num * tmpDen / this -> den +
source.num * tmpDen / source.den;
tmp.SetDen(tmpDen);
tmp.Reduce();
return tmp;
}
Остальные операции определить гораздо проще. В приложении приведены определения этих операций.
И последнее в этой заметке: объявление и определение принято писать в различных файлах: fract1.h - объявление класса, fract1.cpp - определение методов. Вот эти файлы:
файл fract1.h
#ifndef _FRACT1_H
#define _FRACT1_H
#include <string>
class CFract
{
public:
CFract(){};
CFract(int, int);
CFract(const CFract& source);
~CFract(){};
CFract& operator=(const CFract&);
CFract& operator=(std::string);
CFract operator*(const CFract&);
CFract operator/(const CFract&);
CFract operator+(const CFract&);
CFract operator-(const CFract&);
std::string GetStr();
private:
int num{0};
int den{1};
protected:
int SetDen(int sourceDen);
void Swap(int *a, int *b);
int Nod(int sa, int sb);
void Reduce();
};
#endif
файл fract1.cpp
#include <iostream>
#include <sstream>
#include "fract1.h"
CFract::CFract(int sourceNum, int sourceDen)
{
num = sourceNum;
SetDen(sourceDen);
}
CFract::CFract(const CFract& source)
{
num = source.num;
SetDen(source.den);
}
CFract&
CFract::operator=(const CFract& source)
{
if(this == &source) return *this;
num = source.num;
SetDen(source.den);
return *this;
}
CFract&
CFract::operator=(std::string source)
{
int a, b; //числитель, знаменатель, счетчик
std::string tmp; //временная переменная для числа
bool flA; //флаг введен или нет числитель
tmp = "";
b = 1;
flA = false;
for(auto s: source){
if((s >= '0') && (s <= '9')) tmp += s;
if(s == '/') {
//возможна исключительная ситуация "неворный формат числа"
try{
a = std::stoi(tmp);
}
catch(...){
std::cout << "Неверный формат числа.";
}
tmp = "";
flA = true;
}
}
try{
b = ((tmp != "") && (flA)) ? std::stoi(tmp) : 1;
a = (!flA) ? std::stoi(tmp): 0;
}
catch(...){
std::cout << "Неверный формат числа.";
}
num = a;
SetDen(b);
return *this;
}
CFract
CFract::operator*(const CFract& source)
{
CFract tmp;
tmp.num = num * source.num;
tmp.SetDen(den * source.den);
tmp.Reduce();
return tmp;
}
CFract
CFract::operator/(const CFract& source)
{
CFract tmp;
tmp.num = num * source.den;
tmp.SetDen(den * source.num);
tmp.Reduce();
return tmp;
}
CFract
CFract::operator+(const CFract& source)
{
CFract tmp;
int tmpNod, tmpDen;
tmpNod = Nod(den, source.den);
tmpDen = (den * source.den) / tmpNod;
tmp.num = num * tmpDen / this -> den +
source.num * tmpDen / source.den;
tmp.SetDen(tmpDen);
tmp.Reduce();
return tmp;
}
CFract
CFract::operator-(const CFract& source)
{
CFract tmp;
int tmpNod, tmpDen;
tmpNod = Nod(den, source.den);
tmpDen = (den * source.den) / tmpNod;
tmp.num = num * tmpDen / den -
source.num * tmpDen / source.den;
tmp.SetDen(tmpDen);
tmp.Reduce();
return tmp;
}
int
CFract::SetDen(int sourceDen)
{
if(sourceDen > 0)
return den = sourceDen;
else{
if(sourceDen == 0){
std::cout<<"Недопустимый знаменатель."<<std::endl;
exit(1);
}
else{
num = -num;
return den = -sourceDen;
}
}
}
std::string
CFract::GetStr()
{
std::ostringstream s;
s <<"{" << num << "/" << den<<"}";
return s.str();
}
void
CFract::Swap(int *a, int *b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
int
CFract::Nod(int sa, int sb)
{
int n, d;
d = sb;
n = (sa<0)? -sa : sa;
if(n < d) Swap(&n, &d);
while(d){
n = n - d;
if(n < d) Swap(&n, &d);
}
return n;
}
void
CFract::Reduce()
{
int tmp = Nod(num, den);
num = num / tmp;
SetDen(den / tmp);
}
//перегрузка метода вывода в поток "<<"
std::ostream& operator<<(std::ostream& out, CFract source)
{
out << source.GetStr();
return out;
}
int main()
{
CFract a(-1, 2), b(-3, 4);
CFract c;
c = a + b;
std::cout<<c<<std::endl;
c = a - b;
std::cout<<c<<std::endl;
c = a * b;
std::cout<<c<<std::endl;
c = a / b;
std::cout<<c<<std::endl;
c = "2/3";
std::cout<<c<<std::endl;
return 0;
}
Функция main нужна только для тестирования. Вообще ее следует определять в отдельном файле.