Наследование классов в C++: что это и как он работает

обложка статьи

Всем привет! Продолжаем изучать классы в C++. Сейчас поговорим об одном из свойств объектно ориентированного программирования - наследование.

Что такое наследование

Это принцип создание класса на базе уже существующего, при этом у нас есть возможность пользоваться функционалом (свойствами и методами) базового. Классы созданные таким образом называются производными или дочерними, а на базе которого создаются - родителем или базовым.

Этот механизм в объектно ориентированном программировании очень сильная фича. Она в несколько раз экономит время на создание проекта, а также не нагружает его повторяющимся кодом.

Производный класс мы можем усовершенствовать, добавляя:

  • Новые переменные.
  • Функции.
  • Конструкторы.

И все это не изменяя базовый класс.

наследование в c++

Например, на базе класса про животного можно создать потомка про собаку.

Модификатор доступа protected

Для начала нужно знать об доступе protected, который является неотъемлемой частью наследования. protected - это модификатор доступа, который работает как private, но распространяется также на свойства предка. На рисунки ниже можно увидеть какими доступами можно пользоваться.

наследование модификаторов c++
class Animals {
  protected:
    int zebras;
};

class Dog : public Animals {
  int counter_zebras () {
    return zebras;
  }
};

Если бы переменная zebras находилась в доступе private, то использование ее в функции counter_zebras привило бы к ошибке.

Как создать дочерний класс

Чтобы наследовать класс нужно использовать конструкцию ниже:

class <имя потомка> : <модификатор наследования> <имя родительского класса>{};

Первое на что надо обратить внимание это на двоеточие (:) оно одинарное, а не двойное как у области видимости.

Второе это <модификатор наследование>. При его оперировании можно задать какими модификаторами доступа родительского класса можно будет пользоваться в дочернем. Давайте поподробнее это разберем.

Вообщем можно указывать: public, private, protected. Из этих трех почти всегда используется public, но не плохо знать как работают другие.

Если вы новичок, то можете после информации про public перейти дальше.

public - использовать можно public и protected родительского класса. Кстати на рисунке выше изображены модификаторы доступа при использовании public.

class Animals {
  public:
    int counter;  // общее кол животных
  protected:
    int zebras;
    int bears;
    int dogs;

    // функция вычисление общего количества животных
    count_animals() {
      counter = dogs + bears + zebras;
    }
    set_dogs(int count_of_dogs) {
      dogs = count_of_dogs;
    }
};

class Dog : public Animals {
  public:
    int count_dogs() {
      return dogs;  // использовали переменную dog
    }
};
  • В строке 20: объявили функцию public: count_dogs(), которая возвращает переменную private: dogs из animals.

private - пользоваться можно лишь свойствами (не функциями) родителя. Чтобы использовать функции нужно разрешить это напрямую (и без круглых скобок, только имя), а также разрешать нужно в публичном доступе (public). Делается это так <родительский класс> :: <свойства>;.

class Dog : private Animals {
  public:
    int count_dogs() {
      return dogs;  // использовали переменную dog
    }
    Animals :: set_dogs; 

};

int main() {
    Dog jack;
    int k;
    cout << "Введите количество собак: "; cin >> k;

    jack.set_dogs(k);
    cout << "Количество собак равняется: "<< jack.count_dogs();
    return 0;
}
  • В строке 6: получили доступ к функции set_dogs().
  • В строке 15 - 16: отсылаем количество собак и потом их выводим.

protected - идентичен private, но свойства public переходит в доступ protected.

Как себя ведут модификаторы доступа при разных модификаторах наследования:

  • Модификатор наследования public: public -> public, private -> public, protected -> protected
  • Модификатор наследования private: public -> нет доступа, private -> нет доступа, protected -> нет доступа
  • Модификатор наследования protected: public -> protected, private -> protected, protected -> protected

Наследование конструктора

Наследованные конструкторы будут вызываться в порядке их наследования. Например, создали класс Earth, от него Animal, а от Animal создали Men. То вызов будет производиться так:

  1. Earth
  2. Animal
  3. Men

Для наследования конструктора нужно использовать следующую конструкцию:

<имя класса>  (<имя переменных конструктора>) :
<родительский класс> (<переменные конструктора>) { <тело> };
  • В начале указываем имя дочернего класса - <имя класса>.
  • Далее <имя переменных конструктора> указываем столько имен переменных сколько требует этого родительский конструктор, дальше передаем базовому конструктору в скобках (<переменные конструктора>)объявленные переменные.
  • <тело> - это тело конструктора, об этом ниже.
Bear (name) : 
Animal (string name) {};

Но чтобы все это работало, в конструкторе базового класса к переменным нужно обращаться через this->.

class Animal {
  public:
    animal () {
      cout << "Создан класс без первичных объвлений"; 
    } 
    animal (int counter) { 
      this->count_of_animal = counter;
    }
  protected:
    int counter;
    int count_of_animal;
};

class Dog : Animal {
  public:
    dog () : animal () {} // перегрузка 
    dog (int counter) :   // конструкторов
    animal (counter) {}

    get_count_animal() {
      return count_of_animal;
    }
};

В строках 16 - 18: мы наследовали два конструктора, которые можем перегружать.

int main () {
  Dog march;  
  Dog april(12);
}

Если хотите подробнее познакомится c перегрузками (функций) переходите сюда.

Но может появится необходимость добавить еще одну дополнительную переменную в конструктор, которой не имеет базовый. Для этого объявляем еще одну переменную в <переменные конструктора> и делаем с ней, все что угодно в <теле>.

class Dog : Animal {
  public:
    dog (int counter, vector  names_dogs) :
    animal (counter) {
      for (int i = 0; i < names_dogs.size(); i++) {
        names.push_back(names_dogs[i]);
      }
    }
    get_names () {
      for (int i = 0; i < names.size(); i++) {
        cout << names[i] << ", ";
      }
    }
  // ...
  private:
    vector  names;
};
  • В строке 3: добавили вектор в конструктор. В этом векторе должны храниться имена собак.
  • В строке 16: объявили вектор names в котором должны хранится клички собак.
  • В строках 5 - 7: считываем имена в вектор.

Давайте посмотрим, как это работает на реальном примере:

int main () {
  setlocale(LC_ALL, "Rus");

  int n;
  cout << "Введите количество собак "; cin >> n;

  vector <int> vec;
  string s;

  for (int i = 0; i < n; i++) {
    cout << "Введите имя " << i + 1 << " собаки: "; cin >> s;
    vec.push_back(s);
  }

  cout << endl;
  Dog jule(n, vec);
  jule.get_names();
  
  return 0;
}
  • В строке 5: предлагаем пользователю ввести количество собак.
  • В строке 7: создали вектор чисел vec.
  • В строке 16: объявили класс jule сразу же передав число собак и вектор и имен.
  • В строке 17: выводим все имена.
Введите количество собак: 3
Имя первой 1 собаки: Jack
Имя первой 2 собаки: Rex
Имя первой 3 собаки: Abbey

Jack, Rex, Abbey
Process returned 0 (0x0) execution time : 0.010 s
Press any key to continue.

Наследование деструктора

Наследованные деструкторы вызываются наоборот по сравнению с вызыванием конструктора. Если классы созданные так Earth -> Animal -> Men, то цепочка вызовов конструктора имеет такой вид:

  1. Men
  2. Animal
  3. Earth

Про деструктор можно почитать здесь.

class name {
private:
    ~name () {
      cout << "1";
    }
};

class second_name : public name {
public:
  ~second_name () {
    cout << "2";
  };
};

На этом все! Если есть вопросы, то задавайте их в комментариях ниже. Удачи!

Обсуждение