C++继承中成员函数的访问
Posted
技术标签:
【中文标题】C++继承中成员函数的访问【英文标题】:Access of member functions in C++ inheritance 【发布时间】:2017-01-07 11:34:01 【问题描述】:我只是对下面关于继承的小程序感到困惑:
#include<iostream>
using namespace std;
struct B
virtual int f() return 1;
; // f is public in B
class D : public B
int f() return 2;
; // f is private in D
int main()
D d;
B& b = d;
cout<<b.f()<<endl; // OK: B::f() is public, D::f() is invoked even though it's private
cout<<d.f()<<endl; // error: D::f() is private
-
我不明白为什么
D::f()
是私有的,D
是从B
继承的,所以B
中的公共函数f
在D
也是公开的(我知道没有继承,成员访问默认是私有的)
f
是B
中的一个虚函数,所以如果我们调用b.f()
,我们实际上是调用D::f()
,但是就像上面的插图,为什么D::f()
是私有的,也能被调用?李>
谁能详细解释一下简单的继承问题?
【问题讨论】:
我不明白为什么 D::f() 是私有的 -- 因为它是一个private
函数。当然不是public
。你有一个class
,你没有访问说明符,函数是private
。
那么问题来了:为什么还能通过基类的函数调用呢?合理的推论...
拥有派生类private
的成员函数不会关闭虚拟机制。谢天谢地。许多设计模式都使用这种技术(例如模板模式),派生类将虚函数实现为私有(或受保护)。
我知道默认访问说明符是私有的,但是由于 D 是从 B 继承的 public,B 中的 public 函数也应该是 公开在D中,我的误会在哪里?
作为比较,请注意在 Java 中不能降低继承方法的可见性(因此,如果方法在超类中是公共的,则不能在子类中设为私有)。
【参考方案1】:
这与虚拟调度是一个运行时概念有关。 B
类不关心哪个类扩展了它,也不关心它是私有的还是公共的,因为它不知道。
我不明白为什么 D::f() 是私有的,D 是从 B 继承的公共函数,所以 B 中的公共函数 f 在 D 中也是公共的(我知道没有继承,成员访问默认是私有的)
D::f()
是私有的,因为您将其设为私有。此规则不受继承和虚拟调度的影响。
f是B中的虚函数,所以如果我们调用bf(),我们实际上是调用了D::f(),但是正如上面的插图,为什么D::f()即使被调用也能被调用是私人的吗?
因为实际上,在调用b.f()
时,编译器并不知道实际调用的是哪个函数。它将简单地调用函数f()
,并且由于B::f
是虚拟的,因此将在运行时 选择被调用的函数。运行时程序没有关于哪个函数是私有或受保护的信息。它只知道函数。
如果函数是在运行时选择的,编译器在编译时无法知道将调用哪个函数,也无法知道访问说明符。事实上,编译器甚至不会尝试检查调用的函数是否是私有的。访问说明符可能存在于编译器尚未看到的某些代码中。
根据您的经验,您不能直接致电D::f
。这正是 private 会做的:禁止成员的直接访问。但是,您可以通过指针或引用间接访问它。您使用的虚拟调度将在内部执行此操作。
【讨论】:
【参考方案2】:为什么 D::f() 是私有的也能被调用?
要了解虚函数机制,最好了解它通常是如何实现的。运行时的函数实际上只不过是函数体的可执行代码所在的内存地址。要调用函数,我们需要知道它的地址(指针)。在内存中具有虚函数表示的 C++ 对象包含所谓的 vtable - 一个指向虚函数的指针数组。
关键是在派生类中vtable重复(并且可能扩展)基类的vtable,但是如果虚函数被覆盖,它的指针被替换在派生对象的vtable中。
当通过基类指针完成虚函数调用时,虚函数的地址被计算为vtable数组中的偏移量。不做其他检查,只取函数地址。 如果它是基类对象,它将是基类函数的地址。如果是派生类对象,就是派生类函数的地址,与是否声明为私有无关。
这就是它的工作原理。
【讨论】:
如何实现虚拟调度完全无关紧要!我想说的是,与其痴迷于内部结构,学习如何抽象地思考更有价值。 C++ 标准已经为我们准备好了抽象,所以我们只需要将我们的语言限制在它的范围内。 :)【参考方案3】:这实际上与虚拟调度关系不大,而与访问说明符的含义有关。
函数本身不是private
;它的名字是。
因此,函数不能在类范围之外命名,例如来自main
。但是,您仍然可以通过 public
的名称(即被覆盖的基类的虚函数)或尽管有 private
限定符但可以访问函数名称的范围(例如该类的成员函数)来执行此操作)。
这就是它的工作原理。
【讨论】:
【参考方案4】:C++ 标准有一个确切的例子:
11.5 访问虚函数 [class.access.virt]
1 虚函数的访问规则(第 11 条)由它的 声明并且不受稍后的函数规则的影响 覆盖它。 [例子:
class B public: virtual int f(); ; class D : public B private: int f(); ; void f() D d; B* pb = &d; D* pd = &d; pb->f(); // OK: B::f() is public, // D::f() is invoked pd->f(); // error: D::f() is private
-- 结束示例]
无法解释清楚。
【讨论】:
“虚函数的访问规则...”有点牵强,同样的规则适用于虚函数和非虚函数。在这个例子中取出virtual
,仍然是pb->f();
正确而pd->f()
不正确的情况
@M.M 对于 OP 的问题的第 1 部分来说,这有点红鲱鱼,但它在第 2 部分中是正确的,即为什么 pb->f()
调用私有派生版本。如果没有虚拟,这将不再有效。【参考方案5】:
struct
的成员默认为公开,class
的成员默认为私有。
所以 B 中的f()
是公共的,当它派生到 D 时,因为你没有明确声明它是公共的,所以根据派生规则,它变成了私有的。
【讨论】:
【参考方案6】:访问说明符仅适用于函数名,它们对如何或何时可以通过其他方式调用函数没有任何限制。如果私有函数通过其名称以外的其他方式(例如,函数指针)可用,则可以在类外部调用它。
对于使用class
关键字声明的类,默认访问说明符是private
。您的代码与以下内容相同:
// ...
class D: public B
private:
int f() return 2;
;
如您所见,f
在D
中是私有的。访问说明符与B
中具有相同名称的任何函数无关。请记住,B::f()
和 D::f()
是两个不同的函数。
virtual
关键字的效果是,如果在引用 D
对象的 B
引用上调用不带范围限定符的 f()
,那么即使它解析为 B::f()
,实际上也是 @而是调用 987654335@。
这个过程仍然使用B::f()
的访问说明符:在编译时检查访问;但调用哪个函数可能是运行时问题。
【讨论】:
我有一个问题,如果D中没有定义f(),那么D::f()
和B::f()
的功能一样吗?在class D
公开吗?
@EricLuo 在那种情况下D::f()
和B::f()
确实是同一个函数,通过名称D::f()
访问函数是由继承规则决定的。 C++ 标准中的确切文本是,“除非在派生类中重新声明,否则基类的成员也被视为派生类的成员。”
终于明白了!还有一个请求,你介意告诉我确切的文字在哪里吗?
@EricLuo see here 查找标准草案(例如 N4140、N3936)然后检查 [class.derived]/2 部分
或者 [class.access.virt] 看我的回答【参考方案7】:
给出的答案说明了正在做什么,但你为什么要这样做,基类调用private
虚函数?
嗯,有一种称为template method pattern 的设计模式使用这种技术,即在派生类中拥有一个调用私有虚函数的基类。
struct B
virtual ~B() ;
int do_some_algorithm()
do_step_1();
do_step_2();
do_step_3();
private:
virtual void do_step_1()
virtual void do_step_2()
virtual void do_step_3()
;
class D : public B
void do_step_1()
// custom implementation
void do_step_2()
// custom implementation
void do_step_3()
// custom implementation
;
int main()
D dInstance;
B * pB = &dInstance;
pB->do_some_algorithm();
这允许我们不将D
类的自定义步骤暴露给public
接口,但同时允许B
使用public
函数调用这些函数。
【讨论】:
该示例没有说明 OP 问题的用例,因为B
中的虚函数是私有的(以及它们在 D
中的实现)。从main
用户无法以任何方式呼叫他们,无论是通过虚拟呼叫还是通过直接呼叫。要说明的是为什么禁止直接调用D::f
是有用的,即使用户可以通过将D 对象引用转换为B 引用,然后虚拟调用f
方法来规避这一点。以上是关于C++继承中成员函数的访问的主要内容,如果未能解决你的问题,请参考以下文章