Главная страница

Лекции_ООП_ИС. Курс лекций по объектноориентированному программированию


Скачать 0.58 Mb.
НазваниеКурс лекций по объектноориентированному программированию
АнкорЛекции_ООП_ИС.doc
Дата15.12.2017
Размер0.58 Mb.
Формат файлаdoc
Имя файлаЛекции_ООП_ИС.doc
ТипКурс лекций
#11496
страница6 из 8
1   2   3   4   5   6   7   8

5. Производные классы.(3 час.)

Наследование классов и производные классы. Конструкторы, деструкторы и наследование. Множественное наследование. Виртуальные базовые классы. Иерархия классов. Виртуальные функции. Полиморфизм. Абстрактные классы и чистые виртуальные функции.


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

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

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

Иерархия классов позволяет определять новые классы на основе уже имеющихся. Имеющиеся классы обычно называют базовыми (иногда порождающими), а новые классы, формируемые на основе базовых, - производными (порожденными), иногда классами-потомкамиилинаследниками. Производные классы «получают наследство» - данные и методы своих классов - и, кроме того, могут пополняться собственными компонентами (данными и собственными методами). Наследуемые компоненты не перемещаются в производный класс, а остаются в базовых классах. Сообщение, обработку которого не могут выполнить методы производного класса, автоматически передается в базовый класс. Если для обработки сообщения нужны данные, отсутствующие в производном классе, то их пытаются отыскать автоматически и незаметно для программиста в базовом классе.

При наследовании некоторые имена методов (компонентных функций) и (или) компонентных данных базового класса могут быть по-новому определены в производном классе. В этом случае соответствующие компоненты базового класса становятся недоступными из производственного класса. Для доступа из производственного класса к компонентам базового класса, имена которых повторно определены в производном, используется операция ‘::’ указания (уточнения) области видимости.

Любой производный класс может, в свою очередь, становиться базовым для других классов, и таким образом формируется направленный граф иерархии классов и объектов. В иерархии производный объект наследует разрешенные для наследования компоненты всех базовых объектов. Другими словами, у объекта имеется возможность доступа к данным и методам всех своих базовых классов.

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

При наследовании классов важную роль играет статус доступа (статус внешней видимости) компонентов. Для любого класса все его компоненты лежат в области его действия. Тем самым любая принадлежащая классу функция может использовать любые компонентные данные и вызывать любые принадлежащие классу функции. Вне класса в общем случае доступны только те его компоненты, которые имеют статус public.

В иерархии классов соглашение относительно доступности компонентов класса следующее.

  • Собственные (private) методы и данные доступны только внутри того класса, где они определены.

  • Защищенные (protected) компоненты доступны внутри класса, в котором они определены, и дополнительно доступны во всех производных классах.

  • Общедоступные (public) компоненты класса видимы из любой точки программы, т.е. являются глобальными.

Если считать, что объекты, т.е. конкретные представители классов, обмениваются сообщениями и обрабатывают их, используя методы и данные классов, то при обработке сообщения используются, во-первых, общедоступные члены всех классов программы; во-вторых, защищенные компоненты базовых и рассматриваемого классов и, наконец, собственные компоненты рассматриваемого класса. Собственные компоненты базовых и производных классов, а также защищенные компоненты производных классов не доступны для сообщения и не могут участвовать в его обработке.

Еще раз отметим, что на доступность компонентов класса влияет не только явное использование спецификаторов доступа (служебных слов) - private (собственный), protected(защищенный), public (общедоступный), но и выбор ключевого слова class, struct, union, с помощью которого объявлен класс.

Определение производного класса. В определении и описании производного класса приводится список базовых классов, из которых он непосредственно наследует данные и методы. Между именем вводимого (нового) класса и списком базовых классов помещается двоеточие. Например, при таком определении

class S: X, Y, Z { ...};

класс S порожден классами X, Y, Z, откуда он наследует компоненты. Наследование компонента не выполняется, если его имя будет использовано в качестве имени компонента в определении производного класса S. Как уже говорилось, по умолчанию из базовых классов наследуются методы и данные со спецификаторами доступа - public(общедоступные) и protected(защищенные).

В порожденном классе эти унаследованные компоненты получают статус доступа согласно таблице.



Доступ в базовом классе

Тип доступа при наследовании

Доступ в производном классе

Возможность восстановления доступа

public

public

public

не требуется

private

нет доступа

нет

protected

protected

не требуется




public

protected

protected

да

private

нет доступа

нет

protected

protected

да




public

Private

private

да

private

нет доступа

нет

protected

private

да



При определении класса J как classJ: X, Z { … }; любые наследуемые компоненты классов X, Zбудут иметь в классе J статус частных (private), так как по умолчанию в этом случае будет реализовано private наследование. При определении класса J как structJ: X, Z { ... };любые наследуемые компоненты классов X, Zсохранят в классе J статус доступа базового класса, так как по умолчанию в этом случае будет реализовано public наследование.

Пример:

class A

{

protected:

int t;

public:

char u;

};

class B: A { ... }; // t и u наследуются как private

struct C: A { ... }; // t - protected, u - public

Явно изменить умалчиваемый статус доступа при наследовании можно с помощью спецификаторов доступа - private, protected и public. Эти спецификаторы доступа указываются в описании производного класса непосредственно перед нужными именами базовых классов. Если класс B определен так, как показано выше, то можно ввести следующие производные классы:

class D: protected A { ... }; // t и u наследуется как protected

class E: public A { ... }; // t - protected, u - public

class F: private A { ... }; // t и u наследуется как private

struct G: protected A { ... };// t и u наследуется как protected

struct H: private A { ... }; // t и u наследуется как private

struct I: public A { ... }; // t - protected, u - public

Объявления доступа.

  • Объявления доступа дают возможность сделать опять защищенными или открытыми защищенные или открытые члены закрытого базового класса в производном классе соответственно.

  • Объявления доступа позволяют сделать снова открытыми открытые члены базового класса в защищенном производном. (Ни этот, ни предыдущий пункт не выполняется, если в производном классе объявлены члены с теми же именами, что и в базовом классе).

  • Для перегруженных функций позволяют вернуть им первоначальное ограничение доступа, которое они имели в базовом классе, если все они имели одинаковое ограничение доступа.

class Base

{

int a;

void g();

public:

int b,c;

void f(),

f(int),

g(int);

};

class Derived : private Base

{

public:

Base::a; // Ошибка нельзя сделать a - public

Base::b; // Вновь делает b public

int c;

Base::c; // Ошибка: нельзя с объявлять дважды

Base::f; // Вновь делает все f() public

Base::g; // Ошибка: функции g() имеют различное

// ограничение доступа

};

Особенности конструкторов и деструкторов. Конструктор базового класса всегда вызывается и выполняется до конструктора производного класса. Итак, конструктор вызывается при создании каждого объекта класса и выполняет все необходимые операции, как для выделения памяти объекта, так и для ее инициализации. При конструировании объекта производного класса необходимо создать объект базового класса, являющегося составной его частью.

class Base { . . . };

class Derived : public Base { . . .};

. . .

Derived::Derived(пар-ры) – конструктор производного класса

{ . . . }; - реализация тела конструктора

В рассмотренном случае в базовом классе Base должен быть описан конструктор Base() или не должно быть ни одного конструктора, т.е. использоваться конструктор по умолчанию. Если в базовом классе описан хотя бы один конструктор, отличный от конструктора по умолчанию, т.е. без параметров, то при описании конструктора производного класса необходимо вызывать нужный конструктор класса Base:

Derived::Derived(пар-ры конструктора Derived) :

Base(пар-ры конструктора Base)

{ . . . }; - реализация тела конструктора

Параметры конструктора класса Base должны быть подмножеством параметра соответствующего конструктора класса Derived, либо задаваться константами. Допустим смешанный вариант. В любом случае последовательность действий при конструировании объекта производного класса выглядит так:

- вызов конструктора класса Derived

- вызов конструктора базового для класса Derived класса Base

- вызов конструктора базового для класса Base (если есть)

. . .

- выполнение тела реализации конструктора базового для класса Base

- выполнение тела реализации конструктора класса Base

- выполнение тела реализации конструктора класса Derived

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

имя_класса();

У деструктора не может быть параметров (даже типа 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 час.)
1   2   3   4   5   6   7   8


написать администратору сайта