лекции (2008) (Фингеров Александр_ Кононов Алексей_ Кузин Сергей) (1160833), страница 15
Текст из файла (страница 15)
объявление новых членов;
}
Модификатор доступа не разрешен, множественное наследование запрещено. Но есть возможность наследовать интерфейсы.
Delphi:
class Derived extends Base implements i1, i2{
объявление новых членов;
}
Оберон:
TYPE BASE = RECORD
END;
TYPE DERIVED = RECORD(BASE)
END;
Язык Ада:
type base is tagged record
…
end;
type Derived is new Base with record
…
end;
Если пишем with null record, это означает, что мы не добавили новых членов данных, но видимо хотим переопределить некоторые функции.
Промышленные языки программирования реализуют возможность линейного распределения памяти. Таким образом, появляется возможность варьировать набор методов выполнения во время выполнения. Это дает языку огромную возможность, но замедляет работу программы, так как при каждом обращении к методу класса требуется поиск по всей цепочке наследования.
Все функции класса существуют в единственном экземпляре. При наследовании каждый класс – своя область действия, то есть
class X{
void f();
};
Class Y:public X{
int f;
};
В классе X и Y имена могут совпадать. Существует два принципа действия области видимости:
-
перегрузка (overloading) (в одной области действия)
-
cкрытие (hiding)
-
переопределение (overriding) (только для функций, которые поддерживают динамическое связывание)
Есть переменная Y y; и мы пишем y.f - что это? Всегда речь идет о int f. В то время, как y.f() работать не будет.
Если же заменить int f на void f(int). Это будет скрытие. Теперь y.f(1) – допустима, а y.f() – недопустима.
Модульные языки: Ада 95, Оберон.
MODULE m;
TYPE BASE*= RECORD
I:INTEGER
j*: REAL;
END;
ENDM;
MODULE M1;
IMPORT M;
TYPE DERIVED *= RECORD (BASE)
K:INTEGER
END;
PROCEDURE P(VAR X:DERIVED);
BEGIN
X.K := 0;
X.J := 1;
X.I := 1; //ОШИБКА
END;
Есть либо публичный член, либо пакетный, а как таковых защищенных и приватных нет.
package M is
type base is tagged private;
….
private
type Base is tagged record;
…
end record;
end M;
package M1 is
use M;
type Derived is new Base
with record
K:integer;
end record;
procedure B(X: inout Derived) is….
В Аде появляется понятие дочерних пакетов.
package M.M1 is … (все объекты, описанные в M, доступны в M1)
type Derived isnew Base with record
K:integer
end record;
procedure P(x: inout Derived)
Множественное наследование.
Проблемы:
-
конфликт имен
-
реализация
-
присваивание объектов разных классов
Base * pb = new Base;
Derived * pd = new Derived;
pb = pd;
pb->f();
Все эти операции верны и допустимы.
class X: public Y, public Z{
};
X * px = new X;
Y * py = new Y;
Z * pz = new Z;
px = pz; //Ошибка
py = px;
pz = px;
Лекция 29.
Проблемы множественного наследования.
-
Конфликт имён. Язык С++ допускает полное и неограниченное множественное наследование. Заметим, что вообще языков с множественным наследованием не так уж много. Ограниченное множественное наследование (фактически понятие интерфейсов) есть во всех современных языках.
-
Проблема с реализацией. (например, то, что значение указателя нужно всё время продвигать)
-
Есть и некоторые идеологические соображения, которые впрочем нас не интересуют.
Примеры иерарахий:
X->W<-Y
L L
→ ←
X Y
→ ←
W
(один и тот же класс может быть несколько раз базовым)
L->W<-L — ошибка, такое не разрешено. Хотя можно ввести аппарат различения. Когда наследуем 2 раза от L, то объект может находиться одновременно в двух списках. Примеров, когда это нужно, можно привести очень много.
Поиск — сначала ищем в своём пространстве имён, а потом параллельно в высших уровнях. Нужно уточнять с помощью спецификатора «::» , чтобы обращаться к определенному уровню.
Такие иерархические структуры изучает раздел математики — теория решёток. Наследование бывает линейным и нелинейным (например, ромбовидным). Пример ромбовидного наследования — iostream.h. Упрощенная иерархия: в начале находится класс ios — обертка файлового дескриптора и есть 2 метода — open/close. Из ios выходят 2 класса — istream и ostream, далее iosrtream наследуется от этих 2х классов — это файловый дескриптор, обладающий свойствами и istream, и ostream.
class A{
..
};
class X:public virtual A{
..
};
class Y:public virtual A{
..
};
class W:publuc X,public Y{
..
};
Ключевое слово — virtual. То, что при единичном наследовании мы уже должны думать о ромбовидном наследовании, на самом деле не есть недостаток языка С++, это недостаток или вообще свойство наследования — при проектированиии мы должны думать уже с первого класса. В С++ запретить наследование вы не можете никаким образом. В отдельных случаях можно делать констуктор приватным, но тогда кому этот класс нужен? Нужно объявить друзей, а можно например сделать виртуальный конструктор — функцию, которая просто порождает эти объекти в динамической памяти, и наследовать эти объекты уже никто не может. В более современных языках — как Java/C# можно вообще запретить наследование: в Java — словом final перед классом, в C# - sealed. Обратим внимание, что очень многие классы из библиотеки .NET являются запечатанными.
Глава 2. Динамическое связывание методов.
Если у нас есть объект super-класса, то при замене объекта super-класса на объект производного класса, надежность падать не должна.
base=derived L*
void Foo(Base b)
base.f()+g(fax)
(не очень понятно о чём конкретно речь в примерах - прим. ред.)
В языках, где есть наследование, появляется понятие статического типа — тип, присвоенный объекту при создании.
var x=new System.Window.Forms.Form(..);
Тип любого объекта в традиционном ЯП статически определим. В то же время в ОО ЯП появляется динамический тип, который используется для указателей или ссылок. Динамический тип — тип объекта данных, на который ссылается указатель/ссылка.
Base *pb;
статический — Base, динамический — как Base, так и производные классы.
Если Derived<=Base, то ДТ<=СТ.
(статический тип данных изменить нельзя)
Объект данных — инициализированный кусок памяти, мы не можем его расширить или уменьшить.
Рассмотрим нестатический метод класса (вызывается через ссылку на объект (this)):
метод называется статически привязанным, если его вызов осуществляется исходя из статического типа ссылки, и динамическим в обратном случае.
У нас есть:
-
Скрытие
-
Перегрузка
-
Переопределение
Переопределяться может только динамически связанный параметр.
Применительно в ОО ЯП ковариантность — применяется к типам данных. Если тип X является производным от типа Y, то тип X ковариантен типу Y.
class Y{
public Y*f(){..}
}
class X extends Y{
public X* f(){..}
}
ковариантные методы. Если изменить возвращаемый тип — ошибка.
В C++,Java,Delphi существует ключевое слово — virtual. Когда мы говорим о динамических методах, мы подразумеваем вызов, в то же время динамическое связывание — модификатор, свойство самого метода.
Представим, что есть некая иерархия классов:
A->B->C->D->...
Пусть есть нечто B *pb; и нестатический метод pb->f();
Как будет осуществляться вызов? Компилятор видит класс B и смотрит в его объявление, ищет там метод f(). Если он его не находит, то идёт в объемлющую область видимости — класс А — и так пока либо найдёт метод, либо не найдёт ничего.
Представим что есть void B::g(){ f(); };
Надо понимать, что в этом случае применяются к вызову f() те же самые правила. Для функций алгоритм поиска чуть усложнен. Если f(1), то есть 5 случаев по приоритету.
Теперь пусть f — виртуальный метод. Тогда компилятор либо найдёт, либо не найдёт данную функцию.
Представим себе такую ситуацию:
B* pb=new D();
pb->f();
В таком случае смотрим на динамический тип и, начиная с D, ищем соответствующие переопределения.
Точно такая же ситуация и для языка C# - отыскивается динамический тип, начинается поиск с него и после этого вызывается первый встреченный подходящий метод.
В Java компилятор, увидев pb->f(); он отыскивает все определения f, выясняя на ходу виртуальность-невиртуальность, параметры и доступ. Если функция определена как приватная, то на неё никто не смотрит, они действительно только для функций членов класса — это называется управление видимостью. Дальше смотрим на динамический тип, если это public, то будет вызов, private вызываться не будут. В С++ можно переопределять приватную виртуальную функцию.
Представим некий класс А:
class A{
private:
virtual void f(){..}
};
class B:public A{
private:
virtual void f(){..}
};
Переопределить private мы можем только private'ом. Мы даём возможность нашу приватную функцию вызывать из базового класса, но лишаемся возможности вызывать её явно из другого класса. Мы как бы предлагаем свою реализацию.
Пример:
Figure->Point