C++ 基础:虚函数杂记
注意:以下内容完全按照自己的理解记录。
这里的编译器为 MSVC c++14
1.虚函数主要解决的问题
正常来说,通过一种父类的指针指向实际为不同子类的对象时,这个父类的指针无法在运行时正确调用子类的成员,因为它是按指针类型决定的:
观察以下简单代码,尝试理解它的输出(下面代码少了一行iostream的头文件包含):
1 class A 2 { 3 public: 4 void show() 5 { 6 std::cout << "A::show value a " << a << std::endl; 7 } 8 9 int a = 0; 10 }; 11 12 class B : public A 13 { 14 public: 15 void show() 16 { 17 std::cout << "B::show value a " << a << std::endl; 18 } 19 }; 20 21 class C : public A 22 { 23 public: 24 void show() 25 { 26 std::cout << "C::show value a " << a << std::endl; 27 } 28 }; 29 30 int main() 31 { 32 A* pA = new A(); 33 pA->a = 1; 34 35 B* pB = new B(); 36 pB->a = 2; 37 38 C* pC = new C(); 39 pC->a = 3; 40 41 A* ptrs[3] = { pA, pB, pC }; 42 for (size_t i = 0; i < 3; i++) 43 ptrs[i]->show(); 44 45 delete pA; 46 delete pB; 47 delete pC; 48 49 return 0; 50 }
其中,class A 是 class B 与 class C 的父类,每个类都有一个 show 函数,其中只包含一句字符串打印,但每个类的输出内容皆不同。 main 函数中想要做的,是分别制造 class A B C 的实例化对象,存储在数组中,然后让它们调用各自的 show 函数。
输出:
A::show value a 1
A::show value a 2
A::show value a 3
发现,成员变量 a 能够正确地输出,但是函数调用却不是那回事,都是调用的 class A 的 show 函数。
在实际编码中有很多情况,我们都希望将某一种功能细分成不同的子类别,但是我们还要存储在同一个数组中,为了之后方便使用,然后在真正使用这些实例的时候,它们能够使用自己的成员。这就是运行时多态。
重回上面的代码,我想让它输出:
A::show value a 1
B::show value a 2
C::show value a 3
那么我可以这样做:
1 #include <iostream> 2 3 enum EClassType 4 { 5 ECT_A, ECT_B, ECT_C 6 }; 7 8 class A 9 { 10 public: 11 void show() 12 { 13 std::cout << "A::show value a " << a << std::endl; 14 } 15 16 int a = 0; 17 EClassType t = ECT_A; 18 }; 19 20 class B : public A 21 { 22 public: 23 B() { t = ECT_B; } 24 25 void show() 26 { 27 std::cout << "B::show value a " << a << std::endl; 28 } 29 30 }; 31 32 class C : public A 33 { 34 public: 35 C() { t = ECT_C; } 36 37 void show() 38 { 39 std::cout << "C::show value a " << a << std::endl; 40 } 41 }; 42 43 int main() 44 { 45 A* pA = new A(); 46 pA->a = 1; 47 48 B* pB = new B(); 49 pB->a = 2; 50 51 C* pC = new C(); 52 pC->a = 3; 53 54 A* ptrs[3] = { pA, pB, pC }; 55 for (size_t i = 0; i < 3; i++) 56 { 57 A* p = ptrs[i]; 58 switch (p->t) 59 { 60 case ECT_A: 61 p->show(); 62 break; 63 64 case ECT_B: 65 static_cast<B*>(p)->show(); 66 break; 67 68 case ECT_C: 69 static_cast<C*>(p)->show(); 70 break; 71 72 default: 73 break; 74 } 75 } 76 77 delete pA; 78 delete pB; 79 delete pC; 80 81 return 0; 82 }
我用一个枚举类型来标记每个实例化的对象是属于那个类,这样在真正调用时判断一下,然后在转一下类型就行了。
暂且不说这其实是编译时的确定它到底应该调用哪个类的成员,并不是运行时的——这种写法也实在过于沙雕。
现在,使用虚函数的方式来更改一下代码,看看输出和代码内容都产生了哪些变化:
1 #include <iostream> 2 3 class A 4 { 5 public: 6 virtual void show() 7 { 8 std::cout << "A::show value a " << a << std::endl; 9 } 10 11 int a = 0; 12 }; 13 14 class B : public A 15 { 16 public: 17 void show() 18 { 19 std::cout << "B::show value a " << a << std::endl; 20 } 21 }; 22 23 class C : public A 24 { 25 public: 26 void show() 27 { 28 std::cout << "C::show value a " << a << std::endl; 29 } 30 }; 31 32 int main() 33 { 34 A* pA = new A(); 35 pA->a = 1; 36 37 B* pB = new B(); 38 pB->a = 2; 39 40 C* pC = new C(); 41 pC->a = 3; 42 43 A* ptrs[3] = { pA, pB, pC }; 44 for (size_t i = 0; i < 3; i++) 45 { 46 ptrs[i]->show(); 47 } 48 49 delete pA; 50 delete pB; 51 delete pC; 52 53 return 0; 54 }
输出:
A::show value a 1
B::show value a 2
C::show value a 3
相对于一开始的代码, 只新增了一个关键字 virtual,将 class A 中的函数声明为虚函数,就完美解决了问题。实际上,当基类 A 中 show 函数声明为虚函数时,原本 class B 与 class C 中用于覆盖基类的 show 函数,此时也是虚函数。
2. 虚函数究竟做了什么?怎么做到的?
在上面的实验中,可以简单总结出一个结论:
show 函数不是虚函数时,调用 show 函数是按照指针类型来调用的;
show函数为虚函数时,调用 show 函数是按照指针指向的实际实例来调用的;
现在我想知道它内部到底做了什么?
通过 vs 的 debug 断点,可以看到指针 pA 里面的内容:
可以看到,里面放了一个叫做 vftable 的函数指针数组(虚函数表指针),里面只有一个元素,就是 show 的函数指针。每个类的vftable都包含了自己的 show 指针。
现在,更改一下代码,看一看拥有多个虚函数的时候,这个指针指向的内容怎样变化:
1 #include <iostream> 2 3 class A 4 { 5 public: 6 virtual void show() 7 { 8 std::cout << "A::show value a " << a << std::endl; 9 } 10 11 virtual void show2() 12 { 13 std::cout << "A::show2 value a " << a << std::endl; 14 } 15 16 int a = 0; 17 }; 18 19 class B : public A 20 { 21 public: 22 void show() 23 { 24 std::cout << "B::show value a " << a << std::endl; 25 } 26 }; 27 28 class C : public A 29 { 30 public: 31 void show() 32 { 33 std::cout << "C::show value a " << a << std::endl; 34 } 35 }; 36 37 int main() 38 { 39 A* pA = new A(); 40 pA->a = 1; 41 42 B* pB = new B(); 43 pB->a = 2; 44 45 C* pC = new C(); 46 pC->a = 3; 47 48 A* ptrs[3] = { pA, pB, pC }; 49 for (size_t i = 0; i < 3; i++) 50 { 51 ptrs[i]->show(); 52 } 53 54 std::cout << "class A size: " << sizeof(A) << std::endl; 55 56 delete pA; 57 delete pB; 58 delete pC; 59 60 return 0; 61 }
然后再次试验:子类 B 中添加成员,尝试将show2覆盖
1 #include <iostream> 2 3 class A 4 { 5 public: 6 virtual void show() 7 { 8 std::cout << "A::show value a " << a << std::endl; 9 } 10 11 virtual void show2() 12 { 13 std::cout << "A::show2 value a " << a << std::endl; 14 } 15 16 int a = 0; 17 }; 18 19 class B : public A 20 { 21 public: 22 void show() 23 { 24 std::cout << "B::show value a " << a << std::endl; 25 } 26 27 void show2() 28 { 29 std::cout << "B::show2 value a " << a << std::endl; 30 } 31 }; 32 33 class C : public A 34 { 35 public: 36 void show() 37 { 38 std::cout << "C::show value a " << a << std::endl; 39 } 40 }; 41 42 int main() 43 { 44 A* pA = new A(); 45 pA->a = 1; 46 47 B* pB = new B(); 48 pB->a = 2; 49 50 C* pC = new C(); 51 pC->a = 3; 52 53 A* ptrs[3] = { pA, pB, pC }; 54 for (size_t i = 0; i < 3; i++) 55 { 56 ptrs[i]->show(); 57 } 58 59 std::cout << "class A size: " << sizeof(A) << std::endl; 60 61 delete pA; 62 delete pB; 63 delete pC; 64 65 return 0; 66 }
其中,虚函数表中的 show2 指针也变成B类自己的了。
现在清楚,当实现多态时,是通过这个虚函数表来找到真正想要调用的函数。无论是父类还是子类,都拥有自己的虚函数表。当某个函数父类拥有却子类不存在时,子类的虚函数表也会将父类的虚函数指针继承下来。
再次更改代码以试验:当存在多重继承时,虚函数表的构成:
1 #include <iostream> 2 3 class A 4 { 5 public: 6 virtual void show() 7 { 8 std::cout << "A::show value a " << a << std::endl; 9 } 10 11 virtual void show2() 12 { 13 std::cout << "A::show2 value a " << a << std::endl; 14 } 15 16 int a = 0; 17 }; 18 19 class A2 20 { 21 public: 22 virtual void show() 23 { 24 std::cout << "A2::show value a2 " << a2 << std::endl; 25 } 26 27 virtual void show2() 28 { 29 std::cout << "A2::show2 value a2 " << a2 << std::endl; 30 } 31 32 int a2 = 0; 33 }; 34 35 class B : public A, public A2 36 { 37 public: 38 void show() 39 { 40 std::cout << "B::show value a " << A::a << std::endl; 41 } 42 43 void show2() 44 { 45 std::cout << "B::show2 value a " << A::a << std::endl; 46 } 47 int b = 10086; 48 }; 49 50 class C : public A 51 { 52 public: 53 void show() 54 { 55 std::cout << "C::show value a " << a << std::endl; 56 } 57 }; 58 59 int main() 60 { 61 A* pA = new A(); 62 pA->a = 1; 63 64 B* pB = new B(); 65 pB->a = 2; 66 67 C* pC = new C(); 68 pC->a = 3; 69 70 A* ptrs[3] = { pA, pB, pC }; 71 for (size_t i = 0; i < 3; i++) 72 { 73 ptrs[i]->show(); 74 } 75 76 delete pA; 77 delete pB; 78 delete pC; 79 80 return 0; 81 }
存在两个虚函数表,分别是属于两个父类的。
再次更改代码以试验:当存在菱形继承时,虚函数表的构成:
(此处不再贴代码了——新增class D, 继承 B C,B C 继承 A )
存在两份虚函数表,结构与多重继承相同。
既然想到了菱形继承,那么顺便看一下虚继承时,虚函数表的构成:
此时的虚函数表只有一份(它们的地址相同)。
但是,会发现同样的继承结构,虚继承要比普通继承多了一块内存,就是最后的那部分:
存储着公共基类的内容,为了防止访问基类成员时的二义性,所以只存了一份,就是这里。
另有一些内容暂未挖掘,之后补充。