Наслідування класів

В принципах ООП ми частково зачепили тему наслідування. Розберемося в цьому детальніше та спробуємо зрозуміти як це варто використовувати.

Давайте розберемося в першу чергу навіщо це нам. Ми вже знаємо, що за правилами інкапсуляції варто робити так щоб один клас представляв один Але ж бувають моменти, коли два бувають надзвичайно схожими або просто мають багато спільного. Наприклад літак та автомобіль. Кожен із них має двигун, певну кількість місць в салоні, мають місце для багажу, но при цьому і мають багато відмінностей. Візьмемо ще для прикладу мотоцикл та човен. Вони теж мають двигун, певну марку чи назву. Уявіть на скільки сильно збільшиться ваша програма, якщо для кожного ми будемо створювати класи в яких будуть повторюватися одні й ті ж самі От саме в таких випадках нам необхідне наслідування. Воно дозволить створити клас зі спільними а потім передати це кожному з класів (дочірні класи будуть наслідувати батьківського Таким чином кожен із дочірніх класів зможуть мати всі батьківського класу і при цьому ви без проблем можете нові у дочірній клас.

Для того щоб наслідувати вам потрібно створити батьківський клас. Далі, ви робите які будуть унаслідуватися публічними . Якщо будуть приватними або захищеними то їх використовувати буде проблематично або взагалі не можливо.

Коли ви будете створювати клас і вам буде потрібно наслідувати інший клас, то потрібно виконати такі дії:

1) Вказати ключове слово «class».

2) Вказати ім’я дочірнього класу.

3) Поставити символ двокрапку «:».

4) Вказати тип наслідування або В залежності від типу наслідування ви зможете або не зможете використовувати батьківського класу за допомогою дочірнього класу (з цим розберемося трішки пізніше).

5) Вказати ім’я батьківського класу.


        class Ім’я_дочірнього_класу : тип_наслідування Ім’я_батьківського_класу
        

Давайте розглянемо як це працює на практиці.

    
            #include <iostream>

            //це батьківський клас який ми будемо наслідувати
            class Father
            {
                //дана змінна є приватною, тому дочірній клас
                //не матиме до неї доступу
                int b = 10;

                public:
                //дана змінна є публічною, тому дочірній клас
                //зможе її використовувати
                int a = 5;
            };

            //ми наслідуємо клас "Father" публічно. Тому, можна буде
            //використовувати певні змінні/методи батьківського класу
            //в середині (та за межами) дочірнього.
            class Child : public Father
            {
                public:
                int c = 21;

                //взаємодіємо зі змінною "а" яка належить батьківському класу
                //як бачите ми можемо її використовувати в дочірньому класі
                //немов вона належить йому (створена в середині дочірнього класу)
                void display_A() { std::cout << "Child A " << a << std::endl; }

                //а тут ми вже маємо помилку, бо змінна "b" в батьківському класі
                //є приватною, тому ми не можемо використовувати її за його межами
                //void display_B() { std::cout << b << std::endl; }

                //виводимо зміну дочірнього класу. дана змінна є публічною, тому
                //тут не має помилки
                void display_C() { std::cout << "Child C " << c << std::endl; }
            };


            int main()
            {
                //створимо змінну типу клас Father
                Father F;
                //виведемо значення змінної "а" класу Father
                std::cout << "F.a " << F.a << std::endl;

                //створимо змінну типу дочірнього класу Child
                Child C;

                //виведемо значення наших змінних які знаходяться в
                //дочірньому класі
                C.display_A();
                C.display_C();

                //змінимо значення змінної "а" за допомогою дочірнього класу
                C.a = 123;

                //та подивимося чи зміниться це значення ще і у батьківському класі
                //як побачите змінна "а" у батьківському класі залишиться такою ж
                //як, і була
                std::cout << "F.a " << F.a << std::endl; //виведе 5
                std::cout << "C.a " << C.a << std::endl; //виведе 123

                //тепер спробуємо змінити значення змінної "а" за допомогою
                //батьківського класу і подивимося чи зміниться це значення, і
                //в дочірньому класі
                F.a = 987;

                //як ви побачите значення змінної "а" змінилося лише в батьківському
                //класі. тому, можна зробити висновок, що ці змінні вже є не залежними
                //і це можна порівняти з тим, що ми просто створили дві однакові змінні
                //але в різних класах
                std::cout << "F.a " << F.a << std::endl; //виведе 987
                std::cout << "C.a " << C.a << std::endl; //виведе 123

                return 0;
            }
        

Від одного батьківського класу можна створити безліч дочірніх. При цьому дочірні класи не будуть знати один про одного і відповідно будуть незалежні один від одного.

Доволі часто зустрічається, що батьківський та дочірній клас мають однакові змінні та методи. Тобто, що їх типи та імена є однакові (бо придумати різні назви для великої кількості змінних та методів буває не так вже і просто).

В таких випадках програма не буде видавати помилку через конфлікт імен (як було із звичайними функціями), а використовуватиме ту змінну чи метод які є найближчими. Тобто, коли ми матимемо в батьківському та дочірньому класі змінну/метод з однаковими іменами і будемо викликати (використовувати) їх з дочірнього класу, то найближчим буде саме дочірнього класу.

Якщо ж буде необхідно використовувати ще батьківського класу, то можна створити в середині дочірнього класу змінну типу батьківський клас і за її допомогою звертатися до батьківського класу.

    
        #include <iostream>

        class Father
        {
            public:
            void display() { std::cout << "Display class Father" << std::endl; }
        };

        class Child : public Father
        {
            public:
            //у дочірньому класі створюємо метод (функцію) з таким же ім'ям
            //як і у батьківському класі
            //при виклику даного методу з дочірнього класу буде використовуватися
            //саме метод дочірнього класу, бо він буде найближчим
            void display() { std::cout << "Display class Child" << std::endl; }
        };

        class Child2 : public Father
        {
            public:
            //якщо нам необхідно викликати функції з батьківського класу, то
            //можна створити змінну типу батьківський клас (Father)
            //та використовувати її як посередника
            Father f;
            void display() { std::cout << "Display class Child 2" << std::endl; }
        };

        int main()
        {
            Father F;
            Child C;
            Child2 C2;

            //викликаємо метод батьківського класу бо програма, бо програма
            //не знає, що є "дочірні класи", тому для неї даний метод
            //є єдиним і знаходиться в класі Father
            F.display();

            //виведеться метод з класу Child, адже для програми він є найближчим
            C.display();

            //за допомогою "посередника" ми можемо в дочірньому класі викликати
            //метод батьківського класу при цьому обійшовши "пріоритет ближнього"
            C2.f.display();

            return 0;
        }
    

Звісно ж створювати кожного разу в дочірньому класі екземпляр батьківського класу буде доволі проблематично, тому часто використовують роз іменування який позначається як

На справді програма створює екземпляр батьківського класу самостійно, але до його елементів не можна звернутися за допомогою оператора (хіба, що ви самостійно створили екземпляр батьківського класу, як у коді з верху), тому доводиться використовувати оператор

Даний оператор працює так:

1) Вказуємо ім’я змінної дочірнього класу (відповідно у вас має бути змінна типу дочірній клас).

2) Ставимо оператор «.».

3) Вказуємо ІМ’Я БАТЬКІВСЬКОГО КЛАСУ. Важливо! Тут використовується не змінна типу батьківський клас, а саме має бути даного класу.

4) Ставимо оператор

5) Вказуємо до якого хочемо звернутися.

Розглянемо це на прикладі.

    
        #include <iostream>

        class Father
        {
            public:
            void display() { std::cout << "Display class Father" << std::endl; }
        };

        class Child : public Father
        {
            //зверніть увагу. ми не створюємо змінну типу
            //батьківський клас
            public:
            void display() { std::cout << "Display class Child" << std::endl; }
        };

        int main()
        {
            Child C;

            //звертаємося до методу display батьківського класу Father
            C.Father::display();

            return 0;
        }
    

Давайте розберемося в першу чергу із типом видимості (мається на увазі доступ) батьківського класу. Ми вже знаємо, що в середині класу змінні та методи можуть бути публічними приватними та захищеними

Відповідно, якщо змінна/метод є публічним, то ми зможемо без проблем його використовувати і в дочірніх класах. Це можна порівняти із загальнодоступною інформацією. Наприклад, інформація, що знаходиться у Вікіпедії є публічною. Тому, нею може користуватися

Якщо ж змінна/метод є приватними, то вони будуть не доступні для дочірніх класів. Це можна порівняти з вашою найбільшою таємницею, яку ви не довіряєте ні кому. Тому, її ніхто не знає і не зможе використати крім вас.

Коли змінна/метод є захищені, то їх можна використовувати лише за певних обставин. Наприклад, у повсякденному житті, школі, університеті чи на роботі ви можете мати свою компанію друзів з якими ділитеся секретами. Відповідно, доступ до цих секретів люди мають лише за певних обставин. Тобто, якщо входять у цю компанію.

Коли ми наслідуємо клас, то ми маємо вказати тип даного наслідування або Цей процес можна порівняти зі спілкуванням. Наприклад, ви маєте і так далі. Відповідно, у вас є рівень довіри до цієї людини, а у неї до вас. Припустиму ця людина розповіла вам щось (якусь Тобто, ви НАСЛІДУЄТЕ (ви є дочірнім класом) якусь інформацію від даної людини (людина є батьківським Після чого у вас постає вибір: ні кому не розповідати дану інформацію (приватне розповісти за певних умов (захищене розповідати всім цю інформацію (публічне

Зауважимо, якщо ви вирішите все розповісти (публічне наслідування), то ви все одно не зможете розповісти інформацію, яку вам не розповіли адже ви її просто не знаєте.

Важливо!!! Ви не зможете використовувати захищені та приватні батьківського класу за межами дочірнього класу на пряму, але зможете використовувати захищені методи в середині дочірнього класу і за допомогою «посередників» навіть за межами дочірнього класу.

Давайте розглянемо все на практиці.

    
        #include <iostream>

        class Father
        {
            private:
            int private_father = 1;

            public:
            int public_father = 2;

            protected:
            int protected_father = 3;
        };

        class public_Child : public Father
        {
            public:
            //не маємо доступу до приватних змінних, тому матимемо помилку
            //void display_private() { std::cout << private_father << std::endl; };
            void display_public() { std::cout << public_father << std::endl; };
            void display_protected() { std::cout << protected_father << std::endl; };
        };

        class private_Child : private Father
        {
            public:
            //не маємо доступу до приватних змінних, тому матимемо помилку
            //void display_private() { std::cout << private_father << std::endl; };
            void display_public() { std::cout << public_father << std::endl; };
            void display_protected() { std::cout << protected_father << std::endl; };
        };

        class protected_Child : protected Father
        {
            public:
            //не маємо доступу до приватних змінних, тому матимемо помилку
            //void display_private() { std::cout << private_father << std::endl; };
            void display_public() { std::cout << public_father << std::endl; };
            void display_protected() { std::cout << protected_father << std::endl; };
        };

        int main()
        {
            public_Child pubC;
            private_Child priC;
            protected_Child proC;

            //Публічне наслідування.
            //на пряму ми можемо використовувати лише публічну
            //зміннну батьківського класу
            std::cout << pubC.public_father << std::endl;
            //приватну та захищену змінну використовувати не можна
            //pubC.private_father;
            //pubC.protected_father;

            //але при цьому ми можемо через "посередників" використовувати
            //публічну та захищену змінну батьківського класу
            pubC.display_public();
            pubC.display_protected();

            //Приватне наслідування.
            //Жодну змінну батьківського класу на пряму використовувати
            //не можна
            //priC.public_father;
            //priC.private_father;
            //priC.protected_father;

            //але при цьому ми можемо через "посередників" використовувати
            //публічну та захищену змінну батьківського класу
            priC.display_public();
            priC.display_protected();

            //Захищене наслідування.
            //Жодну змінну батьківського класу на пряму використовувати
            //не можна
            //proC.public_father;
            //proC.private_father;
            //proC.protected_father;

            //але при цьому ми можемо через "посередників" використовувати
            //публічну та захищену змінну батьківського класу
            proC.display_public();
            proC.display_protected();

            return 0;
        }
    

Підведемо підсумок по модифікаторах доступу і наслідуванні.

Якщо наслідування є публічним то:

1) Публічні змінні/методи залишаються публічними і їх можна використовувати на пряму.

2) Захищенні змінні/методи залишаються захищеними. Їх не можна використовувати на пряму, но їх можна використовувати за допомогою проміжних

3) Залишаються приватними і їх не можна використовувати взагалі.

Якщо наслідування є захищеним («protected»), то:

1) Публічні змінні/методи стають захищеними. Їх не можна використовувати на пряму, но їх можна використовувати за допомогою проміжних

2) Захищенні змінні/методи залишаються захищеними. Їх не можна використовувати на пряму, но їх можна використовувати за допомогою проміжних

3) Залишаються приватними і їх не можна використовувати взагалі.

Якщо наслідування є приватним («private»), то:

1) Публічні змінні/методи стають приватними. Їх не можна використовувати на пряму, но їх можна використовувати за допомогою проміжних

2) Захищенні змінні/методи залишаються захищеними. Їх не можна використовувати на пряму, но їх можна використовувати за допомогою проміжних

3) Залишаються приватними і їх не можна використовувати взагалі.

Ви можете створювати ціле «класове дерево». Тобто, ви можете створити клас який є дочірнім для одного класу і одночасно батьківським для іншого. Для цього необхідно лише повторити наслідування.

    
        #include <iostream>

        //перший батьківський клас.
        class A
        {
            public:
            int a = 1;
        };

        //клас "В" є дочірнім для класу "А".
        //враховуючи, що ми будемо робити клас "В"
        //батьківським для класу "С", то щоб для нього
        //(класу "С") були доступні методи класу "А"
        //наслідування має бути публічним
        class B : public A
        {
            public:
            int b = 22;
        };

        //клас "С" є дочірнім від класу "В", а клас "В"
        //є дочірнім від класу "А", то маючи публічне наслідування
        //ми будемо мати доступ до методів класу "А"
        class C : public B
        {
            public:
            int c = 333;
        };


        int main()
        {
            C class_C;

            //перевіряємо, що ми дійсно маємо доступ до
            //змінних/методів батьківських класів
            std::cout << class_C.a << std::endl;
            std::cout << class_C.b << std::endl;
            std::cout << class_C.c << std::endl;

            return 0;
        }
    

Коли під час дерева наслідування ви в декількох батьківських класах маєте однакові то при потребі скористатися конкретного класу вам допоможе роз іменування

    
        #include <iostream>

        class A
        {
            public:
            void display() { std::cout << "Display class A" << std::endl; }
        };

        class B : public A
        {
            public:
            void display() { std::cout << "Display class B" << std::endl; }
        };

        class C : public B
        {
            public:
            void display() { std::cout << "Display class C" << std::endl; }
        };

        int main()
        {
            C class_C;

            //звертаємося до методу display батьківського класу "A"
            //таким чином ми ігноруємо правило "найближчого" і можемо
            //використовувати змінну/метод необхідно класу
            class_C.A::display();

            return 0;
        }
    

Кожна людина в реальному житті успадковує гени своїх батька та матері. Тобто, вона наслідує гени двох батьків. Схожа ситуація може бути і з класами. Тобто, ми можемо зробити наслідування одночасно від декількох класів.

Для того щоб зробити наслідування декількох класів потрібно перечислити ці класи через кому при цьому для кожного з них необхідно вказати його тип наслідування.

    
        #include <iostream>

        class A
        {
            public:
            int a = 1;
        };

        class B
        {
            public:
            int b = 22;
        };

        //зробимо наслідування декількох класів
        //всі класи від яких робимо наслідування мають бути
        //перечисленні через кому.
        //для кожного класу ми вказуємо свій тип наслідування
        //відповідно класи "А" і "В" є батьківськими
        //для класу "С"
        class C : public A, public B
        {
            public:
            int c = 333;
        };

        int main()
        {
            C class_C;

            //виводимо значення кожної змінної
            //з усіх класів дочірнього та батьківських
            std::cout << "A: " << class_C.a << std::endl;
            std::cout << "B: " << class_C.b << std::endl;
            std::cout << "C: " << class_C.c << std::endl;

            return 0;
        }
    

Ми вже розглядали ситуацію, коли батьківський та дочірній клас мають однакові змінні/методи. І, ми вже з’ясували, що в таких випадках по замовчуванню використовується змінна/метод найближчого класу. Розглянемо ситуацію, коли в декількох батьківських класах буде однакова змінна/метод. То, в таких ситуаціях використовують «оператор роз іменування контенту» (::).

    
        #include <iostream>

        class A
        {
            public:
            void display() { std::cout << "Display class A" << std::endl; }
        };

        class B
        {
            public:
            void display() { std::cout << "Display class B" << std::endl; }
        };

        //зробимо наслідування декількох класів
        //всі класи від яких робимо наслідування мають бути
        //перечисленні через кому.
        //для кожного класу ми вказуємо свій тип наслідування
        //відповідно класи "А" і "В" є батьківськими
        //для класу "С"
        //при цьому зауважте, що ми не створюємо екземплярів
        //батьківських класів
        class C : public A, public B
        {
        };

        int main()
        {
            C class_C;

            //Оскільки даний метод є як в батьківському класі "А"
            //так і в "В", то програма не знає який з них необхідно
            //використовувати. тому, вона видаватиме помилку
            //class_C.display();

            //після імені класу ми за допомогою "." звертаємося до
            //конкретного батьківського класу, а далі
            //використовуємо "оператор роз іменування" (::) для того
            //щоб чітко вказати до якого методу (чи змінної)
            //даного класу ми будемо звертатися
            class_C.B::display();

            return 0;
        }
    

В даному уроці ми розглянули базові принципи наслідування. Наслідування класів з конструкторами розглянемо в наступному уроці.