--- 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`**