只有通过对象指针和引用调用虚函数才能产生多态?

luuillu 2014-07-23 04:52:55
多年前,我曾在CSDN问过一个问题“http://bbs.csdn.net/topics/370154900”。这个问题的答案是:
多态的三个必要条件:
1.存在继承2.虚方法重写3.父类(指针或者引用)指向子类对象。
当时信以为真。
然而今天看C++标准。得到了一个反例:

#include <iostream>

struct A{
virtual void f()
{
puts("A");
}
};
struct B: A{
void f()
{
puts("B");
}
};
struct C:B{
using A::f;
};

int main(int argc, char* argv[])
{


C c;
c.f(); //输出B
c.C::f(); //输出A
}


从上面的结果可以看出直接通过对象调用,也产生了多态的效果。C++对此的解释是:
The rules for member lookup (10.2) are used to determine the final overrider for a virtual function in the scope of a
derived class but ignoring names introduced by using-declarations.
尽管C++没有直接说这是多态,但也没否认。而且在标准中没有找到只有指针或引用才能产生多态的说法。C++标准中的描述为:the interpretation of the call of a virtual function depends on the type of the object for which it is called (the dynamic type)。

因此,调用虚函数总能产生多态,无论是通过引用,指针还是对象。
我的问题是:上面的分析正确吗?
...全文
818 20 打赏 收藏 转发到动态 举报
写回复
用AI写文章
20 条回复
切换为时间正序
请发表友善的回复…
发表回复
ri_aje 2014-07-26
  • 打赏
  • 举报
回复
再加一句个人理解,主楼的程序是虚函数调用,但是不能算多态。 c++ 中的多态通过虚函数实现,但不是任何调用虚函数的动作都一定实现了多态的行为,即虚函数是多态的必要条件;我猜这也是 BS 说 (#14 引用):To get polymorphic behavior in C++, the member functions called must be virtual and objects must be manipulated through pointers or references. 主楼的程序只有 virtual function,没有 objects manipulated through pointers or references,所以没有多态行为。
ri_aje 2014-07-26
  • 打赏
  • 举报
回复
引用 18 楼 xiaohuh421 的回复:
[code=c#include <iostream> struct A{ virtual void f() { puts("A"); } }; struct B: A{ void f() { puts("B"); } }; struct C:B{ using A::f; }; int main(int argc, char* argv[]) { C c; c.f(); //输出B c.C::f(); //输出A }][/code] 这个例子的关键在于 using A::f; 如果没有它, 可能大家都能很好的理解各种情况下输出 . 有它的时候, 实际上编译器(VS2012)在编译c的 f 函数时, 会使用f函数的地址. 当有vitural的时候, 就是f函数在虚表中的首地址. 这时由于虚表内存布局可以知道, 派生类的始终在最前, 刚好就会得到派生类,即B类的f的地址了. 当没有virtual的时候, c.f还是使用A::f的地址, 因为没有虚函数, 就没有虚表, 那f的地址仍然是A::f的地址. 所以调用了A的f. 所以, 会导致你的结果的最终原因是: 虚表的内存布局导致的: 派生类的总放在最前. 只是正好行为像是在使用多态性. 不过你也可以理解为bug, 就好像我们可以使用指针偏移来访问类的私有成员一样, 符合语法规定,但破坏了语法规定. 我给这种情况下的定义是: Hack 编程.
我觉得你这种理解刚好反了。 有 using A::f 的时候,对 f 的 name lookup 决议为 A::f,后者是个虚函数,根据标准的要求,需要调用该虚函数在派生类动态类型 (这里是 C) 中的 final overrider,即 B::f,所以编译器生成了调用 B::f 的代码。正如你观察到的,编译器使用的机制就是虚函数调用的机制,但这不是"正好行为像..."或者什么 hack 之类的动作,这就是标准 c++ 规定的语义。
luuillu 2014-07-25
  • 打赏
  • 举报
回复
多谢各位热情回答,多谢lovesmiles亲自做实验。
引用 13 楼 xiaohuh421 的回复:
而你这里是在使用派生类, 跟多态没什么关系了. 即使你的f()函数不使用vitrual同样能达到你程序效果.
不是这样,这个例子的特殊就在于把vitrual去掉后,程序会输出不同的结果。
引用 14 楼 pengzhixi 的回复:
从c++之父的c++编程语言书上拷贝的一段文字:To get polymorphic behavior in C++, the member functions called must be virtual and objects must be manipulated through pointers or references.
由此是否可以推断出,直接通过对象调用函数 f , 执行的结果应与 f 是否是虚函数无关? 那么可以推断出前面所举的例子,是C++标准中的一个Bug,即 10.3.2: The rules for member lookup (10.2) are used to determine the final overrider for a virtual function in the scope of a derived class but ignoring names introduced by using-declarations. 这个规则是错误,应该去掉。
xiaohuh421 2014-07-25
  • 打赏
  • 举报
回复
[code=c#include <iostream> struct A{ virtual void f() { puts("A"); } }; struct B: A{ void f() { puts("B"); } }; struct C:B{ using A::f; }; int main(int argc, char* argv[]) { C c; c.f(); //输出B c.C::f(); //输出A }][/code] 这个例子的关键在于 using A::f; 如果没有它, 可能大家都能很好的理解各种情况下输出 . 有它的时候, 实际上编译器(VS2012)在编译c的 f 函数时, 会使用f函数的地址. 当有vitural的时候, 就是f函数在虚表中的首地址. 这时由于虚表内存布局可以知道, 派生类的始终在最前, 刚好就会得到派生类,即B类的f的地址了. 当没有virtual的时候, c.f还是使用A::f的地址, 因为没有虚函数, 就没有虚表, 那f的地址仍然是A::f的地址. 所以调用了A的f. 所以, 会导致你的结果的最终原因是: 虚表的内存布局导致的: 派生类的总放在最前. 只是正好行为像是在使用多态性. 不过你也可以理解为bug, 就好像我们可以使用指针偏移来访问类的私有成员一样, 符合语法规定,但破坏了语法规定. 我给这种情况下的定义是: Hack 编程.
ri_aje 2014-07-25
  • 打赏
  • 举报
回复
引用 16 楼 luuillu 的回复:
多谢各位热情回答,多谢lovesmiles亲自做实验。 [quote=引用 13 楼 xiaohuh421 的回复:] 而你这里是在使用派生类, 跟多态没什么关系了. 即使你的f()函数不使用vitrual同样能达到你程序效果.
不是这样,这个例子的特殊就在于把vitrual去掉后,程序会输出不同的结果。
引用 14 楼 pengzhixi 的回复:
从c++之父的c++编程语言书上拷贝的一段文字:To get polymorphic behavior in C++, the member functions called must be virtual and objects must be manipulated through pointers or references.
由此是否可以推断出,直接通过对象调用函数 f , 执行的结果应与 f 是否是虚函数无关? 那么可以推断出前面所举的例子,是C++标准中的一个Bug,即 10.3.2: The rules for member lookup (10.2) are used to determine the final overrider for a virtual function in the scope of a derived class but ignoring names introduced by using-declarations. 这个规则是错误,应该去掉。[/quote] c++11 已经没有这句了。不过我觉得也算不上 bug,可能就是措辞很迷惑吧。
赵4老师 2014-07-24
  • 打赏
  • 举报
回复
《深度探索C++对象模型》 《C++反汇编与逆向分析技术揭秘》
pengzhixi 2014-07-24
  • 打赏
  • 举报
回复
从c++之父的c++编程语言书上拷贝的一段文字:To get polymorphic behavior in C++, the member functions called must be virtual and objects must be manipulated through pointers or references.
xiaohuh421 2014-07-24
  • 打赏
  • 举报
回复
多态的关键在于: 使用基类指针或者引用 能访问派生类中的方法. 而你这里是在使用派生类, 跟多态没什么关系了. 即使你的f()函数不使用vitrual同样能达到你程序效果. 我觉得之所以会使用多态这个特性. 是方便遍历调用同一类东西, 统一接口.
勤奋的小游侠 2014-07-24
  • 打赏
  • 举报
回复
引用 11 楼 baichi4141 的回复:
我对多态的理解就是“不需要知道对象的实际类型,也能够调用正确的功能” 所以楼主的写法在我看来不算多态,你不关心对象的类型直接指定某个功能,哪来的“多”啊 至于咬文嚼字“多态的定义是什么”,抱歉我不关心。
真心受不了你这种那么叼的态度,学术讨论多少就是咬文嚼字。你不想咬这些定义,那你就楼主举的例子做一个结论,到底有没有用到“你的理解的多态”?你只需要回答yes或no,无须更多文字。
baichi4141 2014-07-24
  • 打赏
  • 举报
回复
我对多态的理解就是“不需要知道对象的实际类型,也能够调用正确的功能” 所以楼主的写法在我看来不算多态,你不关心对象的类型直接指定某个功能,哪来的“多”啊 至于咬文嚼字“多态的定义是什么”,抱歉我不关心。
「已注销」 2014-07-23
  • 打赏
  • 举报
回复
虚函数是动态绑定的。编译器生成的函数调用指令是从动态方法表中去找函数的偏移,不是静态固定的。在运行时,通过虚拟方法表找到函数地址。从而达到多态。 这跟以对象本身调用还是以对象的引用或指针调用没什么关系。书上的翻译很拗口,有时候不一定是字面的意思,关键是弄清楚c++对象内存模型,包括成员函数,成员变量是一种怎样的存在。
勤奋的小游侠 2014-07-23
  • 打赏
  • 举报
回复
引用 4 楼 luuillu 的回复:
之所以说它是多态是因为“c.f(); //输出B”,它是通过虚函数表找到的f,否则它应该输出A。 如果f不是虚函数,那么c.f()输出的是A。 另外又想到了一个问题,如果把B中的f 变成私有,将会输出什么?

struct A{
	virtual void f()
	{
		puts("A");
	}
};
struct B: A{
	private: void f()
	{
		puts("B");
	}
};
struct C:B{
	using A::f;
	
};

int main(int argc, char* argv[])
{
   

	C c;
	c.f();
}
根据我做的实验结果,楼主,我撑你,记得把分给我啊。哈哈
勤奋的小游侠 2014-07-23
  • 打赏
  • 举报
回复
引用 5 楼 pengzhixi 的回复:
... c.f(); //输出B C的最终重载版本就是B的f()所以输出B是没任何问题的 c.C::f(); //输出A 至于这里输出A你看下member look up规则 Member name lookup determines the meaning of a name (id-expression) in a class scope
这是vs2008relase反汇编的结果,你们分析一下红色的是什么意思?这不是用到了虚表吗?绿色的反而是正直就call A::f这个函数地址了 int main(int argc, char* argv[]) { 01021010 push ecx C c; c.f(); //Êä³öB 01021011 lea ecx,[esp] 01021014 mov dword ptr [esp],offset C::`vftable' (1022118h) 0102101B call dword ptr [C::`vftable' (1022118h)] c.C::f(); //Êä³öA 01021021 push offset string "A" (102210Ch) 01021026 call dword ptr [__imp__puts (10220A8h)] }
  • 打赏
  • 举报
回复
勤奋的小游侠 2014-07-23
  • 打赏
  • 举报
回复
引用 4 楼 luuillu 的回复:
之所以说它是多态是因为“c.f(); //输出B”,它是通过虚函数表找到的f,否则它应该输出A。 如果f不是虚函数,那么c.f()输出的是A。 另外又想到了一个问题,如果把B中的f 变成私有,将会输出什么?

struct A{
	virtual void f()
	{
		puts("A");
	}
};
struct B: A{
	private: void f()
	{
		puts("B");
	}
};
struct C:B{
	using A::f;
	
};

int main(int argc, char* argv[])
{
   

	C c;
	c.f();
}
呵呵,这个问题到了这里就有点意思了。
pengzhixi 2014-07-23
  • 打赏
  • 举报
回复
... c.f(); //输出B C的最终重载版本就是B的f()所以输出B是没任何问题的 c.C::f(); //输出A 至于这里输出A你看下member look up规则 Member name lookup determines the meaning of a name (id-expression) in a class scope
luuillu 2014-07-23
  • 打赏
  • 举报
回复
之所以说它是多态是因为“c.f(); //输出B”,它是通过虚函数表找到的f,否则它应该输出A。 如果f不是虚函数,那么c.f()输出的是A。 另外又想到了一个问题,如果把B中的f 变成私有,将会输出什么?

struct A{
	virtual void f()
	{
		puts("A");
	}
};
struct B: A{
	private: void f()
	{
		puts("B");
	}
};
struct C:B{
	using A::f;
	
};

int main(int argc, char* argv[])
{
   

	C c;
	c.f();
}
ggglivw 2014-07-23
  • 打赏
  • 举报
回复
引用 2 楼 zhousitiaoda 的回复:
应该是只有通过对象指针和引用调用虚函数才能产生运行期多态?除此之外还有通过模板实现的编译期多态。 另外楼主举得例子根本不算多态。
+1. 项目不会有你那种用法
zhousitiaoda 2014-07-23
  • 打赏
  • 举报
回复
应该是只有通过对象指针和引用调用虚函数才能产生运行期多态?除此之外还有通过模板实现的编译期多态。 另外楼主举得例子根本不算多态。
mujiok2003 2014-07-23
  • 打赏
  • 举报
回复
引用
the interpretation of the call of a virtual function depends on the type of the object for which it is called (the dynamic type)。
这句话是没有错的。
c.C::f();  //输出A
这里没用到它的动态类型。 只是::改变了name lookup的范围, 我不认为这也算多态。

64,682

社区成员

发帖
与我相关
我的任务
社区描述
C++ 语言相关问题讨论,技术干货分享,前沿动态等
c++ 技术论坛(原bbs)
社区管理员
  • C++ 语言社区
  • encoderlee
  • paschen
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
  1. 请不要发布与C++技术无关的贴子
  2. 请不要发布与技术无关的招聘、广告的帖子
  3. 请尽可能的描述清楚你的问题,如果涉及到代码请尽可能的格式化一下

试试用AI创作助手写篇文章吧