Наследование классов в языке C++
Наследование используется в случае, когда у нескольких классов есть набор членов или методов, являющихся общими для всех этих классов. В этом случае целесообразно выделить эти члены и методы в отдельный родительский класс, а сами классы унаследовать от него. В этом случае все методы и члены родительского класса будут также присутствовать и в дочерних классах.
В языке C++ родительский класс указывается при объявлении дочернего класса. После имени дочернего класса ставится двоеточие, далее указывается спецификатор доступа, и после него указывается имя родительского класса.
class Parent < public: int val = 100; >; class Child : public Parent < >; Child child; std::coutСпецификаторы доступа
Спецификатор доступа, указываемый при наследовании, позволяет переопределить тип доступа к членам родительского класса при обращении к ним через объект дочернего класса. Таким образом, например, можно сделать открытые члены родительского класса закрытыми, для доступа через объект дочернего класса.
Возможные значения спецификаторов:
- public - используется в большинстве случаев. При его использовании не происходит никаких изменений в доступе к членам родительского класса.
- protected - не используется практически никогда. При его использовании открытые (public) члены родительского класса становятся защищенными (protected).
- private - используется, когда нужно закрыть все члены родительского класса. При его использовании открытые и защищенные члены родительского класса становятся закрытыми.
class Parent < public: int val = 100; >; class Child : private Parent < public: int getVal() < return val; >>; Child child; std::coutСпецификаторы доступа при наследовании влияют на доступ к членам родительского класса только при обращении к ним через объект дочернего класса. Доступ к членам родительского класса из методов дочернего класса не меняется и остается таким, каким он был задан в родительском классе.
Если спецификатор доступа при наследовании не указан, то считается, что он private.
Конструкторы и деструкторы
При наследовании дочернего класса от родительского при создании объекта дочернего класса сначала выполняется конструктор родительского класса, а потом конструктор дочернего класса.
При уничтожении объекта выполнение деструкторов происходит наоборот - сначала выполняется деструктор дочернего класса, а потом выполняется деструктор родительского класса.
class Parent < public: Parent() < std::cout ; ~Parent() < std::cout ; >; class Child : public Parent < public: Child() < std::cout ; ~Child() < std::cout ; >; Child child; /* Output: Parent constructor Child constructor Child destructor Parent destructor */Инициализация родительского класса
Если при создании дочернего класса требуется определенным образом инициализировать родительский класс, то необходимо указать конструктор родительского класса с параметрами в списке инициализации дочернего класса. Если конструктор не указан, то используется конструктор по-умолчанию. Если такого нет, то генерируется ошибка.
class Parent < public: int val; Parent(int _v) : val(_v) <>>; class Child : public Parent < public: Child(int v) : Parent(v) <>// Конструктор родительского класса >; Child child(10); std::coutПереопределение методов родительского класса
Для переопределения методов родительского класса в дочернем классе достаточно просто указав их с тем же именем, что и в родительском классе. При этом унаследованный метод полностью заменяется на новый. Заменяются как параметры вызова метода, так и типы возвращаемого значения. То же самое относится и к переопределению переменных-членов родительского класса. Их также можно переопределять в дочернем классе.
class ParentClass < private: double val = 12.34f; public: double getVal() < return val; >double getMultVal(double mult) < return val * mult; >>; class ChildClass : public ParentClass < public: int val = 100; ChildClass(int v) : val(v) <>int getMultVal() < return val * 2; >>; ChildClass child(200); std::coutИз переопределенных методов можно обращаться к изначальным методам родительского класса. Для этого их вызов осуществляется через имя родительского класс и оператор разрешения области видимости ::.
class ChildClass : public ParentClass < public: auto getVal() < return ParentClass::getVal(); >>;Переопределение видимости
В случае, когда в дочернем классе требуется переопределить лишь спецификатор видимости метода родительского класса, используется using-объявление.
class ParentClass < protected: int val = 100; int getVal() < return val; >>; class ChildClass : public ParentClass < public: using ParentClass::val; using ParentClass::getVal; >; ChildClass child; std::coutОднако такое переопределение можно сделать только для тех членов родительского класса, которые доступны дочернему классу. Соответственно не получится сделать закрытые члены родительского класса открытыми. Тем не менее, при помощи этого метода можно закрыть открытые члены родительского класса. Также можно просто удалить ненужные члены класса, просто присвоив им значение delete.
class ParentClass < public: int val = 100; int getVal() < return val; >int getDoubleVal() < return val * 2; >>; class ChildClass : public ParentClass < private: using ParentClass::val; // Закрываем using ParentClass::getVal; // Закрываем public: int getDoubleVal() = delete; // Удаляем >;Множественное наследование
Язык C++ поддерживает возможность наследоваться от нескольких классов. Для этого они вместе со спецификаторами доступа указываются через запятую.
class ParentOne < public: ParentOne() <>>; class ParentTwo < public: ParentTwo() <>>; class ChildClass : public ParentOne, public ParentTwo < public: ChildClass() <>>;Конфликты
При множественном наследовании дочерний класс наследует все свойства и методы родительских классов. Вследствие этого может возникнуть неоднозначная ситуация, когда, например, несколько родительских классов имеют переменную или метод с одним и тем же именем. Обойти эту проблему можно, указав какой родительский класс использовать в данном случае.
class ParentOne < public: int val; ParentOne() <>>; class ParentTwo < public: int val; ParentTwo() <>>; class ChildClass : public ParentOne, public ParentTwo < public: ChildClass() <>>; ChildClass child; std::coutВиртуальные функции
Виртуальные функции предназначены для того, чтобы иметь возможность вызывать методы дочернего класса через ссылку или указатель типа родительского класса, указывающих тем не менее на объект дочернего класса.
class ParentClass < public: const char* getFirstMsg() < return "First message from ParentClass"; >virtual const char* getSecondMsg() < return "Second message from ParentClass"; >>; class ChildClass : public ParentClass < public: const char* getFirstMsg() < return "First message from ChildClass"; >virtual const char* getSecondMsg() < return "Second message from ChildClass"; >>; ChildClass child; ParentClass& p = child; std::coutЭто полезно, когда, например, требуется собрать массив объектов различных дочерних классов, имeющих общего родителя. В этом случае в массиве нельзя размещать указатели разных типов, но можно разместить указатели одного типа - указателя на родительский класс. В дальнейшем для каждого указателя в этом массиве можно вызывать один виртуальный метод, а выполняться будут различные методы соответствующих дочерних классов.
class ParentClass < public: virtual const char* getMsg() < return "Hello from ParentClass"; >>; class ChildClass1 : public ParentClass < public: virtual const char* getMsg() < return "Hello from ChildClass1"; >>; class ChildClass2 : public ParentClass < public: virtual const char* getMsg() < return "Hello from ChildClass2"; >>; ChildClass1 child1; ChildClass2 child2; ParentClass* arr[] = < &child1, &child2 >; for (auto p : arr) std::cout getMsg() Hello from ChildClass1 //> Hello from ChildClass2Важные замечания
- Сигнатура виртуального метода дочернего класса должна полностью соответствовать сигнатуре виртуального метода родительского класса. Если у дочернего метода будет другой тип параметров, нежели у родительского, то этот метод вызываться не будет.
- Нельзя вызывать виртуальные функции в конструкторах и деструкторах классов. Если это сделать, будут вызываться родительские методы.
- Обработка и выполнение виртуальных функций происходит медленнее, чем обработка и выполнение обычных методов
Исключение
При использовании виртуальных функций есть одно исключение, когда сигнатура метода дочернего класса может не совпадать с сигнатурой родительского класса, но переопределение все же выполнится. Это тот случай, когда возвращаемый тип метода - это указатель на сам класс (так называемый "ковариантный тип возврата"). В этом случае родительский класс может возвращать указатель на свой класс, а дочерний класс может возвращать указатель на свой. При этом переопределение все равно произойдет.
class ParentClass < public: virtual ParentClass* getThis() < std::cout >; class ChildClass : public ParentClass < public: virtual ChildClass* getThis() < std::cout >; ChildClass child; ParentClass& p = child; p.getThis(); // Child
Игнорирование виртуальных функций
При вызове виртуального метода, можно указать, что он не будет вызывать метод дочернего класса. Для этого при вызове следует указать метод какого класса следует вызвать.
class ParentClass < public: virtual const char* getMsg() < return "parent msg"; >>; class ChildClass : public ParentClass < public: const char* getMsg() override < return "child msg"; >>; ChildClass child; ParentClass& p = child; std::coutМодификатор override
При использовании виртуальных функций часто возникают случаи ошибок из-за несовпадения сигнатур переопределяемых методов у дочернего и родительского класса. В этом случае сложно отловить ошибку, потому что она возникает только при выполнении программы. Именно для выявления таких случаев в версии 11 языка C++ добавили модификатор override.
Этот модификатор ставится после указания сигнатуры метода и контролирует возможность переопределения метода при компиляции программы. То есть, если у метода указан данный модификатор и этот метод не переопределяет никакой родительский метод, то при компиляции возникает ошибка.
class ParentClass < public: virtual const char* getMsg() const < return "Hello from ParentClass"; >>; class ChildClass : public ParentClass < public: virtual const char* getMsg() const override < return "Hello from ChildClass"; >>;Использование модификатора override никак не влияет на эффективность или производительность программы, но помогает избежать непреднамеренных ошибок. Следовательно, настоятельно рекомендуется использовать модификатор override для каждого из своих переопределений.
Модификатор final
Модификатор final используется тогда, когда требуется запретить переопределять метод родительского класса в его наследниках, либо даже запретить наследовать весь класс целиком. В первом случае модификатор ставится там же, где и модификатор override - после сигнатуры метода.
class ParentClass < public: virtual const char* getMsg() const final < return "Hello from ParentClass"; >>;В случае, когда модификатор запрещает наследовать весь класс целиком, его ставят сразу после названия класса.
class ParentClass final < public: virtual const char* getMsg() const < return "Hello from ParentClass"; >>;Виртуальные деструкторы
При использовании наследования класов, деструктор у родительского класса еобходимо указывать, как виртуальный. Иначе может получиться такая ситуация, что при уничтожении объекта через ссылку или указатель на родительский класс, деструктор дочернего класса не будет вызван, что может привести к утечке памяти и прочим ошибкам.
class ParentClass < public: virtual ~ParentClass() < std::cout >; class ChildClass : public ParentClass < int* arr; public: ChildClass() < arr = new int[100]; >~ChildClass() < std::cout >; ParentClass* p = new ChildClass(); delete p; //> child destructor //> parent destructorАбстрактные функции и классы
Есть случаи, когда метод не имеет смысла определять в родительском классе, а имеет смысл только в дочерних классах. В этом случае в родительском классе этот метод можно сделать абстрактным или "чистой виртуальной функцией". Для этого необходимо вместо определения метода присвоить ему значение 0.
class ParentClass < public: virtual const char* getMsg() = 0; >;Класс, который содержит хоть одну абстрактную функцию, также называется абстрактным. Создать объект такого класса невозможно, потому что у него есть методы без определения. Возможно только создавать объекты наследников абстрактного класса. У наследников абстрактного класса все методы абстрактного класса должны быть определены, иначе эти классы также считаются абстрактными.
Определение абстрактного метода можно задать отдельно от определения класса. Это бывает полезно, когда необходимо сделать класс абстрактным, чтобы нельзя было создать его объект, но в то же время чтобы у дочерних классов была возможность использовать родительское определение метода по-умолчанию.
class ParentClass < public: virtual const char* getMsg() = 0; >; const char* ParentClass::getMsg() < return "default"; >class ChildClass : public ParentClass < public: const char* getMsg() override < return ParentClass::getMsg(); >>; ChildClass child; std::coutЕсли у класса нет переменных-членов и все методы класса являются абстрактными, то такой класс называется интерфейсным классом. Принято название такого класса начинать с буквы "I" (Inteface).
Виртуальный базовый класс
В случае множественного наследования дочернего класса D от нескольких родителей B и C, эти родительские классы в свою очередь также могут быть унаследованы от какого-то базового класса A. В этом случае при создании объекта дочернего класса D происходит конструирование нескольких копий базового класса A - по одной на каждого родителя B и C.
class A < public: A() < std::cout >; class B : public A < public: B() < std::cout >; class C : public A < public: C() < std::cout >; class D : public B, public C < public: D() < std::cout >; D d; // A B A C DЕсли такое поведение нежелательно, и требуется, чтобы базовый класс создавался только один раз, то при объявлении дочерних классов при наследовании следует указать ключевое слово virtual.
class A < public: A() < std::cout >; class B : virtual public A < public: B() < std::cout >; class C : virtual public A < public: C() < std::cout >; class D : public B, public C < public: D() < std::cout >; D d; // A B C DВ этом случае объект базового класса A создается уже не классами B и C, а классом D. То есть, если создавать классы B и C по-отдельности, то объект класса A будет создан два раза.
class A < public: A() < std::cout >; class B : virtual public A < public: B() < std::cout >; class C : virtual public A < public: C() < std::cout >; class D : public B, public C < public: D() < std::cout >; B b; // A B C c; // A CПриведение к родительскому типу
При присваивании объекта дочернего класса объекту родительского класса происходит приведение типа объекта дочернего класса к типу родительского класса. то есть все, что было в объекте дочернего класса убирается, и остается только та часть объекта, которая соответствует родительскому классу.
class ParentClass < public: virtual const char* getMsg() < return "Parent"; >>; class ChildClass : public ParentClass < public: const char* getMsg() override < return "Child"; >>; ChildClass child; ParentClass& p = child; ParentClass parent = child; std::coutДинамическое приведение типов
Если существует объект дочернего класса и на него есть ссылка или указатель типа родительского класса, то есть возможность привести эту ссылку или указатель к типу этого дочернего класса. Для этого используется оператор динамического приведения типов dynamic_cast.
class ParentClass < public: virtual const char* getMsg() < return "ParentClass"; >>; class ChildClass : public ParentClass < public: const char* str = "ChildClass"; >; ChildClass child; ParentClass* ptr = &child; ChildClass* cp = dynamic_cast(ptr); // Динамическое приведение типа std::cout strЕсть случаи, когда оператор dynamic_cast не может выполнить конвертацию. Это может произойти, например, если переданный ему указатель не соответствует тому типу, в который производится конвертация. В таких случаях оператор dynamic_cast возвращает нулевой указатель в случае работы с указателями, или выбрасывает исключение std::bad_cast, в случае работы со ссылками.
Поскольку динамическое приведение типов происходит в процессе выполнения программы, оно дает небольшое замедление работы. Также динамическое приведение типов не работает в случаях, когда у класса отсутствуют виртуальные функции (нет таблиц виртуальных функций).
- Уголок в Вконтакте
- Уголок в Телеграм
- Уголок в YouTube
Какой вид наследования используется в языке c
Наследование (inheritance) представляет один из ключевых аспектов объектно-ориентированного программирования, который позволяет наследовать функциональность одного класса (базового класса) в другом - производном классе (derived class).
Зачем нужно наследование? Рассмотрим небольшую ситуацию, допустим, у нас есть классы, которые представляют человека и сотрудника компании:
class Person < public: void print() const < std::cout std::string name; // имя unsigned age; // возраст >; class Employee < public: void print() const < std::cout std::string name; // имя unsigned age; // возраст std::string company; // компания >;
В данном случае класс Employee фактически содержит функционал класса Person: свойства name и age и функцию print. В целях демонстрации все переменные здесь определены как рубличные. И здесь, с одной стороны, мы сталкиваемся с повторением функционала в двух классах. С другой строны, мы также сталкиваемся с отношением is ("является"). То есть мы можем сказать, что сотрудник компании ЯВЛЯЕТСЯ человеком. Так как сотрудник компании имеет в принципе все те же признаки, что и человек (имя, возраст), а также добавляет какие-то свои (компанию). Поэтому в этом случае лучше использовать механизм наследования. Унаследуем класс Employee от класса Person:
class Person < public: void print() const < std::cout std::string name; // имя unsigned age; // возраст >; class Employee : public Person < public: std::string company; // компания >;
Для установки отношения наследования после названия класса ставится двоеточие, затем идет спецификатор доступа и название класса, от которого мы хотим унаследовать функциональность. В этом отношении класс Person еще будет называться базовым классом (также называют суперклассом, родительским классом), а Employee - производным классом (также называют подклассом, классом-наследником).
Спецификатор доступа позволяет указать, к каким членам класса производный класс будет иметь доступ. В данном случае используется спецификатор public , который позволяет использовать в производном классе все публичные члены базового класса. Если мы не используем модификатор доступа, то класс Employee ничего не будет знать о переменных name и age и функции print.
После установки наследования мы можем убрать из класса Employee те переменные, которые уже определены в классе Person. Используем оба класса:
#include class Person < public: void print() const < std::cout std::string name; // имя unsigned age; // возраст >; class Employee : public Person < public: std::string company; // компания >; int main() < Person tom; tom.name = "Tom"; tom.age = 23; tom.print(); // Name: Tom Age: 23 Employee bob; bob.name = "Bob"; bob.age = 31; bob.company = "Microsoft"; bob.print(); // Name: Bob Age: 31 >
Таким образом, через переменную класса Employee мы можем обращаться ко всем открытым членам класса Person.
Конструкторы
Но теперь сделаем все переменные приватными, а для их инициализации добавим конструкторы. И тут стоит учитывать, что конструкторы при наследовании не наследуются . И если базовый класс содержит только конструкторы с параметрами, то производный класс должен вызывать в своем конструкторе один из конструкторов базового класса:
#include class Person < public: Person(std::string name, unsigned age) < this->name = name; this->age = age; > void print() const < std::cout private: std::string name; // имя unsigned age; // возраст >; class Employee: public Person < public: Employee(std::string name, unsigned age, std::string company): Person(name, age) < this->company = company; > private: std::string company; // компания >; int main() < Person person ; person.print(); // Name: Tom Age: 38 Employee employee ; employee.print(); // Name: Bob Age: 42 >
После списка параметров конструктора производного класса через двоеточие идет вызов конструктора базового класса, в который передаются значения параметров n и a.
Employee(std::string name, unsigned age, std::string company): Person(name, age) < this->company = company; >
Если бы мы не вызвали конструктор базового класса, то это было бы ошибкой.
Консольный вывод программы:
Name: Tom Age: 38 Name: Bob Age: 42
Таким образом, в строке
Employee employee ;
Вначале будет вызываться конструктор базового класса Person, в который будут передаваться значения "Bob" и 42. И таким образом будут установлены имя и возраст. Затем будет выполняться собственно конструктор Employee, который установит компанию.
Также мы могли бы определить конструктор Employee следующим образом, используя списки инициализации:
Employee(std::string name, unsigned age, std::string company): Person(name, age), company(company)
Подключение конструктора базового класса
В примерах выше конструктор Employee отличается от конструктора Person одним параметром - company. Все остальные параметры из Employee передаются в Person. Однако, если бы у нас было бы полное соответствие по параметрам между двумя классами, то мы могли бы и не определять отдельный конструктор для Employee, а подключить конструктор базового класса:
#include class Person < public: Person(std::string name, unsigned age) < this->name = name; this->age = age; > void print() const < std::cout private: std::string name; // имя unsigned age; // возраст >; class Employee: public Person < public: using Person::Person; // подключаем конструктор базового класса >; int main() < Person person ; person.print(); // Name: Tom Age: 38 Employee employee ; employee.print(); // Name: Bob Age: 42 >
Здесь в классе Employee подключаем конструктор базового класса с помощью ключевого слова using :
using Person::Person;
Таким образом, класс Employee фактически будет иметь тот же конструктор, что и Person с теми же двумя параметрами. И этот конструктор мы также можем вызвать для создания объекта Employee:
Employee employee ;
Определение конструкторов копирования
При определении конструктора копирования в производном классе следует вызывать в нем конструктор копирования базового класса. Например, добавим в классы Person и Employee конструкторы копирования:
#include class Person < public: // конструктор копирования класса Person Person(const Person& person) < name = person.name; age = person.age; >Person(std::string name, unsigned age) < this->name = name; this->age = age; > void print() const < std::cout private: std::string name; unsigned age; >; class Employee: public Person < public: Employee(std::string name, unsigned age, std::string company): Person(name, age) < this->company = company; > // конструктор копирования класса Employee // вызываем конструктор копирования базового класса Employee(const Employee& employee): Person(employee) < company=employee.company; >private: std::string company; >; int main() < Employee tom; Employee tomas; // вызываем конструктор копирования tomas.print(); // Name: Tom Age: 38 >
В конструкторе копирования производного класса Employee вызываем конструктор копирования базового класса Person:
Employee(const Employee& employee): Person(employee)
При этом в конструктор копирования Person передается объект employee, где будут установлены переменные name и age. В самом же конструкторе класса Employee лишь устанавливается переменная company.
Наследование деструкторов
Уничтожение объекта производного класса может вовлекать как собственно деструктор производного класса, так и деструктор базового класса. Например, определим в обоих классах деструкторы
#include class Person < public: Person(std::string name, unsigned age) < this->name = name; this->age = age; std::cout ~Person() < std::cout void print() const < std::cout private: std::string name; unsigned age; >; class Employee: public Person < public: Employee(std::string name, unsigned age, std::string company): Person(name, age) < this->company = company; std::cout ~Employee() < std::cout private: std::string company; >; int main() < Employee tom; tom.print(); >
В обоих классах деструктор просто выводит некоторое сообщение. В функции main создается один объект Employee, однако при завершении программы будет вызываться деструктор как из производного, так и из базового класса:
Person created Employee created Name: Tom Age: 38 Employee deleted Person deleted
По консольному выводу мы видим, что при создании объекта Employee сначала вызывается конструктор базового класса Person и затем собственно конструктор Employee. А при удалении объекта Employee процесс идет в обратном порядке - сначала вызывается деструктор производного класса и затем деструктор базового класса. Соответственно, если в деструкторе базового класса идет освобождение памяти, то оно в любом случае будет выполнено при удалении объекта производного класса.
Запрет наследования
Иногда наследование от класса может быть нежелательно. И с помощью спецификатора final мы можем запретить наследование:
class Person final < >;
После этого мы не сможем унаследовать другие классы от класса User. И, например, если мы попробуем написать, как в случае ниже, то мы столкнемся с ошибкой:
class Employee : public Person < >;
Наследование
Наследование является одной из главных особенностей объектно-ориентированного программирования. В С++ наследование поддерживается за счет того, что одному классу разрешается при своем объявлении включать в себя другой класс. Наследование позволяет построить иерархию классов от более общего к более частным. Этот процесс включает в себя определение базового класса, определяющего общие качества всех объектов, которые будут выведены затем из базового класса. Базовый класс представляет собой наиболее общее описание. Выведенные из базового класса классы обычно называют производными классами. Производные классы включают в себя все черты базового класса и, кроме того, добавляют новые качества, характерные именно для данного производного класса. Чтобы продемонстрировать, как работает этот процесс, в следующем примере созданы классы, классифицирующие различные типы средств передвижения.
В качестве начального рассмотрим класс, названный road_vehicle (дорожное средство передвижения), который служит очень широким определением средств передвижения по дорогам. Он хранит информацию о числе колес движущегося средства и о числе пассажиров, которые он может вмещать:
class road_vehicle int wheels;
int passengers;
public:
void set_wheels(int num);
int get_wheels();
void set_pass(int num);
int get_pass();
>;
Теперь можно использовать это определение дорожного средства передвижения для того, чтобы определить конкретные типы. Например, в следующем фрагменте кода определен класс truck (грузовик), используя класс road_vehicle:
class truck: public road_vehicle int cargo;
public:
void set_cargo(int size);
int get_cargo();
void show();
>;
Обратим внимание, каким образом наследуется класс road_vehicle. Общая форма записи наследования имеет следующий вид
class имя_нового_класса: доступ наследуемый_класс //тело нового класса
>
Здесь использование доступ факультативно, но если оно используется, то должно принимать значение public или private. Пока же все наследуемые классы будут использовать спецификатор public. Он означает, что все члены класса-предшественника, имеющие спецификатор доступа public, сохраняют тот же спецификатор доступа во всех производных классах. Поэтому в данном примере члены класса truck имеют доступ к функциям-членам класса road_vehicle так, как если бы эти функции-члены были объявлены внутри класса truck. Однако функции-члены класса truck не имеют доступа к частным членам класса road_vehicle.
Следующая программа иллюстрирует наследование с помощью создания двух подклассов класса road_vehicle: truck и automobile:
#include
class road_vehicle int wheels;
int passengers;
public:
void set_wheels(int num);
int get_wheels ();
void set_pass(int num);
int get_pass ();
>;
class truck: public road_vehicle int cargo;
public:
void set_cargo(int size);
int get_cargo();
void show ();
>;
enum type ;
class automobile: public road_vehicle enum type car_type;
public:
void set_type (enum type t);
enum type get_type();
void show();
>;
void road_vehicle::set_wheels(int num)
wheels = num;
>
int road_vehicle::get_wheels()
return wheels;
>
void road_vehicle::set_pass(int num)
passengers = num;
>
int road_vehicle::get_pass()
return passengers;
>
void truck::set_cargo(int num)
cargo = num;
>
int truck::get_cargo ()
return cargo;
>
void truck::show ()
cout cout cout >
void automobile::set_type(enum type t)
car_type = t;
>
enum type automobile::get_type()
return car_type;
>
void automobile::show( )
cout cout cout switch(get_type ()) case van: cout break;
case car; cout break;
case wagon: cout >
>
int main()
truck t1, t2;
automobile c;
t1.set_wheels (18);
t1.set_pass (2);
t1.set_cargo (3200);
t2.set_wheels (6);
t2.set_pass (3);
t2.set_cargo (1200);
t1.show ();
с.set_wheels(4);
с.set_pass(6);
с.set_type(van);
с.show();
return 0;
>
Как показывает эта программа, наибольшим достоинством наследования служит возможность создания базовой классификации, которая может быть затем включена в конкретные классы. Таким образом, каждый объект выражает в точности те свои черты, которые определяют его место в классификации.
Обратим внимание, что классы truck и automobile включают функции-члены с одинаковым именем show(), которые служат для вывода информации об объекте. Это еще один аспект полиморфизма. Поскольку каждая функция show() относится к своему собственному классу, компилятор может легко установить, какую из них вызывать в конкретной ситуации.
Наследование в C++: beginner, intermediate, advanced
В этой статье наследование описано на трех уровнях: beginner, intermediate и advanced. Expert нет. И ни слова про SOLID. Честно.
Beginner
Что такое наследование?
Наследование является одним из основополагающих принципов ООП. В соответствии с ним, класс может использовать переменные и методы другого класса как свои собственные.
Класс, который наследует данные, называется подклассом (subclass), производным классом (derived class) или дочерним классом (child). Класс, от которого наследуются данные или методы, называется суперклассом (super class), базовым классом (base class) или родительским классом (parent). Термины “родительский” и “дочерний” чрезвычайно полезны для понимания наследования. Как ребенок получает характеристики своих родителей, производный класс получает методы и переменные базового класса.
Наследование полезно, поскольку оно позволяет структурировать и повторно использовать код, что, в свою очередь, может значительно ускорить процесс разработки. Несмотря на это, наследование следует использовать с осторожностью, поскольку большинство изменений в суперклассе затронут все подклассы, что может привести к непредвиденным последствиям.
В этом примере, метод turn_on() и переменная serial_number не были объявлены или определены в подклассе Computer . Однако их можно использовать, поскольку они унаследованы от базового класса.
Важное примечание: приватные переменные и методы не могут быть унаследованы.
#include using namespace std; class Device < public: int serial_number = 12345678; void turn_on() < cout private: int pincode = 87654321; >; class Computer: public Device <>; int main() < Computer Computer_instance; Computer_instance.turn_on(); cout
Типы наследования
В C ++ есть несколько типов наследования:
- публичный ( public )- публичные ( public ) и защищенные ( protected ) данные наследуются без изменения уровня доступа к ним;
- защищенный ( protected ) — все унаследованные данные становятся защищенными;
- приватный ( private ) — все унаследованные данные становятся приватными.
Для базового класса Device , уровень доступа к данным не изменяется, но поскольку производный класс Computer наследует данные как приватные, данные становятся приватными для класса Computer .
#include using namespace std; class Device < public: int serial_number = 12345678; void turn_on() < cout >; class Computer: private Device < public: void say_hello() < turn_on(); cout >; int main() < Device Device_instance; Computer Computer_instance; coutКласс Computer теперь использует метод turn_on() как и любой приватный метод: turn_on() может быть вызван изнутри класса, но попытка вызвать его напрямую из main приведет к ошибке во время компиляции. Для базового класса Device , метод turn_on() остался публичным, и может быть вызван из main .
Конструкторы и деструкторы
В C ++ конструкторы и деструкторы не наследуются. Однако они вызываются, когда дочерний класс инициализирует свой объект. Конструкторы вызываются один за другим иерархически, начиная с базового класса и заканчивая последним производным классом. Деструкторы вызываются в обратном порядке.
Важное примечание: в этой статье не освещены виртуальные десктрукторы. Дополнительный материал на эту тему можно найти к примеру в этой статье на хабре.
#include using namespace std; class Device < public: // constructor Device() < cout // destructor ~Device() < cout >; class Computer: public Device < public: Computer() < cout ~Computer() < cout >; class Laptop: public Computer < public: Laptop() < cout ~Laptop() < cout >; int main()Конструкторы: Device -> Computer -> Laptop .
Деструкторы: Laptop -> Computer -> Device .Множественное наследование
Множественное наследование происходит, когда подкласс имеет два или более суперкласса. В этом примере, класс Laptop наследует и Monitor и Computer одновременно.
#include using namespace std; class Computer < public: void turn_on() < cout >; class Monitor < public: void show_image() < cout >; class Laptop: public Computer, public Monitor <>; int main()Проблематика множественного наследования
Множественное наследование требует тщательного проектирования, так как может привести к непредвиденным последствиям. Большинство таких последствий вызваны неоднозначностью в наследовании. В данном примере Laptop наследует метод turn_on() от обоих родителей и неясно какой метод должен быть вызван.
#include using namespace std; class Computer < private: void turn_on() < cout >; class Monitor < public: void turn_on() < cout >; class Laptop: public Computer, public Monitor <>; int main() < Laptop Laptop_instance; // Laptop_instance.turn_on(); // will cause compile time error return 0; >
Несмотря на то, что приватные данные не наследуются, разрешить неоднозначное наследование изменением уровня доступа к данным на приватный невозможно. При компиляции, сначала происходит поиск метода или переменной, а уже после — проверка уровня доступа к ним.
Intermediate
Проблема ромба
Проблема ромба (Diamond problem)- классическая проблема в языках, которые поддерживают возможность множественного наследования. Эта проблема возникает когда классы B и C наследуют A , а класс D наследует B и C .
К примеру, классы A , B и C определяют метод print_letter() . Если print_letter() будет вызываться классом D , неясно какой метод должен быть вызван — метод класса A , B или C . Разные языки по-разному подходят к решению ромбовидной проблем. В C ++ решение проблемы оставлено на усмотрение программиста.
Ромбовидная проблема — прежде всего проблема дизайна, и она должна быть предусмотрена на этапе проектирования. На этапе разработки ее можно разрешить следующим образом:
- вызвать метод конкретного суперкласса;
- обратиться к объекту подкласса как к объекту определенного суперкласса;
- переопределить проблематичный метод в последнем дочернем классе (в коде — turn_on() в подклассе Laptop ).
#include using namespace std; class Device < public: void turn_on() < cout >; class Computer: public Device <>; class Monitor: public Device <>; class Laptop: public Computer, public Monitor < /* public: void turn_on() < cout // uncommenting this function will resolve diamond problem */ >; int main() < Laptop Laptop_instance; // Laptop_instance.turn_on(); // will produce compile time error // if Laptop.turn_on function is commented out // calling method of specific superclass Laptop_instance.Monitor::turn_on(); // treating Laptop instance as Monitor instance via static cast static_cast( Laptop_instance ).turn_on(); return 0; >
Если метод turn_on() не был переопределен в Laptop, вызов Laptop_instance.turn_on() , приведет к ошибке при компиляции. Объект Laptop может получить доступ к двум определениям метода turn_on() одновременно: Device:Computer:Laptop.turn_on() и Device:Monitor:Laptop.turn_on() .
Проблема ромба: Конструкторы и деструкторы
Поскольку в С++ при инициализации объекта дочернего класса вызываются конструкторы всех родительских классов, возникает и другая проблема: конструктор базового класса Device будет вызван дважды.
#include using namespace std; class Device < public: Device() < cout >; class Computer: public Device < public: Computer() < cout >; class Monitor: public Device < public: Monitor() < cout >; class Laptop: public Computer, public Monitor <>; int main()
Виртуальное наследование
Виртуальное наследование (virtual inheritance) предотвращает появление множественных объектов базового класса в иерархии наследования. Таким образом, конструктор базового класса Device будет вызван только единожды, а обращение к методу turn_on() без его переопределения в дочернем классе не будет вызывать ошибку при компиляции.
#include using namespace std; class Device < public: Device() < cout void turn_on() < cout >; class Computer: virtual public Device < public: Computer() < cout >; class Monitor: virtual public Device < public: Monitor() < cout >; class Laptop: public Computer, public Monitor <>; int main()
Примечание: виртуальное наследование в классах Computer и Monitor не разрешит ромбовидное наследование если дочерний класс Laptop будет наследовать класс Device не виртуально ( class Laptop: public Computer, public Monitor, public Device <>; ).
Абстрактный класс
В С++, класс в котором существует хотя бы один чистый виртуальный метод (pure virtual) принято считать абстрактным. Если виртуальный метод не переопределен в дочернем классе, код не скомпилируется. Также, в С++ создать объект абстрактного класса невозможно — попытка тоже вызовет ошибку при компиляции.
#include using namespace std; class Device < public: void turn_on() < cout virtual void say_hello() = 0; >; class Laptop: public Device < public: void say_hello() < cout >; int main() < Laptop Laptop_instance; Laptop_instance.turn_on(); Laptop_instance.say_hello(); // Device Device_instance; // will cause compile time error return 0; >
Интерфейс
С++, в отличии от некоторых ООП языков, не предоставляет отдельного ключевого слова для обозначения интерфейса (interface). Тем не менее, реализация интерфейса возможна путем создания чистого абстрактного класса (pure abstract class) — класса в котором присутствуют только декларации методов. Такие классы также часто называют абстрактными базовыми классами (Abstract Base Class — ABC).
#include using namespace std; class Device < public: virtual void turn_on() = 0; >; class Laptop: public Device < public: void turn_on() < cout >; int main() < Laptop Laptop_instance; Laptop_instance.turn_on(); // Device Device_instance; // will cause compile time error return 0; >
Advanced
Несмотря на то, что наследование — фундаментальный принцип ООП, его стоит использовать с осторожностью. Важно думать о том, что любой код который будет использоваться скорее всего будет изменен и может быть использован неочевидным для разработчика путем.
Наследование от реализованного или частично реализованного класса
Если наследование происходит не от интерфейса (чистого абстрактного класса в контексте С++), а от класса в котором присутствуют какие-либо реализации, стоит учитывать то, что класс наследник связан с родительским классом наиболее тесной из возможных связью. Большинство изменений в классе родителя могут затронуть наследника что может привести к непредвиденному поведению. Такие изменения в поведении наследника не всегда очевидны — ошибка может возникнуть в уже оттестированом и рабочем коде. Данная ситуация усугубляется наличием сложной иерархии классов. Всегда стоит помнить о том, что код может изменяться не только человеком который его написал, и пути наследования очевидные для автора могут быть не учтены его коллегами.
В противовес этому стоит заметить что наследование от частично реализованных классов имеет неоспоримое преимущество. Библиотеки и фреймворки зачастую работают следующим образом: они предоставляют пользователю абстрактный класс с несколькими виртуальными и множеством реализованных методов. Таким образом, наибольшее количество работы уже проделано — сложная логика уже написана, а пользователю остается только кастомизировать готовое решение под свои нужды.
Интерфейс
Наследование от интерфейса (чистого абстрактного класса) преподносит наследование как возможность структурирования кода и защиту пользователя. Так как интерфейс описывает какую работу будет выполнять класс-реализация, но не описывает как именно, любой пользователь интерфейса огражден от изменений в классе который реализует этот интерфейс.
Интерфейс: Пример использования
Прежде всего стоит заметить, что пример тесно связан с понятием полиморфизма, но будет рассмотрен в контексте наследования от чистого абстрактного класса.
Приложение выполняющее абстрактную бизнес логику должно настраиваться из отдельного конфигурационного файла. На раннем этапе разработки, форматирование данного конфигурационного файла до конца сформировано не было. Вынесение парсинга файла за интерфейс предоставляет несколько преимуществ.
Отсутствие однозначности касательно форматирования конфигурационного файла не тормозит процесс разработки основной программы. Два разработчика могут работать параллельно — один над бизнес логикой, а другой над парсером. Поскольку они взаимодействуют через этот интерфейс, каждый из них может работать независимо. Данный подход облегчает покрытие кода юнит тестами, так как необходимые тесты могут быть написаны с использованием мока (mock) для этого интерфейса.
Также, при изменении формата конфигурационного файла, бизнес логика приложения не затрагивается. Единственное чего требует полный переход от одного форматирования к другому — написания новой реализации уже существующего абстрактного класса (класса-парсера). В дальнейшем, возврат к изначальному формату файла требует минимальной работы — подмены одного уже существующего парсера другим.
Заключение
Наследование предоставляет множество преимуществ, но должно быть тщательно спроектировано во избежание проблем, возможность для которых оно открывает. В контексте наследования, С++ предоставляет широкий спектр инструментов который открывает массу возможностей для программиста.