Наследование в 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) для этого интерфейса.
Также, при изменении формата конфигурационного файла, бизнес логика приложения не затрагивается. Единственное чего требует полный переход от одного форматирования к другому — написания новой реализации уже существующего абстрактного класса (класса-парсера). В дальнейшем, возврат к изначальному формату файла требует минимальной работы — подмены одного уже существующего парсера другим.
Заключение
Наследование предоставляет множество преимуществ, но должно быть тщательно спроектировано во избежание проблем, возможность для которых оно открывает. В контексте наследования, С++ предоставляет широкий спектр инструментов который открывает массу возможностей для программиста.
Как наследовать класс в 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 < >;
Объектно-ориентированное программирование
Наследование (inheritance) является одним из ключевых моментов ООП. Благодаря наследованию один класс может унаследовать функциональность другого класса.
Пусть у нас есть следующий класс Person, который описывает отдельного человека:
class Person < private string _name = ""; public string Name < get < return _name; >set < _name = value; >> public void Print() < Console.WriteLine(Name); >>
Но вдруг нам потребовался класс, описывающий сотрудника предприятия - класс Employee. Поскольку этот класс будет реализовывать тот же функционал, что и класс Person, так как сотрудник - это также и человек, то было бы рационально сделать класс Employee производным (или наследником, или подклассом) от класса Person, который, в свою очередь, называется базовым классом или родителем (или суперклассом):
class Employee : Person
После двоеточия мы указываем базовый класс для данного класса. Для класса Employee базовым является Person, и поэтому класс Employee наследует все те же свойства, методы, поля, которые есть в классе Person. Единственное, что не передается при наследовании, это конструкторы базового класса с параметрами.
Таким образом, наследование реализует отношение is-a (является), объект класса Employee также является объектом класса Person:
Person person = new Person < Name = "Tom" >; person.Print(); // Tom person = new Employee < Name = "Sam" >; person.Print(); // Sam
И поскольку объект Employee является также и объектом Person, то мы можем так определить переменную: Person p = new Employee() .
По умолчанию все классы наследуются от базового класса Object , даже если мы явным образом не устанавливаем наследование. Поэтому выше определенные классы Person и Employee кроме своих собственных методов, также будут иметь и методы класса Object: ToString(), Equals(), GetHashCode() и GetType().
Все классы по умолчанию могут наследоваться. Однако здесь есть ряд ограничений:
- Не поддерживается множественное наследование, класс может наследоваться только от одного класса.
- При создании производного класса надо учитывать тип доступа к базовому классу - тип доступа к производному классу должен быть таким же, как и у базового класса, или более строгим. То есть, если базовый класс у нас имеет тип доступа internal , то производный класс может иметь тип доступа internal или private , но не public . Однако следует также учитывать, что если базовый и производный класс находятся в разных сборках (проектах), то в этом случае производый класс может наследовать только от класса, который имеет модификатор public.
- Если класс объявлен с модификатором sealed , то от этого класса нельзя наследовать и создавать производные классы. Например, следующий класс не допускает создание наследников:
sealed class Admin
Доступ к членам базового класса из класса-наследника
Вернемся к нашим классам Person и Employee. Хотя Employee наследует весь функционал от класса Person, посмотрим, что будет в следующем случае:
class Employee : Person < public void PrintName() < Console.WriteLine(_name); >>
Этот код не сработает и выдаст ошибку, так как переменная _name объявлена с модификатором private и поэтому к ней доступ имеет только класс Person . Но зато в классе Person определено общедоступное свойство Name, которое мы можем использовать, поэтому следующий код у нас будет работать нормально:
class Employee : Person < public void PrintName() < Console.WriteLine(Name); >>
Таким образом, производный класс может иметь доступ только к тем членам базового класса, которые определены с модификаторами private protected (если базовый и производный класс находятся в одной сборке), public , internal (если базовый и производный класс находятся в одной сборке), protected и protected internal .
Ключевое слово base
Теперь добавим в наши классы конструкторы:
class Person < public string Name < get; set;>public Person(string name) < Name = name; >public void Print() < Console.WriteLine(Name); >> class Employee : Person < public string Company < get; set; >public Employee(string name, string company) : base(name) < Company = company; >>
Класс Person имеет конструктор, который устанавливает свойство Name. Поскольку класс Employee наследует и устанавливает то же свойство Name, то логично было бы не писать по сто раз код установки, а как-то вызвать соответствующий код класса Person. К тому же свойств, которые надо установить в конструкторе базового класса, и параметров может быть гораздо больше.
С помощью ключевого слова base мы можем обратиться к базовому классу. В нашем случае в конструкторе класса Employee нам надо установить имя и компанию. Но имя мы передаем на установку в конструктор базового класса, то есть в конструктор класса Person, с помощью выражения base(name) .
Person person = new Person("Bob"); person.Print(); // Bob Employee employee = new Employee("Tom", "Microsoft"); employee.Print(); // Tom
Конструкторы в производных классах
Конструкторы не передаются производному классу при наследовании. И если в базовом классе не определен конструктор по умолчанию без параметров, а только конструкторы с параметрами (как в случае с базовым классом Person), то в производном классе мы обязательно должны вызвать один из этих конструкторов через ключевое слово base. Например, из класса Employee уберем определение конструктора:
class Employee : Person < public string Company < get; set; >= ""; >
В данном случае мы получим ошибку, так как класс Employee не соответствует классу Person, а именно не вызывает конструктор базового класса. Даже если бы мы добавили какой-нибудь конструктор, который бы устанавливал все те же свойства, то мы все равно бы получили ошибку:
class Employee : Person < public string Company < get; set; >= ""; public Employee(string name, string company) // ! Ошибка < Name = name; Company = company; >>
То есть в классе Employee через ключевое слово base надо явным образом вызвать конструктор класса Person:
class Employee : Person < public string Company < get; set; >= ""; public Employee(string name, string company) : base(name) < Company = company; >>
Либо в качестве альтернативы мы могли бы определить в базовом классе конструктор без параметров:
class Person < public string Name < get; set; >// конструктор без параметров public Person() < Name = "Tom"; Console.WriteLine("Вызов конструктора без параметров"); >public Person(string name) < Name = name; >public void Print() < Console.WriteLine(Name); >>
Тогда в любом конструкторе производного класса, где нет обращения конструктору базового класса, все равно неявно вызывался бы этот конструктор по умолчанию. Например, следующий конструктор
public Employee(string company)
Фактически был бы эквивалентен следующему конструктору:
public Employee(string company) :base()
Порядок вызова конструкторов
При вызове конструктора класса сначала отрабатывают конструкторы базовых классов и только затем конструкторы производных. Например, возьмем следующие классы:
class Person < string name; int age; public Person(string name) < this.name = name; Console.WriteLine("Person(string name)"); >public Person(string name, int age) : this(name) < this.age = age; Console.WriteLine("Person(string name, int age)"); >> class Employee : Person < string company; public Employee(string name, int age, string company) : base(name, age) < this.company = company; Console.WriteLine("Employee(string name, int age, string company)"); >>
При создании объекта Employee:
Employee tom = new Employee("Tom", 22, "Microsoft");
Мы получим следующий консольный вывод:
Person(string name) Person(string name, int age) Employee(string name, int age, string company)
В итоге мы получаем следующую цепь выполнений.
- Вначале вызывается конструктор Employee(string name, int age, string company) . Он делегирует выполнение конструктору Person(string name, int age)
- Вызывается конструктор Person(string name, int age) , который сам пока не выполняется и передает выполнение конструктору Person(string name)
- Вызывается конструктор Person(string name) , который передает выполнение конструктору класса System.Object, так как это базовый по умолчанию класс для Person.
- Выполняется конструктор System.Object.Object() , затем выполнение возвращается конструктору Person(string name)
- Выполняется тело конструктора Person(string name) , затем выполнение возвращается конструктору Person(string name, int age)
- Выполняется тело конструктора Person(string name, int age) , затем выполнение возвращается конструктору Employee(string name, int age, string company)
- Выполняется тело конструктора Employee(string name, int age, string company) . В итоге создается объект Employee
Наследование (C++)
В этом разделе рассматривается использование производных классов для создания расширяемых программ.
Обзор
Новые классы могут быть производными от существующих классов с помощью механизма с именем "наследование" (см. сведения, начинающиеся в одном наследовании). Классы, используемые для наследования, называются "базовыми классами" определенного производного класса. Производный класс объявляется с помощью следующего синтаксиса:
class Derived : [virtual] [access-specifier] Base < // member list >; class Derived : [virtual] [access-specifier] Base1, [virtual] [access-specifier] Base2, . . . < // member list >;
После тега (имени) класса следует двоеточие и список базовых спецификаций. Названные таким образом базовые классы, вероятно, были объявлены ранее. Базовые спецификации могут содержать описатель доступа, который является одним из ключевое слово public или protected private . Эти описатели доступа отображаются перед именем базового класса и применяются только к базовому классу. Эти описатели контролируют разрешение производного класса на использование членов базового класса. Сведения о доступе к элементам базового класса см. в разделе "Член-контроль доступа". Если описатель доступа опущен, считается, что доступ к этой базе считается private . Базовые спецификации могут содержать ключевое слово virtual для указания виртуального наследования. Это ключевое слово может отображаться до или после описателя доступа, если таковые имеются. Если используется виртуальное наследование, базовый класс называется виртуальным базовым классом.
Можно определить несколько базовых классов, разделив их запятыми. Если указан один базовый класс, модель наследования является одним наследованием. Если указано несколько базовых классов, модель наследования называется множественным наследованием.
В этой статье содержатся следующие разделы:
- Одно наследование
- Несколько базовых классов
- Виртуальные функции
- Явные переопределения
- Абстрактные классы
- Сводка правил область