Особенности наследования классов в C++
Необходимость использования в программе множественного наследования чаще всего говорит о плохой структуризации классов. Это могло возникнуть в том случае, если программист уделил недостаточно времени для предварителього анализа предметной области программы на самом первом этапе ее составления. Структурный подход к объектно-ориентированному проектированию предполагает более четкую организацию… Читать ещё >
Особенности наследования классов в C++ (реферат, курсовая, диплом, контрольная)
1. Абстрактные классы
2. Множественное наследование
3. Адреса базовых классов
4. Виртуальное наследование Заключение Список литературы
Цель объектно-ориентированного программирования состоит в повторном использовании созданных вами классов, что экономит ваше время и силы. Если вы уже создали некоторый класс, то возможны ситуации, что новому классу нужны многие или даже все особенности уже существующего класса, и необходимо добавить один или несколько элементов данных или функций. В таких случаях C++ позволяет вам строить новый объект, используя характеристики уже существующего объекта. Другими словами, новый объект будет наследовать элементы существующего класса (называемого базовым классом). Когда вы строите новый класс из существующего, этот новый класс часто называется производным классом. В этом уроке впервые вводится наследование классов в C++.
Ели ваши программы используют наследование, то для порождения нового класса необходим базовый класс, т. е. новый класс наследует элементы базового класса.
Для инициализации элементов производного класса ваша программа должна вызвать конструкторы базового и производного классов. Используя оператор точку, программы могут легко обращаться к элементам базового и производного классов.
Для разрешения конфликта имен между элементами базового и производного классов ваша программа может использовать оператор глобального разрешения, указывая перед ним имя базового или производного класса.
Наследование является фундаментальной концепцией объектно-ориентированного программирования. Выберите время для экспериментов с программами, представленными в этом уроке. И вы обнаружите, что реально наследование реализуется очень просто и может сохранить огромные усилия, затрачиваемые на программирование.
1. Абстрактные классы
Абстрактным называется класс, который содержит как минимум одну чистую виртуальную компонентную функцию. Чистая виртуальная функция — это виртуальная функция, для которой программист не планирует в текущей версии программы иметь каких-либо реализаций. Объявление такой функции может иметь, например, следующий вид:
class Employee { // Класс «Служащий»
private:
char name[40]; // Имя служащего
public:
Employee (char* n);
//Чистая виртуальная функция
virtual void* promote ()=0;
Абстрактный класс не может быть реализован в объекте. Так, следующая строка:
Employee s («My name»);
вызовет ошибку, о которой сообщит компилятор.
Однако программа может объявить указатель абстрактного класса, так что следующая строка вполне допустима:
Employee* sPtr;
Компоненты абстрактного класса могут наследоваться. Если все чистые виртуальные методы класса перегружены правилами, не являющимися чистыми и виртуальными, класс может не являться абстрактным. В следующем примере класс Secretary может быть реализован объектом, поскольку функция promote была перегружена и имеет теперь другой смысл.
class Secretary: public Employee {
private:
int moreData;
public:
Secretary (char* n): Employee (n) { }
virtual void* promote ();
};
Secretary sec («Another Name»);
Адрес объекта Secretary может быть передан в функцию, которая ожидает передачи Employee:
void fn (Employee*);
Secretary sec («Another Name»);
fn (&sec);
Абстрактные классы полезны для организации иерархической структуры классов. Например, может быть известно, что все служащие должны принадлежать к подклассу Employee. Поскольку разные служащие имеют разные циклы продвижения по службе (promotion), каждый подкласс должен иметь собственный метод promote (). Объявив promote () как чистую виртуальную функцию, разработчик класса Employee требует тем самым, чтобы разработчик подкласса написал метод promote () до реализации любого производного класса от Employee.
2. Множественное наследование
Наследование представляет собой способность производного класса наследовать характеристики существующего базового класса. Простыми словами это означает, что, если у вас есть класс, чьи элементы данных или функции-элементы могут быть использованы новым классом, вы можете построить этот новый класс в терминах существующего (или базового) класса. Новый класс в свою очередь будет наследовать элементы (характеристики) существующего класса. Использование наследования для построения новых классов сэкономит вам значительное время и силы на программирование. Объектно-ориентированное программирование широко использует наследование, позволяя вашей программе строить сложные объекты из небольших легко управляемых объектов.
В C++ класс может наследовать свойства более чем одного класса. Формат определения наследования классом свойств нескольких базовых классов аналогичен формату определения наследования свойств отдельного класса. Приведем пример.
class SubClass: public Base1, private Base2 {
// остальная часть определения класса
}
В определении может быть перечислено любое число базовых классов через запятую. Ни один базовый класс не может быть прямо унаследован более одного раза. Каждый базовый класс может быть унаследован как public или как private; умолчанием является private.
Когда класс мог наследовать свойства только одного единственного класса, последовательность выполнения конструкторов не являлась жестко заданной. С переходом к множественному наследованию порядок выполнения конструкторов становится очень важен. Этот порядок вызова следующий:
(1) конструкторы всех виртуальных базовых классов; если их имеется более одного, то конструкторы вызываются в порядке их наследования;
(2) конструкторы невиртуальных базовых классов в порядке их наследования;
(3) конструкторы всех компонентных классов.
Рассмотрим следующий пример.
#include
struct Base1 {
Base1() { cout<< «Создание Base1» <
};
struct Base2 {
Base2() { cout<< «Создание Base2» <
};
struct Base3 {
Base3() { cout<< «Создание Base3» <
};
struct Base4 {
Base4() { cout<< «Создание Base4» <
};
struct Derived: private Base1, private Base2, private Base3 {
Base4 anObject;
Derived () {}
};
void main () {
Derived anObject;
}
На выходе этой программы будет следующее:
Создание Base1
Создание Base2
Создание Base3
Создание Base4
Добавление в конструктор для Derived конкретных вызовов с другим порядком и повторение программы не изменит сообщения после выполнения этой программы.
struct Derived: private Base1, private Base2, private Base3 {
Base4 anObject;
Derived (): anObject (), Base3(), Base2(), Base1() {}
};
Изменение порядка наследования классов изменит последовательность появления сообщений на выходе программы. Объявив Derived следующим образом:
struct Derived: private Base3, private Base2, private Base1 {
Base4 anObject;
Derived (): anObject (), Base1(), Base2(), Base3() {}
};
можно получить на выходе программы:
Создание Base3
Создание Base2
Создание Base1
Создание Base4
Последовательность вызова деструкторов будет обратной относительно последовательности вызова конструкторов.
Необходимость использования в программе множественного наследования чаще всего говорит о плохой структуризации классов. Это могло возникнуть в том случае, если программист уделил недостаточно времени для предварителього анализа предметной области программы на самом первом этапе ее составления. Структурный подход к объектно-ориентированному проектированию предполагает более четкую организацию классов.
3. Адреса базовых классов
Большое число проблем при проектировании объектно-ориентированных программ возникает в связи со способом размещения производных классов в памяти. Одна из ключевых причин того, что указатель подкласса может передаваться как указатель суперкласса, состоит в том, что когда-то суперкласс впервые появляется в подклассе. Рассмотрим простой пример наследования в классах.
struct Base {
int a;
float b;
void f1();
struct Derived: public Base {
int c;
} object;
Рассматривая размещение объекта производного класса Derived в памяти, например, с помощью команды отладчика Inspect, можно получить несколько упрощенную графическую диаграмму, которая приведена на рис. 1.
Base*
Derived* Base
Derived
Рис. 1. Схема размещения в памяти простого производного класса Если Derived* передан в функцию, ожидающую получения Base*, никаких проблем не возникает. Класс Derived совпадает с классом Base во всем, что касается его части, перекрывающейся с Base. То же самое касается и вызова функции object. f1(). Передаваемый указатель this имеет одно и то же значение безотносительно к типу. Однако в отношении класса Derived с множественным наследованием сказанное ранее будет несправедливо.
Рассмотрим пример.
программный наследование класс
struct Base1 {
int a; float b;
void f1();
};
struct Base2 {
int c;
float d;
void f2();
};
struct Derived: public Base1, public Base2 {
int e;
} object;
Примерная схема памяти для этого случая показана на рис. 2.
Класс Base2 более не находится в начале класса Derived. Если попробовать передать Derived* функции, ожидающей поступления Base1*, то проблем не возникнет. Вместе с тем, при вызове функции, ожидающей поступления Base2*, полученный ей адрес окажется неправильным.
Чтобы исправить этот адрес, необходимо прибавить к адресу Derived: object смещение Base2 в Derived, таким образом, чтобы результат указывал на ту часть, которая относится к Base2. Такая же коррекция должна выполняться для каждого случая приведения типа указателей из Derived* в Base2*, включая и скрытый указатель this, передаваемый компонентным функциям Base2.
Base1*
Derived*
Base 1
Base2* Derived
Base 2
Рис. 2. Схема размещения в памяти класса с множественным наследованием
Derived object;
object.f2() // Перед передачей в f2() адрес объекта object
// должен быть соответственно скорректирован По тем же причинам C++ также должен выполнить коррекцию и при обратном приведении типа из Base2* в Derived*.
4. Виртуальное наследование
В следующем примере класс Derived наследует свойства двух копий класса Base: одной через класс FirstBase, а второй — через SecondBase:
struct Base {
int object;
};
struct FirstBase: public Base {
int a;
};
struct SecondBase: public Base {
float b;
class Derived: public FirstBase, public SecondBase {
long dObject;
};
Примерная схема памяти для объекта класса Derived показана на рис. 3.
Base
FirstBase
Base Derived
SecondBase
Рис. 3. Схема размещения в памяти множественного производного класса Чтобы позволить наследование в таких случаях одной и той же копии Base, в C++ необходимо включить в команду наследования ключевое слово virtual. В этом случае, программу можно переписать следующим образом:
struct Base {
int object;
};
struct FirstBase: virtual public Base {
int a;
};
struct SecondBase: virtual public Base {
float b;
class Derived: virtual public FirstBase,
virtual public SecondBase {
long dObject;
};
Такая модификация программы изменит схему размещения объекта класса Derived, как показано на рис. 4.
int object Base
int a FirstBase
float b SecondBase Derived
long
dObject
Рис. 4. Схема размещения в памяти виртуального множественного производного класса Теперь существует всего одна копия класса Base.
Заключение
Наследование в C++ позволяет вам строить /порождать) новый класс из существующего класса. Строя таким способом один класс из другого, вы уменьшаете объем программирования, что, в свою очередь, экономит ваше время. C++ позволяет вам порождать класс из двух или нескольких базовых классов. Использование нескольких базовых классов для порождения класса представляет собой множественное наследование. В заключении можно сделать следующие выводы:
1. Наследование представляет собой способность производить новый класс из существующего базового класса.
2. Производный класс — это новый класс, а базовый класс — существующий класс.
3. Когда вы порождаете один класс из другого (базового класса), производный класс наследует элементы базового класса.
4. Для порождения класса из базового начинайте определение производного класса ключевым словом class, за которым следует имя класса, двоеточие и имя базового класса, например class dalmatian: dog.
5. Когда вы порождаете класс из базового класса, производный класс может обращаться к общим элементам базового класса, как будто эти элементы определены внутри самого производного класса. Для доступа к частным данным базового класса производный класс должен использовать интерфейсные функции базового класса.
6. Внутри конструктора производного класса ваша программа должна вызвать конструктор базового класса, указывая двоеточие, имя конструктора базового класса и соответствующие параметры сразу же после заголовка конструктора производного класса.
7. Чтобы обеспечить производным классам прямой доступ к определенным элементам базового класса, в то же время защищая эти элементы от оставшейся части программы, C++ обеспечивает защищенные {protected) элементы класса. Производный класс может обращаться к защищенным элементам базового класса, как будто они являются общими. Однако для оставшейся части программы защищенные элементы эквивалентны частным.
8. Если в производном и базовом классе есть элементы с одинаковым именем, то внутри функций производного класса C++ будет использовать элементы производного класса. Если функциям производного класса необходимо обратиться к элементу базового класса, вы должны использовать оператор глобального разрешения, например base class: member.
1) Т. Сван. Освоение Borland C++ 4.5: Пер. с англ. — Киев: Диалектика, 1996. 544с.
2) Г. Шилдт. Самоучитель C++: Пер. с англ. — Санкт-Петербург: BHV-Санкт-Петербург, 1998. 620с.
3) У. Сэвитч. C++ в примерах: Пер. с англ. — Москва: ЭКОМ, 1997. 736с.
4) К. Джамса. Учимся программировать на языке C++: Пер. с англ. — Москва: Мир, 1997. 320с.
5) В. А. Скляров. Язык C++ и объектно-ориентированное программирование: Справочное издание. — Минск: Вышэйшая школа, 1997. 480с.
6) Х. Дейтел, П. Дейтел. Как программировать на C++: Пер. с англ. — Москва: ЗАО «Издательство БИНОМ», 1998. 1024с.