PROGLANG (1129115), страница 4
Текст из файла (страница 4)
class Z : public virtual X{..};
class T : public Y, public Z{..};
В данном случае получится, что объект класса X не будет дублироваться, даже если написать:
class T : public virtual X, public Y, public X
{..};
Во всех современных языках наследование идёт монотонно, то есть при наследовании не происходить сужение свойств объекта класса.
Java – чисто объектно-ориентированный язык, поэтому все классы выводятся из корневого класса Object. Понятие логический модуль представлено классами и пакетами, а физический модуль – пакетами
package имя1.имя2. ... –––– имена полные
import класс/интерфейс –––– в качестве имён можно использовать сетевые адреса.
В программе должна быть функция main, аналогичная функции main из C++.
class C1
{
public static int main (String[] arg)
}
Количество элементов не надо указывать, так как у массива есть атрибут – длина.
arg.length
Глобальных функций нет, есть только статический эквивалент глобальных. Как и в C++ есть 3 вида доступа к данным:
public – аналогично C++, доступны всюду, где доступен класс;
protected – в функциях-членах производных классов;
private – только в функциях-членах данного класса.
Если употребляется ключевое слово final, то это означает, что у данного класса не может быть наследников. Таким образом получается, что:
final int i=3
фактически определяет константу. Введение слова final вызвано проблемами надёжности, эффективности и прагматичности (не надо динамически искать метод объекта).
Кстати об исключениях,
finally {..};
эквивалентно
catch(...) { .. };
в С++, то есть перехватывает все остальные исключения.
Понятие деструктор в полной своей мере отсутствует. Так как удалением объектов управляет сборщик мусора, поэтому метод finalize (); вызывается не тогда когда мы предполагаем. Можно в самом методе указать для вызова метода базового класса слово super:
super.finalize();
В данном случае слово super эквивалентно слову inherited в Borland Pascal. Если мы имеем:
int[] a1;
int[] ia2;
ia1 = new int[15]; // только теперь объект
// массив существует
ia2 = ia1; // массив не копируется, только ссылки
ia1 = null; // ссылка ia1 заглушается
ia2 = new int[5]; // первый массив повисает
Теперь надо ждать, когда сборщик мусора найдёт потерянный массив и вызовет метод finalize(), если он у нас есть.
При присваивании
ia1 = ia2;
происходит копирование ссылочное, поверхностное, поэтому нужно перекрыть оператор «=». Аналогично, для глубокого сравнения объектов нужно переопределить метод equals.
В базовом классе Object все функции-члены открыты, так как из него выходят все остальные классы. Функция
public int hashCode ();
вырабатывает число для своего объекта, что облегчает и стандартизирует работу с хеш таблицами. С помощью
public final class setClass ();
можно динамически определять тип объекта. Имеется возможность произвести клонирование объекта, то есть произвести глубокую копию:
protected Object clone ();
Объект может запретить клонировать себя, указав, что clone возбуждает исключение CloneNotSupportedException
class X
{ ...
Object clone() throws CloneNotSupportedException {
CloneNotSupportedException;
}
...
}
В Java существует понятие интерфейс, интерфейсы не содержат тел объектов
interface имя
{ список_описаний;
список_прототипов_функций;
}
class NEWCLASS extends OLDCLASS implements
список_интерфейсов;
Это удобно для обеспечения множественного наследования. Класс, который наследует интерфейсы, но не все из них реализованы, считается абстрактным. Специальное слово abstract указывает на это. Если мы создадим свой класс «множество», то клиенты не должны беспокоиться о внутренней реализации представления самого множества.
class MySet extends Slist implements Set
{
список_функций;
}
...
interface Set
{ ... }
Глава 2 Динамическое связывание типов.
В Оберон-2 Н. Вирт добавил механизм динамического связывания типов, чем вывел свой язык в ранг объектно-ориентированных.
TYPE FIGURE=POINTER TO FIGURE_DESC;
FIGURE_DESC = RECORD NEXT:FIGURE END;
TYPE LINE = POINTER TO LINE_DESC;
LINE_DESC = RECORD (FIGURE_DESC)
X1,Y1,X2,Y2 : INTEGER
END;
...
PROCEDURE (P:FIGURE) Draw();
PROCEDURE (P:FIGURE) Move(DX, DY : INTEGER);
...
DEFINITION
TYPE FIGURE_DESC = RECORD
NEXT : FIGURE;
PROCEDURE Draw();
PROCEDURE Move(DX, DY : INTEGER);
END;
Вот и попался профессор Вирт, он сам ругал описания классов в С++ и SmallTalk, а здесь мы видим, что вместе с полями описываются свои методы объекта. И указываемый параметр Р в описании базовых функциях Draw и Move есть не что иное как параметр this, который в С++ передаётся автоматически по умолчанию. Тогда соответственно функция отрисовки всех объектов будет безопасна и изящна
PROCEDURE DRAWALL
VAR P : FIGURE;
BEGIN
P := LIST;
WHILE P#NIL DO
P.Draw ();
P = P.NEXT
END;
END DRAWALL;
Её тело не надо перекомпилировать при изменении типов объектов. Не смотря на то что базовые функции Draw и Move абстрактны, тела их надо описать (по семантике).
PROCEDURE (P:FIGURE) Draw()
BEGIN ERROR() END;
Если объявлены 2 класса:
class X
{ virtual int f(); };
class Y
{ virtual int f(); };
то при выполнении следующего фрагмента будет подразумеваться, что
X::f() Y::f()
X
*px;
Y *py;
px = py; px->f(); py->f();
А если:
Y::f()
class X
{ virtual int f(); };
class Y
{ virtual int f(int); }; // oops!
то компилятор обижается и запрещает статическое перекрывание.
Напомним, что виртуальными могут быть только функции-члены. В С++ функции не могут менять свою виртуальность случайно. Удачное толкование для термина overriding – переопределение – «подмена» функционального свойства. Если нет виртуальных методов в классе, то размещение в памяти будет идентично структурам (линейная). Поэтому
class Complex
{
double Re, Im;
double Abs();
...
};
эквивалентно по присваиванию полей структуре
struct Complex
{
double Re, Im;
};
Это обстоятельство удобно для совместимости с программами на С.
Вспомним о статическом перекрытии имён.
class Y:X
{
int g();
virtual int g(double);
...
}
py->g(); py->g(int);
Компилятор по профилю параметров определит какая функция должна вызываться. Подводный камень проявляется, если в классе X функция g определена как
virtual int g(int);
Тогда при исполнении
px = py; px->g(10);
произойдёт неявное преобразование типов, что нам совсем не требуется. Единственное, что сделано для безопасности, так это то, что перекрываемые функции должны возвращать значения одинакового типа.
Если имеется критический класс, от которого зависят все классы проектов, то, чтобы не производить перекомпиляцию всего проекта при изменении критического класса, надо объявить все функции-члены виртуальными в этом классе.
class Struct
{
virtual op1();
virtual op2();
...
virtual ~Struct(); // виртуальный деструктор
virtual *clone();
};
а в другом классе Struct_Impl файла struct_impl.cpp
class Struct_Impl : Struct
{
op1() {...};
op2() {...};
...
struct *clone() {...};
};
Таким образом виртуальными функциями мы описали чистый интерфейс с объектом. Для создания объекта конструкция struct x; не пройдёт, нужно создавать объект в динамической памяти. Для этого мы описали метод clone();, который будет создавать объект
Struct *px = Struct::clone();
Для удаления объекта нужно вызвать деструктор
delete px
Если он не описан как виртуальный, то не произойдёт глубокого удаления. Подобъекты, на которые ссылается этот объект, не будут удалены, что не нужно, так как при этом останется мусор в динамической памяти.
Чем мы платим? В памяти появляется таблица виртуальных методов ТВМ. Если архитектура не оптимизирована для косвенных вызовов, то на каждый вызов виртуального метода тратится на 6-8 инструкций больше. Виртуальный метод в общем случае по своей природе не может быть inline типа, кроме того случая, когда метод не наследуется и не перекрывается. Но по памяти накладные расходы для каждого объекта минимальны, в начало добавляется ссылка на свою ТВМ.
Допустим мы имеем следующее
c lass X
{ ТВМ X
virtual f();
virtual g(); X
};
c lass Y:X ТВМ Y
{
X
f();
virtual h(); Y
};
c
lass Z:Y ТВМ Z
{
g(); X
h();
virtual y(); Y
} Z
Хотя ТВМ создаётся для каждого класса, но для всех объектов этого класса она одна общая. В простейшем случае ТВМ состоит из указателей на тела методов. В нашем случае получится следующее
Т
ВМ X ТВМ Y ТВМ Z
X:: f Y:: f Y:: f
X:: g X:: g Z:: g
Y:: h Z:: h
Z:: y
Можно отметить, что все указатели на виртуальные методы ТВМ упорядочены, имеют одинаковое смещение от начала. Таким образом удобно производить обращение к ТВМ.
px = py;
px -> f();
Загрузчик лезет в Y и вызывает Y::f(), но вызов px->h() – забракуется компилятором статически на стадии компиляции, так как в классе X не объявлена функция h().
Проблема заключается в том, что в ТВМ надо поместить указатель на тело метода, вспомним наш класс Struct. А если функция виртуальная, то на что должен указывать указатель? Писать пустые NULL заглушки бессмысленно, так как вызов их ошибочен по сути потому, что должны вызываться-то методы класса реализаций. Поэтому нужно использовать абстрактный метод.
virtual op1()=0; – pure function
Так называемая «чистая» виртуальная функция. При этом в ТВМ помещается не NULL, а в действительности указатель на диагностирующую функцию времени выполнения. Так как абстрактную функцию могут нечаянно вызвать через прямое обращение к базовому классу. Абстрактный класс нельзя явным образом инициализировать, только через указатель.
К недостаткам нужно отнести то факт, что при большом количестве типов много места занимают множественные ТВМ объёмного содержания (например, 144 обработчика сообщений в системе Windows).
В С++ понятие абстрактного класса размыто, так как он, класс, может содержать, как мы уже видели, поля и методы (например, конструкторы). А в Java в принципе нельзя вызвать абстрактный метод – для защиты перед словом class ставиться слово abstract. В Java запрещено множественное наследование объектов, только иерархическое. Но интерфейсы могут иметь несколько предков, так как они абстрактны и ничего не реализуют. Например
// класс наследует С и реализует интерфейсы
class C1 extends C implements I1, I2, ... , IN