---
title: ООП, операторы, наследование
tags: cpp, mipt, 3sem, teaching, OOP
---
# Третье занятие
#### 2022-09-17
## Основы ООП. Наследование
### Перегрузка операторов
#### Оператор копирующего присваивания
Каноничный оператор копирующего присваивания:
```*.cpp=
class MyClass {
private:
int *m_array;
unsigned size;
public: // must be public, otherwise can't use it
MyClass& operator=(const MyClass &other) {
// Guard self assignment
if (this == &other) {
return *this;
}
delete[] m_array; // release resource in *this
m_array = nullptr;
size = 0; // preserve invariants in case next line throws
mArray = new int[other.size]; // allocate resource in *this
size = other.size;
std::copy(other.mArray, other.mArray + other.size, mArray);
return *this;
}
};
```
Данный оператор должен возвращать именно ссылку, так как возможна запись вида ```a = b = c```
#### Оператор сложения с присваиванием
Для классов можно [перегрузить стандартные операторы](https://en.cppreference.com/w/cpp/language/operators) из следующего списка: `+`, `-`, `*`, `/`, `%`, `=`, `<`, `>`, `+=`, `-=`, `*=`, `/=`, ... etc.
Правильно начинать с реализации оператора с присваиванием (так как он может быть использован в операторе без присваивания). Рассмотрим следующую реализацию:
```cpp=
class Fraction {
private:
int denominator = 1, numerator = 0;
public:
Fraction(int numerator, int denominator) : numerator(numerator),
denominator(denominator) { }
Fraction& operator+=(const Fraction& other) {
if ((this->denominator) == (other.denominator)) {
this->numerator += other->numerator;
} else {
this->numerator = (this->numerator) * (other.denominator)
+ (this->denominator) * (other.numerator);
this->denominator = (this->denominator) * (other.denominator);
}
return *this;
}
};
```
Возвращаемое значение должно быть ссылкой на тот же объект для сохранения смысла оператора `+=`. Передаваемое значение никак не модифицируется при исполнении оператора `+=`, тогда и передается оно по константной ссылке.
#### Оператор сложения
Для реализации сложения `+` воспользуемся оператором сложения с присваиванием
```cpp=
class Fraction {
private:
int numerator = 0;
int denominator = 1;
public:
Fraction(int numerator, int denominator) : numerator(numerator),
denominator(denominator) { }
Fraction& operator+=(const Fraction& other) {
if ((this->denominator) == (other.denominator)) {
this->numerator += other.numerator;
} else {
this->numerator = (this->numerator) * (other.denominator)
+ (this->denominator) * (other.numerator);
this->denominator = (this->denominator) * (other.denominator);
}
return *this;
}
Fraction operator+(const Fraction& other) const {
Fraction sum = *this;
sum += other;
return sum;
}
};
```
Возвращаемое значение -- копия экземпляра класса, а не ссылка на уже имеющийся экземплер класса. Метод константный -- он никак не модифицирует поля внутри экземпляра класса, лишь создает новый экзмепляр класса.
#### Префиксные и постфиксные операторы инкрементирования
```cpp=
class Fraction {
private:
int numerator = 0;
int denominator = 1;
public:
Fraction(int numerator, int denominator) : numerator(numerator),
denominator(denominator) { }
Fraction(int numerator) : Fraction(numerator, 1) { }
Fraction& operator++() {
return *this += 1;
}
Fraction operator++(int) {
Fraction res = *this;
++(*this);
return res;
}
};
```
#### Определение операторов вне класса
Объявление оператора внутри класса говорит, что оператор будет вызываться от экземпляра класса. Соответственно, будут работать операции типа `MyClass + MyClass`.
Допустим, что для дробей определены конструктор из одного `int` и оператор сложения внутри класса. Тогда рассмотрим следующий код
```cpp=
Fraction first(3, 4);
first + 5; // Неявное преобразование к следующей форме
// first.operator+(Fraction(5))
5 + fraction; // Error
```
В первом выражении происходит неявное преобразование `int->Fraction` и оператор сложения вызывается от левого объекта. Отсюда следует, что если оператор должен быть равноправен по своему действию к левому и правому аргументу, то он должен быть объявлен вне класса. Делается это следующим образом:
```cpp=
class Fraction {
...
};
Fraction operator+(const Fraction& lha, const Fraction& rha) {
Fraction sum = lha;
sum += rha;
return sum;
}
```
#### Оператор ввода/вывода
Для перегрузки операторов ```>>``` и ```<<```, которые принимают ```std::istream&``` левым аргументом называются операторами ввода/вывода. Так как эти операторы принимают определенные пользователем типы вторым аргументом, они должны быть реализованы вне пользовательских типов.
Синтаксис операторов ввода/вывода:
```cpp=
std::ostream& operator<<(std::ostream& os, const T& obj)
{
// write obj to stream
return os;
}
std::istream& operator>>(std::istream& is, T& obj)
{
// read obj from stream
if( /* T could not be constructed */ )
is.setstate(std::ios::failbit);
return is;
}
```
Так как у объекта может не быть функций, возвращающих значения полей, то такие операторы могут быть объявлены как дружественные.
#### Друзья
Объявление друга в теле класса даёт доступ объявленной функции или классу доступ к полям с модификаторами доступа ```private, protected```.
Пример функции друга:
```cpp=
class Y
{
int data; // private member
// the non-member function operator<< will have access to Y's private members
friend std::ostream& operator<<(std::ostream& out, const Y& o);
friend char* X::foo(int); // members of other classes can be friends too
friend X::X(char), X::~X(); // constructors and destructors can be friends
};
// friend declaration does not declare a member function
// this operator<< still needs to be defined, as a non-member
std::ostream& operator<<(std::ostream& out, const Y& y)
{
return out << y.data; // can access private member Y::data
}
```
### Оператор вызова функции
Иногда классы могут перегружать оператор вызова функции. В этом случае такие классы называются фукторами и могут быть вызваны как функции.
Пример перегрузки оператора вызова функции:
```*.cpp=
// An object of this type represents a linear function of one variable a * x + b.
struct Linear {
double a, b;
double operator()(double x) const {
return a * x + b;
}
};
int main() {
Linear f{2, 1}; // Represents function 2x + 1.
Linear g{-1, 0}; // Represents function -x.
// f and g are objects that can be used like a function.
double f_0 = f(0);
double f_1 = f(1);
double g_0 = g(0);
}
```
### Операторы сравнения
Для многих стандартных алгоритмов, таких как qsort(), map() и др. требуется оператор сравнения для возможности хранения в них пользовательских классов. В этом случае возможно перегрузить операторы сравнения.
Пример перегрузки оператора '<':
```*.cpp=
class Record {
std::string name;
unsigned int floor;
double weight;
public:
friend bool operator<(const Record& l, const Record& r) {
return l.name < r.name
}
};
```
Для написания всех операторов сравнения достаточно определить один оператор сравнения, остальные же могут быть реализованы через него без копирования кода.
```cpp=
bool operator< (const X& lhs, const X& rhs) { /* do actual comparison */ }
bool operator> (const X& lhs, const X& rhs) { return rhs < lhs; }
bool operator<=(const X& lhs, const X& rhs) { return !(lhs > rhs); }
bool operator>=(const X& lhs, const X& rhs) { return !(lhs < rhs); }
```
### Операторы обращения к массиву
Пользовательские классы, представляющие собой структуры данных с доступом как у массивов могут перегружать оператор обращения к массиву. Существует две версии данного оператора - константная и неконстантная.
```cpp=
struct T {
value_t& operator[](std::size_t idx) { return mVector[idx]; }
const value_t& operator[](std::size_t idx) const { return mVector[idx]; }
};
```
Стоит заметить, что этот оператор не должен проверять выход за границы массива.
Константная версия используется для получения значения, а неконстантная для записи в ячейку.
```*.cpp=
T[idx] = 123; // use of non-const version
value_T tmp = T[idx]; // use of const version
```
### Оператор приведения типа
[Ссылка на cppreference: `cast_operator`](https://en.cppreference.com/w/cpp/language/cast_operator)
## Наследование
Наслеедование -- механизм переиспользования и расширения функциональности имеющегося класса, от общего к специализированному.
Наследование в С++ от одного класса может быть трех типов: `private`, `public`, `protected`. Данные модификаторы позволяют указать возможность обращения к методам и полям класса-родителя вне класса-наследника.
```cpp=
class Base {
public:
int a = 0;
void f() {
std:: cout <<"Base\n";
}
};
class DerivedPublic: public Base { // наследование может public, private, protected
public:
int b = 1;
void g() {
std::cout << "DerivedPublic " << a << "\n";
}
};
class DerivedPrivate: private Base {
public:
int b = 1;
void g() {
std::cout << "DerivedPrivate " << a << "\n";
}
};
int main() {
DerivedPublic derived_public = DerivedPublic();
DerivedPrivate derived_private = DerivedPrivate();
derived_public.f(); // OK, наследование типа public
derived_public.g(); // OK, метод публичный
derived_private.f(); // NOT OK, наследование типа private
derived_private.g(); // OK, метод публичный
return 0;
}
```
Запрет на использование полей класса `Base` в `DerivedPrivate` следует из `DerivedPrivate`. Именно `DerivedPrivate` запрещает публичное использование методов и полей вне класса `DerivedPrivate`.
Стоит упомянуть, что функцию `int main` можно сделать `friend` (но так делать плохо) в классе `DerivedPrivate`. Тогда внутри `main` обращаение к полям и методам `Base` из `DerivedPrivate` будет разрешен.
Если функцию `int main` объявить `friend` в классе `Base`, то для `DerivedPrivate` ничего не изменится. **`friend` не наследуется!**
### Модификатор доступа protected
`private` позволяет предоставлять доступ к полям и методам внутри класса и друзьям (`friend`).
`public` разрешает доступ к полям и методам не только внутри класса, но и снаружи (друзья теряют смысл в таком случае).
А вот `private` особый тип доступа, который позволяет получить доступ к полям и методам внутри класса-наследника, но ограничивает доступ к таким полям и методам снаружи. Пример:
```cpp=
class Base {
protected:
int a = 0;
void f() {
std:: cout <<"Base\n";
}
};
class Derived: private Base {
public:
int b = 1;
void g() {
std::cout << "DerivedPublic " << a << "\n";
}
};
class SubDerived: public Derived {
public:
int c = 2;
void h() {
std::cout << "SubDerived " << a << "\n";
}
};
int main() {
Derived derived = Derived();
SubDerived sub_derived = SubDerived();
derived.g(); // OK,
derived.f(); // NOT OK, protected
sub_derived.g(); // OK
sub_derived.h(); // NOT OK, Compilation Error
return 0;
}
```
Примечание 1: по умолчанию классы наследуются приватно, а структуры -- публично. Второе главное отличие.
Примечание 2: все модификаторы определяются на уровне компиляции. Если на уровне теста программы видно, что доступа к методу или полю класса нет, то выдается ошибка. Программа не компилируетс.я
### Порядок инициализации полей
Перед выполнением конструктора следует инициализация полей класса. Если одно из полей класса является структурой, или же классом, то при инициализации полей основного класса следует первоначальная инициализация структуры/класса-поля. Пример
```cpp=
struct Atom {
int Z;
int A;
Atom() : Z(0), A(1) { }
};
class Material {
protected:
Atom base;
int n_atoms;
public:
Material() : base(Atom()), n_atoms(0) { }
};
class DerivedMaterial: public Material {
private:
double temperature;
public:
DerivedMaterial() : Material(), temperature(273.15) { }
};
```
### Порядок конструкторов/деструкторов при наследовании
При создании класса без наследования сначала создаются и инициализируются поля, затем вызывается сам конструктор класса. Это правило сохраняется и при наследовании: сначала создается родительская часть со своими полями, затем инициализируются поля дочернего класса, затем конструктор дочернего класса. Деструкторы соответственно вызываются в обратном порядке.
```*.cpp=
struct A {
A() {
std::cout << "A\n";
}
~A() {
std::cout << "~A\n";
}
};
struct B {
B() {
std::cout << "B\n";
}
~B() {
std::cout << "~B\n";
}
};
struct Mom {
A a;
Mom() {
std::cout << "Mom\n";
}
~Mom() {
std::cout << "~Mom\n";
}
Mom(int x) : x(x) {}
private:
int x = 0;
};
struct Son: Mom {
B b;
Son() {
std::cout << "Son\n";
}
~Son() {
std::cout << "~Son\n";
}
Son(int x) : Mom(x), x(x) {}
// since c++11
// allow to use base class constructors
using Mom::Mom;
private:
int x = 0;
};
int main() {
Son s; // print AMomBSon~Son~B~Mom~A
}
```
### Приведение типов при наследовании
```*.cpp=
struct Base {
int a = 0;
};
struct Derived: public Base {
int a = 1;
};
void f(Base& b) {
std::cout << b.a << '\n';
}
void ff(Base* b) {
std::cout << b->a << '\n';
}
void fff(Base b) {
std::cout << b.a << '\n';
}
int main() {
Derived b;
// implicit cast to base class reference
f(d); // OK // print 0
// implicit cast to base class pointer
ff(&d); // OK // print 0
// creates copy of base class
fff(d); // OK
}
```
Как классы хранятся в памяти и сколько памяти выделяется для их хранения?
```*.cpp=
struct GrandBase {
// What size is the instance of GrandBase class?
};
struct Base: public GrandBase {
int a = 0;
Base() = default;
Base(const Base& b) {
std::cout << "A";
}
};
struct Derived: public Base {
int a = 1;
Derived() = default;
};
void f(Base b) {
std::cout << b.a << '\n';
}
// Empty base optimization
// The size of empty class can't be zero, so it's size is actually 1
// In memory inherited classes are stored as followed
// [[ Base ][ Derived ]]
int main() {
// cast from heir to base
Derived d; // OK
Base& b = d; // OK
GrandBase gb; // OK
std::cout << sizeof(gb) << '\n'; // print 1
std::cout << sizeof(b) << '\n'; // print 4
std::cout << b.a << '\n'; // 0
std::cout << d.a << '\n'; // 1
std::cout << *(&b.a + 1) << '\n'; // 1
// cast from base to heir
//Derived dd = b; // CE
//Derived& dd = b; // CE
//Derived* dd = &b; // CE
}
```
При наследовании можно приводить объекты дочерних классов к родительским, но не наоборот.
```*.cpp=
struct Base {
int a = 0;
Base() = default;
Base(const Base& b) {
std::cout << "A";
}
};
struct Derived: private Base {
int a = 1;
Derived() = default;
};
void f(Base b) {
std::cout << b.a << '\n';
}
int main() {
// cast from heir to base
Derived d;
Base& b = d; // CE, because Derived class heirs privately from Base, so outside we don't know
// that Derived is heir of Base
}
```
### Множественное наследование
В C++ возможно множественное наследование, в отличие от других языков, таких как Java. Множественное наследование позволяет наследовать функциональность сразу нескольких классов.
```*.cpp=
struct Mom {
void fm() { std::cout << "Mom::f" << '\n'; }
int m = 1;
};
struct Dad {
void fd() { std::cout << "Mom::f" << '\n'; }
int d = 2;
};
struct Son: public Mom, public Dad { // order of inheritance have influency on how objects are stored in memory
int s = 3;
};
// Memory order
// [[ Mom::m ][ Dad::d ][ Son::s ]]
int main() {
Son s;
}
```
### Проблема ромба и неоднозначность вызова при наследовании
Множественное наследование несет в себе кроме удобства некоторые проблемы. Первая возникает при конфликте имен в родительских классах.
```*.cpp=
struct Mom {
void f() { std::cout << "Mom::f" << '\n'; }
int m = 1;
};
struct Dad {
void f() { std::cout << "Mom::f" << '\n'; }
int d = 2;
};
struct Son: public Mom, public Dad {
int s = 3;
};
// [ Mom::f ][ Dad::f ][ Son::s ]
int main() {
Son s;
s.f(); // ambiguous call of f() because both Dad and Mom have it's own f()
s.Mom::f(); // OK
}
```
Кроме неоднозначности вызовов при множественном наследовании также может возникнуть проблема ромба (Diamond problem).
```*.cpp=
struct Granny {
int g = 0;
};
struct Mom : public Granny {
int m = 1;
};
struct Dad : public Granny {
int d = 2;
};
struct Son: public Mom, public Dad {
int s = 3;
};
// [[[ Granny::g ][ Mom::m ]][[ Granny::g ][ Dad::d ]][ Son::s ]]
int main() {
Son s;
s.g; // CE because there are actually two copies of Granny::g
std::cout << sizeof(s) << '\n'; // 20 bytes
}
```
### Виртуальные методы/классы
#### Виртуальное наследование
Для решения проблемы ромба и подобных в С++ используется виртуальное наследование. Такой тип наследования позволяет хранить в памяти не сами копии родительских объектов, а указатели на них посредством виртуальных таблиц.
```*.cpp=
struct Granny {
int g = 0;
};
struct Mom : public virtual Granny {
int m = 1;
};
struct Dad : public virtual Granny {
int d = 2;
};
struct Son: public Mom, public Dad {
int s = 3;
};
// Memory representation
// [ mom_ptr ][ Mom::m ][ dad_ptr ][ Dad::d ][ Son::s ][ Granny:g ]
int main() {
Son s;
Dad* pd = &s; // cast to parent class
pd->g; // OK
// to see how it is stored in memory
std::cout << &s.Dad::d << ' ' << &s.Mom::m << ' ' << &s.Son::s << '\n';
std::cout << &s.Dad::g << ' ' << &s.Mom::g << '\n';
}
```
#### Виртуальные методы
При наследовании имена методов могут совпадать, в этом случае компилятором будет выбираться наиболее подходящая по некоторым параметрам версия этого метода. Например,
```*.cpp=
class A {
public:
void f() {
std::cout << "A\n";
}
};
class B: public A {
public:
void f() {
std::cout << "B\n";
}
};
int main() {
A a;
a.f(); // A
B b;
b.f(); // B
A* pb = &b;
pb->f(); // A
}
```
Т.е. при приведении к родительскому типу будет выбрана версия этого метода родителя. Однако, это нарушает принцип инкапсуляции, т.к. может давать
## Задание
Реализовать операторы для класса дробей:
```cpp=
class Fraction {
public:
friend std::ostream& operator<<(std::ostream& out, const Fraction& obj); // Оператор вывода << "числитель знаминатель"
friend std::istream& operator>>(std::istream& is, Fraction& obj); // Оператор ввода >> числитель >> знаминатель
Fraction& operator+=(const Fraction& other);
Fraction& operator-=(const Fraction& other);
Fraction& operator*=(const Fraction& other);
friend Fraction operator+(const Fraction& other) const;
friend Fraction operator-(const Fraction& other) const;
friend Fraction operator*(const Fraction& other) const;
Fraction& operator++();
Fraction operator++(int);
Fraction& operator--();
Fraction operator--(int);
Fraction(int numerator, int denominator) {
...
}
Fraction(int numerator) {
...
}
Fraction() {
...
}
Fraction(const Fraction& other) {
...
}
void fracSimplify() { // Сокращает дробь по возможности
...
}
double calc() const { // Возвращает значение дроби в десятичном виде
...
}
int getNumerator() const { // Возвращает значение числителя
...
}
int getDenominator() const { // Возвращает знамечение знаменателя
...
}
private:
int numerator;
int denominator;
}
```
**Файл сохраняется в репозитории под названием `classwork03/fraction.cpp`**