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 )

  

 

   存在两份虚函数表,结构与多重继承相同。

 

  既然想到了菱形继承,那么顺便看一下虚继承时,虚函数表的构成:

  

 

  此时的虚函数表只有一份(它们的地址相同)。 

   但是,会发现同样的继承结构,虚继承要比普通继承多了一块内存,就是最后的那部分:

  

 

   存储着公共基类的内容,为了防止访问基类成员时的二义性,所以只存了一份,就是这里。

 

  另有一些内容暂未挖掘,之后补充。

posted on 2022-05-17 00:06  __Even  阅读(26)  评论(0编辑  收藏  举报

导航