Записи по ЯП (1161118), страница 6
Текст из файла (страница 6)
}
class ComboBox : IDropList, IEdit // Элемент управления ComboBox реализует оба //интерфейса. И для каждого из них должен быть свой //Paint;
{
void IDropList.Paint(){…; } //Явная реализация
void IEdit.Paint(){….;}//Явная реализация
public void Paint() //Этот метод будет вызываться по умолчанию
{
.…
((IEdit)this).Paint(); // Явный вызов
…..
}
}
В C# реализованные методы интерфейсов считаются по умолчанию «sealed» - запечатанными. В наследниках они перекрываются.
class ExampleClass : IControl
{
public void Paint(){ … ;}
}
class FancyComBox: ExampleClass
{
public void Paint(){….; } // Компилятор выдаст предупреждение для этой строчки, в котором сообщит, что «FancyComBox.Paint() скрывает унаследованный метод ExampleClass.Paint(). Используйте ключевое слово «new», если это сделано целенаправленно.» Т.е. если поставить «new» перед определением, то предупреждение исчезнет.
public void override Paint (){… ; } // Для этой строчки компилятор выдаст ошибку, т.к. нельзя замещать(переопределять) методы, не указанные в базовых классах как virtual, override или abstact
}
В целом множественное наследование можно заменить включением (агрегацией).
Как было показано выше, конфликт имён решается через явное приведение и уточнение класса.
Но существует ещё одна проблема – эффективность динамического полиморфизма (виртуальных функций)
Эта проблема возникает только при наследовании по данным.
Рассмотрим одиночное наследование:
class A
{
public:
A(){};
virtual void a(){ a1 = 1;};
virtual void second(){..;}
int a1, a2, a3;
};
class C : public A
{
public:
C() : A(){};
virtual void goo(){};
void a(){}; // переопределение
int c1;
};
….
C c;
Если в классе C переопределить метод, то в соответствующую ячейку в таблице виртуальных функций будет записан указатель на новый метод. Если же в классе C добавляются новые функции – они дописываются в конец таблицы. При вызове методов никаких лишних действий не происходит.
А теперь рассмотрим множественное наследование:
class A
{
public:
A(){};
virtual void a(){ a1 = 1;};
virtual void second(){..;}
int a1, a2, a3;
};
class B
{
public:
B(){};
virtual void bar(){};
virtual void bbar(){};
int b1, b2, b3;
};
class C : public A
{
public:
C() : A(){};
virtual void goo(){};// Собственная новая виртуальная функция
void a(){}; // переопределение
void bar();// переопределение
int c1;
};
….
C c;
Тут надо обратить внимание на следующее:
-
Таблица виртуальных методов самого нижнего класса в иерархии доступна через первый указатель vptr.
-
Каждый подобъект, который содержит виртуальные методы, имеет свою таблицу виртуальных функций.
Если в классе C переопределить метод, то в соответствующую ячейку в таблице родительского объекта будет записан указатель на новый метод. Если же в классе C добавляются новые функции – они дописываются в конец первой таблицы.
Такой алгоритм становится понятен, если рассмотреть возможные преобразования типов:
-
С -> A. Через указатель на класс A можно вызывать только методы, которые прописаны в этом классе.
-
C -> B. Ситуация аналогична, только мы можем вызывать виртуальные методы, определенные в классе B.
Новые виртуальные методы (которых нет в родительских классах) можно использовать только через указатель на класс C. В этом случае всегда используется первая таблица виртуальных функций.
Сложность реализации заключается в следующем:
Во время преобразования типов меняется адрес указателя:
C c;
B *p = &c;
Указатель p будет содержать адрес объекта c + смещение подобъекта B. Т.е. все вызовы методов через такой указатель будут использовать вторую таблицу виртуальных методов объекта C. Но ведь в такой ситуации при вызове переопределённой в C функции через указатель на B в эту функцию передастся неправильный указатель this! Он будет указывать не на C, как это нужно, а на B.
Приходится расширять таблицу виртуальных функций добавлением в неё смещения от указателя на объект класса до таблицы виртуальных функций для каждой функции. Если виртуальная функция из B переопределена в C, то для неё такое смещение будет равно (-смещение подобъекта B). Если же не была переопределена, то оно будет равно нулю. Для всех виртуальных функций из класса A это смещение будет нулевым, т.к. указатель на подобъект A совпадает с указателем на весь объект C(объект А находится в начале объекта C). Теперь в функцию можно передать правильный указатель:
this = current_this + offset
где current_this – на подобъект, через который вызывается функция. offset – значение, которое берётся из расширенной таблицы виртуальных функций.
Без наследования по данным таких проблем не возникает, т.к. указатель на таблицу виртуальных функций всегда один.
Ромбовидное и не ромбовидное наследование
Н е ромбовидное: : В объекте Z будет два экземпляра объекта A с разными реализациями таблицы виртуальных функций
сlass A{ .. ;}
class X:public A{ …; }
class Y:public A{… ; }
class Z: public X, public Y {…;}
A A
X Y
Z
Ромбовидное: В объекте Z будет только один экземпляр объекта A
сlass A{ .. ;}
class X: public virtual A{ …; }
class Y: public virtual A{… ; }
class Z: public X, public Y {…;}
A
X Y
Z