Почему разрешено вызывать закрытый виртуальный метод производного класса через указатель базового класса?
# include <iostream>
using namespace std;
class A
{
public:
virtual void f()
{
cout << "A::f()" << endl;
}
};
class B:public A
{
private:
virtual void f()
{
cout << "B::f()" << endl;
}
};
int main()
{
A *ptr = new B;
ptr->f();
return 0;
}
Этот код работает правильно и печатает B::f(). Я знаю, как это работает, но почему этот код разрешен?
6 ответов
Контроль доступа выполняется во время компиляции, а не во время выполнения. Там вообще нет возможности для вызова f()
знать тип времени выполнения объекта, на который указывает ptr
, поэтому нет проверки на спецификаторы доступа производного класса. Вот почему звонок разрешен.
Что касается того, почему классу B вообще разрешено переопределять с помощью закрытой функции - я не уверен. Конечно, B нарушает интерфейс, подразумеваемый его наследованием от A, но в целом язык C++ не всегда обеспечивает наследование интерфейса, так что тот факт, что это просто неправильно, не означает, что C++ остановит вас.
Таким образом, я предполагаю, что, вероятно, есть некоторый вариант использования для этого класса B - замена все еще работает с динамическим полиморфизмом, но статически B не является заменой для A (например, могут быть шаблоны, которые вызывают f
, это будет работать с A в качестве аргумента, но не с B в качестве аргумента). Могут быть ситуации, когда это именно то, что вы хотите. Конечно, это может быть непреднамеренным следствием какого-то другого соображения.
Этот код разрешен, потому что f является общедоступным в интерфейсе А. Производный класс не может изменить интерфейс базового класса. (Переопределение виртуального метода не меняет интерфейс и не скрывает членов базы, хотя может показаться, что оба они делают это.) Если производный класс может изменить интерфейс базы, он нарушит отношение "является".
Если разработчики A хотят сделать f недоступным, то он должен быть помечен как защищенный или закрытый.
В дополнение к ответу Стива:
- B публично получен из A. Это подразумевает заменяемость по Лискову
- Переопределение f для приватности, кажется, нарушает этот принцип, но на самом деле это не обязательно - вы все равно можете использовать B как A без мешающего кода, так что если частная реализация f все еще в порядке для B, никаких проблем
- Возможно, вы захотите использовать этот шаблон, если B должен заменять Лискова на A, но B также является корнем другой иерархии, которая на самом деле не связана (в стиле Лискова с заменой) с A, где f больше не является частью открытого интерфейса., Другими словами, класс C, производный от B, используемый через указатель на B, скрывал бы f.
- Тем не менее, это на самом деле весьма маловероятно, и, вероятно, было бы лучше получить B из частного или защищенного
Проверка контроля доступа к функциям происходит на более поздней стадии вызова функции C++. Порядок на высоком уровне будет таким, как поиск имени, вывод аргумента шаблона (если есть), разрешение перегрузки, затем проверка контроля доступа (public/protect/private).
Но в вашем фрагменте вы использовали указатель на базовый класс, а функция f() в базовом классе действительно общедоступна, это то, что компилятор может видеть во время компиляции, поэтому компилятор обязательно пропустит ваш фрагмент.
A *ptr = new B;
ptr->f();
Но все вышеперечисленное происходит во время компиляции, поэтому они действительно статичны. В то время как вызов виртуальной функции, часто приводимый в действие vtable & vpointer, - это динамические вещи, которые происходят во время выполнения, поэтому вызов виртуальной функции ортогональн к управлению доступом (вызов виртуальной функции происходит после контроля доступа), поэтому вызов функции f() фактически завершился B::f() независимо от того, является ли управление доступом приватным.
Но если вы попытаетесь использовать
B* ptr = new B;
ptr->f()
Это не пройдет, несмотря на vpointer & vtable, компилятор не позволит ему скомпилироваться во время компиляции.
Но если вы попробуете:
B* ptr = new B;
((static_cast<A*>(ptr))->f();
Это будет работать просто отлично.
Ваш базовый класс определяет интерфейс для всех унаследованных потомков. Я не понимаю, почему это должно препятствовать упомянутому доступу. Вы можете попробовать извлечь класс из 'B' и использовать базовый интерфейс для вызова, что приведет к ошибке.
Ура!
Как и в Java, в C++ вы можете увеличить видимость методов, но не уменьшить ее.