Лекции_ООП_ИС. Курс лекций по объектноориентированному программированию
Скачать 0.58 Mb.
|
имя_класса(); |
Доступ в базовом классе | Тип доступа при наследовании | Доступ в производном классе | Возможность восстановления доступа |
public | public | public | не требуется |
private | нет доступа | нет | |
protected | protected | не требуется | |
| |||
public | protected | protected | да |
private | нет доступа | нет | |
protected | protected | да | |
| |||
public | Private | private | да |
private | нет доступа | нет | |
protected | private | да |
Объявления доступа дают возможность сделать опять защищенными или открытыми защищенные или открытые члены закрытого базового класса в производном классе соответственно.
Объявления доступа позволяют сделать снова открытыми открытые члены базового класса в защищенном производном. (Ни этот, ни предыдущий пункт не выполняется, если в производном классе объявлены члены с теми же именами, что и в базовом классе).
Для перегруженных функций позволяют вернуть им первоначальное ограничение доступа, которое они имели в базовом классе, если все они имели одинаковое ограничение доступа.
У деструктора не может быть параметров (даже типа void), и деструктор не имеет возможности возвращать какой-либо результат, даже типа void. Статус доступа деструктора по умолчанию public (т.е. деструктор доступен во всей области действия определения класса). В несложных классах деструктор обычно определяется по умолчанию.
Деструкторы не наследуются, поэтому даже при отсутствии в производном классе деструктора, они не передается из базового, а формируется компилятором по умолчанию со статусом доступа public. Этот деструктор вызывает деструкторы базовых классов.
В любом классе могут быть в качестве компонентов определены другие классы. В этих классах будут свои деструкторы, которые при уничтожении объекта охватывающего (внешнего) класса выполняются после деструктора охватывающего класса.
Деструкторы базовых классов выполняются в порядке, обратном перечислению классов в определении производного класса. Таким образом, порядок уничтожения объекта противоположен по отношению к порядку его конструирования.
Вызовы деструкторов для объектов класса и для базовых классов выполняются неявно и не требуют никаких действий программиста. Однако вызов деструктора того класса, объект которого уничтожается в соответствии с логикой выполнения программы, может быть явным. Это может быть, например, случай, когда при создании объекта для него явно выделялась память.
Множественное наследование и виртуальные базовые классы. Класс называют непосредственным (прямым) базовым классом(прямой базой), если он входит в список базовых при определении класса. В то же время для производного класса могут существовать косвенные или непрямые предшественники, которые служат базовыми для классов, входящих в список базовых. Если некоторый класс А является базовым для В, а В есть базовый класс для С, то класс В является непосредственным базовым классом для С, а класс А является непрямым базовым классом для С. Обращение к компоненту ха, входящему в А и унаследованному последовательно классами В и С, можно обозначить в классе С, либо как А::ха, либо как В::ха. Обе конструкции обеспечивают обращение к элементу ха класса А.
Производные классы принято изображать ниже базовых. Именно в таком порядке их тексты размещаются в листинге программы и рассматриваются компилятором. Класс может иметь несколько непосредственных базовых классов, т.е. может быть порожден из любого числа базовых классов, например,
class X1 { ... };
class X2 { ... };
class X3 { ... };
class Y1: public X1, public X2, public X3 { ... };
Наличие нескольких прямых базовых классов называют множественным наследованием.
Определения базовых классов должны предшествовать их использованию в качестве базовых. При множественном наследовании никакой класс не может больше одного раза использоваться в качестве непосредственного базового. Однако класс может больше одного раза быть непрямым базовым классом:
class X { ...; f () ; ... };
class Y: public X { ... };
class Z: public X { ... };
class D: public Y, public Z { ... };
В данном примере класс Х дважды опосредовано наследуется классом D.
Проиллюстрированное дублирование класса соответствует включению в производный объект нескольких объектов базового класса. В нашем примере существуют два объекта класса Х, и поэтому для устранения возможных неоднозначностей вне объектов класса D нужно обращаться к конкретному компоненту класса Х, используя полную квалификацию: D::Y::X::f() или D::Z::X::f(). Внутри объекта класса D обращения упрощаются Y::X::f() или Z::X::f(), но тоже содержат квалификацию.
Чтобы устранить дублирование объектов непрямого базового класса при множественном наследовании, этот базовый класс объявляют виртуальным. Для этого в списке базовых классов перед именем класса необходимо поместить ключевое слово virtual. Например, класс Х будет виртуальным базовым классом при таком описании:
class X { ... f(); ... };
class Y: virtual public X { ... };
class Z: virtual public X { ... };
class D: public Y, public Z { ... };
Теперь класс D будет включать только один экземпляр Х, доступ к которому равноправно имеют классы Y и Z.
Обратите внимание, что размеры производных классов при отсутствии виртуальных базовых равны сумме длин их компонентов и длин унаследованных базовых классов. «Накладные расходы» памяти здесь отсутствуют.
При множественном наследовании один и тот же базовый класс может быть включен в производный класс одновременно несколько раз, причем и как виртуальный, и как не виртуальный.
class X { ... };
class Y: virtual public X { ... };
class Z: virtual public X { ... };
class B: virtual public X { ... };
class C: virtual public X { ... };
class E: public X { ... };
class D: public X { ... };
class A: public D,public B,public Y,public Z,public C,public E { ... };
В данном примере объект класса А включает три экземпляра объектов класса Х: один виртуальный, совместно используемый классами B, Y, C, Z, и два не виртуальных относящихся соответственно к классам D и E. Таким образом, можно констатировать, что виртуальность класса в иерархии производных классов является не свойством класса как такового, а результатом особенностей процедуры наследования.
Возможны и другие комбинации виртуальных и не виртуальных базовых классов. Например:
class BB { ... };
class AA: virtual public BB { ... };
class CC: virtual public BB { ... };
class DD: public AA, public CC, public virtual BB { ... };
При использовании наследования и множественного наследования могут возникать неоднозначности при доступе к одноименным компонентам разных базовых классов. Простейший и самый надежный способ устранения неоднозначностей - использование квалифицированных имен компонентов. Как обычно, для квалификации имени компонента используется имя класса. Следующий пример иллюстрирует упомянутую неоднозначность и ее разрешение с помощью квалификационных имен компонентов:
class X { public: int d; ... };
class Y { public: int d; ... };
class Z: public X, public Y
{
public:
int d;
...
d=X::d + Y::d;
...
};
Виртуальные функции и абстрактные классы. К механизму виртуальных функций обращаются в тех случаях, когда в базовый класс необходимо поместить функцию, которая должна по-разному выполняться в производных классах. Точнее, по-разному должна выполняться не единственная функция из базового класса, а в каждом производственном классе требуется свой вариант этой функции.
Например, базовый класс может описывать фигуру на экране без конкретизации ее вида, а производные классы (треугольник, эллипс и т.п.) однозначно определяют ее формы и размеры. Если в базовом классе ввести функцию для изображения фигуры на экране, то выполнение этой функции будет возможно только для объектов каждого из производных классов, определяющих конкретные изображения.
До объяснения возможностей виртуальных функций отметим, что классы, включающие такие функции, играют особую роль в объектно-ориентированном программировании. Именно поэтому они носят специальное название - полиморфные.
Рассмотрим теперь, как ведут себя при наследовании не виртуальные компонентные функции с одинаковыми именами, типами и сигнатурами параметров.
Если в базовом классе определена некоторая компонентная функция, то такая же функция (с тем же именем, того же типа и с тем же набором и типами параметров) может быть введена в производном классе. Рассмотрим следующее определение классов:
class base
{
public:
void funс (int i)
{ printf("\nbase::i =",i); };
};
class derived: public base
{
public:
void funс (int i)
{ printf("\nder::i =",i); };
};
В данном случае внешне одинаковые функции void func (int)определены в базовом классе baseи в производном классе derived.
В теле класса derivedобращение к функции func(), принадлежащей классу base, может быть выполнено с помощью полного квалифицированного имени, явно включающего имя базового класса: base::func(). При обращении в классе derivedк такой же (по внешнему виду) функции, принадлежащей классу derived, достаточно использовать имя func() без предшествующего квалификатора.
В программе, где определены и доступы оба класса baseи derived, обращения к функциям func()могут быть выполнены с помощью указателей на объекты соответствующих классов:
void main(void) {
base B, *bp = &B;
derived D, *dp = &D;
base *pbd = &D;
bp->func(1); // Печатает : base::i = 1
dp->func(5); // Печатает : der::i = 5
pbd->func(4); // Печатает : base::i = 4
};
В программе введены три указателя на объекты разных классов. Следует обратить внимание на инициализацию указателя pbd. В ней адрес объекта производного класса (объекта D) присваивается указателю на объект его прямого базового класса (base *). При этом выполняется стандартное преобразование указателей, предусмотренное синтаксисом языка Си++. Обратное образование, т.е. преобразование указателя на объект базового класса в указатель на объект производного класса, невозможно (запрещено синтаксисом). Обращения к функциям классов baseи derived с помощью указателей bpи dp не представляют особого интереса. Вызовpbd->func()требуется прокомментировать. Указатель pbd имеет тип base*, однако его значение - адрес объекта D класса derived.
Какая же из функций base::func()или derived::func() вызывает при обращении pbd->func()? Результат выполнения программы показывает, что вызывается функция из базового класса. Именно такой вызов предусмотрен синтаксисом языка Си++, т.е. выбор функции (не виртуальной) зависит только от типа указателя, но не от его значения. «Настроив» указатель базового класса на объект производного класса, не удается с помощью этого указателя вызвать функцию из производного класса.
Пусть в этом классе определена компонентная функция void show(). Доступ к функции show() производного класса возможен только с помощью явного указания области видимости:
имя_производного_класса::show();
либо с использованием имени конкретного объекта:
имя_объекта_производного_класса.show();
В обоих случаях выбор нужной функции выполняется при написании исходного текста программы и не изменяется после компиляции. Такой режим называется ранним илистатическим связыванием.
Большую гибкость (особенно при использовании уже готовых библиотек классов) обеспечивает позднее (отложенное), или динамическое связывание, которое предоставляется механизмом виртуальных функций. Любая нестатическая функция базового класса может быть сделана виртуальной, если в ее объявлении использовать спецификатор virtual. Прежде чем объяснить преимущества динамического связывания, приведем пример. Опишем в базовом классе виртуальную функцию и введем два производных класса, где определим функции с такими же прототипами, но без спецификатора virtual.
class base
{
public:
virtual void vfunc (int i)
{ printf("\nbase::i = %d",i); };
};
class derived1: public base
{
public:
void vfunc (int i)
{ printf("\nder1::i = %d",i); };
};
class derived2: public base
{
public:
void vfunc (int i)
{ printf("\nder2::i = %d",i); };
};
void main(void)
{
base B, *bp = &B;
derived1 D1, *dp1 = &D1;
derived2 D2, *dp2 = &D2;
bp->vfunc(1); // Печатает : base::i = 1
dp1->vfunc(2); // Печатает : der1::i = 2
dp2->vfunc(3); // Печатает : der2::i = 3
bp = &D1; bp->vfunc(4); // Печатает : der1::i = 4
bp = &D2; bp->vfunc(5); // Печатает : der2::i = 5
}
Заметим, что доступ к функциям vfunc() организован через указатель bp на базовый класс. Когда он принимает значение адреса объекта базового класса, то вызывается функция из базового класса. Когда указателю присваиваются значения ссылок на объекты производных классов &D1, &D2, выбор соответствующего экземпляра функции определяется именно объектом. Таким образом, интерпретация каждого вызова виртуальной функции через указатель на базовый класс зависит от значения этого указателя, то есть от типа объекта, для которого выполняется вызов. Для не виртуальной функции ее вызов через указатель интерпретируется в зависимости от типа указателя.
Виртуальными могут быть не любые функции, а только нестатические компонентные функции какого-либо класса. После того как функция определена как виртуальная, ее повторное определение в производном классе (с тем же самым прототипом) создает в этом классе новую виртуальную функцию, причем спецификатор virtual может не использоваться.
В производном классе нельзя определять функцию с тем же именем и с тем же набором параметров, но с другим типом возвращаемого значения, чем у виртуальной функции базового класса. Это приводит к ошибке на этапе компиляции.
Если в производном классе ввести функцию с тем же именем и типом возвращаемого значения, что и виртуальная функция базового класса, но с другим набором параметров, то эта функция производного класса не будет виртуальной. В этом случае с помощью указателя на базовый класс при любом значении этого указателя выполняется обращение к функции базового класса (несмотря на спецификатор virtualи присутствие в производном классе похожей функции). Рассмотрим это на примере:
// особенности виртуальных функций
# inclube
class base
{
public:
virtual void f1(void) { printf("\nbase::f1"); };
virtual void f2(void) { printf("\nbase::f2"); };
virtual void f3(void) { printf("\nbase::f3"); };
};
class derived: public base
{
public:
void f1(void) { printf("\nder::f1"); }; // виртуальная
//int f2(void){ printf("\nder::f2"); }; // ошибка в типе
void f3(int i) {printf("\nder::f3:%d",i);}; //невиртуальная
};
void main(void)
{
base B, *bp = &B; dir D, *dp = &D;
bp->f1(); // Печатает : base::f1
bp->f2(); // Печатает : base::f2
bp->f3(); // Печатает : base::f3
dp->f1(); // Печатает : der::f1
dp->f2(); // Печатает : base::f2
//dp->f3(); // Не печатает - вызов без параметра
dp->f3(3); // Печатает : der::f3::3
dp = &D;
bp->f1(); // Печатает : der::f1
bp->f2(); // Печатает : base::f2
bp->f3(); // Печатает : base::f3
//bp->f3(3); // Не печатает - лишний параметр
};
Как уже было упомянуто, виртуальной функцией может быть только нестатическая компонентная функция. Виртуальной не может быть глобальная функция. Функция, подменяющая виртуальную в производном классе, может быть описана как со спецификатором virtual, так и без него. В обоих случаях она будет виртуальной, т.е. ее вызов возможен только для конкретного объекта. Виртуальная функция может быть объявлена дружественной (friend) в другом классе.
Механизм виртуального вызова может быть подавлен с помощью явного использования полного квалифицированного имени. Таким образом, при необходимости вызова из производного класса виртуального метода (компонентной функции) базового класса употребляется полное имя. Например,
class base
{
public:
virtual int f(int j) { return j * j; };
};
class derived: public base
{
public:
int f(int i) { return base::f (i * 2); };
};
Абстрактные классы. Абстрактным классом называется класс, в котором есть хотя бы одна чистая (пустая) виртуальная функция. Чистой виртуальнойназывается компонентная функция, которая имеет следующее определение:
virtual тип имя_функции(список_формальных_параметров) = 0;
В этой записи конструкция «= 0» называется «чистый спецификатор». Пример описания чистой виртуальной функции:
virtual void fpure(void) = 0;
Чистая виртуальная функция «ничего не делает» и недоступна для вызовов. Ее назначение - служить основой для подменяющих ее функций в производных классах. Исходя из этого, становится понятной невозможность создания самостоятельных объектов абстрактного класса. Абстрактный класс может использоваться только в качестве базового для производных классов. При создании объектов такого производного класса в качестве подобъектов создаются объекты базового абстрактного класса. Пример:
class B
{
protected:
virtual void f(int) = 0;
void s(int);
};
class D: public B
{
. . .
void f (int);
};
class E: public B
{
void s (int);
};
Здесь B - абстрактный, D - нет, поскольку f - переопределена, а s - наследуется, E - абстрактный, так как s - переопределена, а f - наследуется.
Как всякий класс, абстрактный класс может иметь явно определенный конструктор. Из конструктора возможен вызов методов класса, но любые прямые или опосредованные обращения из конструктора к чистым виртуальным функциям приведут к ошибкам во время выполнения программы.
По сравнению с обычными классами абстрактные классы пользуются «ограниченными правами». Абстрактный класс нельзя употреблять для задания типа параметра функции или в качестве типа возвращаемого функцией значения. Абстрактный класс нельзя использовать при явном приведении типов. В то же время можно определять указатели и ссылки на абстрактные классы. Объект абстрактного класса не может быть формальным параметром функции, однако формальным параметром может быть указатель абстрактного класса. В этом случае появляется возможность передавать в вызываемую функцию в качестве фактического параметра значение указателя на производный объект, заменяя им указатель на абстрактный базовый класс.
6. Классы потоков С++. (4 час.)