Учитывая следующий фрагмент кода, каковы различия в вызовах функций? Что такое скрытие функций? Что такое переопределение функции? Как они связаны с перегрузками функций? Какая разница между двумя? Я не мог найти хорошее описание их в одном месте, поэтому я спрашиваю здесь, чтобы я мог объединить информацию.
class Parent {
public:
void doA() { cout << "doA in Parent" << endl; }
virtual void doB() { cout << "doB in Parent" << endl; }
};
class Child : public Parent {
public:
void doA() { cout << "doA in Child" << endl; }
void doB() { cout << "doB in Child" << endl; }
};
Parent* p1 = new Parent();
Parent* p2 = new Child();
Child* cp = new Child();
void testStuff() {
p1->doA();
p2->doA();
cp->doA();
p1->doB();
p2->doB();
cp->doB();
}
… это форма сокрытия имени. Простой пример:
void foo(int);
namespace X
{
void foo();
void bar()
{
foo(42); // will not find `::foo`
// because `X::foo` hides it
}
}
Это также относится к поиску имени в базовом классе:
class Base
{
public:
void foo(int);
};
class Derived : public Base
{
public:
void foo();
void bar()
{
foo(42); // will not find `Base::foo`
// because `Derived::foo` hides it
}
};
Это связано с концепцией виртуальных функций. [Class.virtual] / 2
Если функция виртуального члена
vf
объявлен в классеBase
и в классеDerived
прямо или косвенноBase
, функция-членvf
с тем же именем, параметром-списком типов, cv-квалификацией и ref-квалификатором (или отсутствием того же), что иBase::vf
объявлен, тоDerived::vf
также является виртуальным (независимо от того, так ли это объявлено), и это ПереопределениеBase::vf
,
class Base
{
private:
virtual void vf(int) const &&;
virtual void vf2(int);
virtual Base* vf3(int);
};
class Derived : public Base
{
public: // accessibility doesn't matter!
void vf(int) const &&; // overrides `Base::vf(int) const &&`
void vf2(/*int*/); // does NOT override `Base::vf2`
Derived* vf3(int); // DOES override `Base::vf3` (covariant return type)
};
Окончательный переопределение становится актуальным при вызове виртуальной функции: [class.virtual] / 2
Виртуальная функция-член
C::vf
объекта классаS
является окончательным переопределением, если самый производный класс которогоS
подобъект базового класса (если есть) объявляет или наследует другую функцию-член, которая переопределяетvf
,
То есть если у вас есть объект типа S
окончательный переопределение — это первое переопределение, которое вы видите при обходе иерархии классов S
вернуться к своим базовым классам. Важным моментом является то, что динамический тип выражения вызова функции используется для определения окончательного переопределения:
Base* p = new Derived;
p -> vf(); // dynamic type of `*p` is `Derived`
Base& b = *p;
b . vf(); // dynamic type of `b` is `Derived`
По сути, функции в базовом классе всегда скрыты функциями с одинаковыми именами в производном классе; не имеет значения, переопределяет ли функция в производном классе виртуальную функцию базового класса:
class Base
{
private:
virtual void vf(int);
virtual void vf2(int);
};
class Derived : public Base
{
public:
void vf(); // doesn't override, but hides `Base::vf(int)`
void vf2(int); // overrides and hides `Base::vf2(int)`
};
Чтобы найти имя функции, используется статический тип выражения:
Derived d;
d.vf(42); // `vf` is found as `Derived::vf()`, this call is ill-formed
// (too many arguments)
Поскольку «скрытие функции» является формой сокрытия имени, все перегрузки затрагиваются, если имя функции скрыто:
class Base
{
private:
virtual void vf(int);
virtual void vf(double);
};
class Derived : public Base
{
public:
void vf(); // hides `Base::vf(int)` and `Base::vf(double)`
};
Для переопределения функции будет переопределена только функция в базовом классе с теми же аргументами; Вы можете, конечно, перегрузить виртуальную функцию:
class Base
{
private:
virtual void vf(int);
virtual void vf(double);
void vf(char); // will be hidden by overrides in a derived class
};
class Derived : public Base
{
public:
void vf(int); // overrides `Base::vf(int)`
void vf(double); // overrides `Base::vf(double)`
};
Разница между вызовом виртуальный функция-член и вызов невиртуальном Функция-член заключается в том, что по определению в первом случае целевая функция выбирается в соответствии с динамический тип выражения объекта, используемого в вызове, в то время как в последнем случае статический тип используется.
Это все, что нужно сделать. Ваш пример ясно иллюстрирует эту разницу p2->doA()
а также p2->doB()
звонки. Статический тип *p2
выражение Parent
в то время как динамический тип одного и того же выражения Child
, Вот почему p2->doA()
звонки Parent::doA
а также p2->doB()
звонки Child::doB
,
В тех случаях, когда это различие имеет значение, сокрытие имени вообще не входит в картину.
Гораздо более простой пример, который отличается ч / б от всех.
class Base {
public:
virtual int fcn();
};
class D1 : public Base {
public:
// D1 inherits the definition of Base::fcn()
int fcn(int); // parameter list differs from fcn in Base
virtual void f2(); // new virtual function that does not exist in Base
};
class D2 : public D1 {
public:
int fcn(int); // nonvirtual function hides D1::fcn(int)
int fcn(); // overrides virtual fcn from Base
void f2(); // overrides virtual f2 from D1
}
Пример кода, который вы написали в вопросе, по сути, дает ответ при его запуске.
Вызов не виртуальной функции будет использовать функцию из того же класса, что и тип указателя, независимо от того, был ли объект фактически создан как некоторый другой производный тип. Принимая во внимание, что вызов виртуальной функции будет использовать функцию из исходного выделенного типа объекта, независимо от того, какой тип указателя вы используете.
Таким образом, вывод вашей программы в этом случае будет:
doA in Parent
doA in Parent
doA in Child
doB in Parent
doB in Child
doB in Child
Начнем с простых.
p1
это Parent
указатель, поэтому он всегда будет вызывать Parent
функции-члены.
cp
это указатель на Child
так будет всегда звонить Child
функции-члены.
Теперь более сложный. p2
это Parent
указатель, но он указывает на объект типа Child
так оно и будет звонить Child
функции всякий раз, когда сопоставление Parent
функция является виртуальной или функция существует только внутри Child
и не в Parent
, Другими словами, Child
шкуры Parent::doA()
со своим doA()
, но это переопределяет Parent::doB()
, Скрытие функции иногда считается формой перегрузки функции, потому что функция с тем же именем имеет другую реализацию. Поскольку скрывающая функция находится в другом классе, чем скрытая, она имеет другую сигнатуру, которая дает понять, какой из них использовать.
Выход для testStuff()
будет
doA in Parent
doA in Parent
doA in Child
doB in Parent
doB in Child
doB in Child
В любом случае, Parent::doA()
а также Parent::doB()
может быть вызван в течение Child
используя разрешение имен, независимо от «виртуальности» функции. Функция
void Child::doX() {
doA();
doB();
Parent::doA();
Parent::doB();
cout << "doX in Child" << endl;
}
демонстрирует это, когда вызывается cp->doX()
путем вывода
doA in Child
doB in Child
doA in Parent
doB in Parent
doX in Child
Дополнительно, cp->Parent::doA()
позвоню Parent
версия doA()
,
p2
не может ссылаться на doX()
потому что это Parent*
, а также Parent
ничего не знает в Child
, Тем не мение, p2
может быть приведен к Child*
, поскольку он был инициализирован как единое целое, а затем его можно использовать для вызова doX()
,