多态内部是如何实现的

这两天一直在寻找这方面的信息,发现这个问题我起得太大了.

对于C++来说,多态是靠虚函数实现的,所以问题就转变为虚函数的相关机制。当一个类中定义了一个或以上的虚函数,C++就会为这个类创建一个虚函数表(vtable),而指向这个虚函数表内存的指针则会存放在这个类的内存当中。当派生类调用重写的虚函数方法,程序内部就会根据这个类的虚函数表查找对应的虚函数,本质上就是传递了这个vtable指针,借助这个指针来判断是调用父类的虚函数还是派生出来的子类虚函数。所以也可以知道,一个类函数的调用并不是在编译时刻被确定的,而是在运行时刻被确定的,它是动态调用的。

这里引用了几篇回答。

多态的最根本好处在于,你不必再向对象询问“你是什么类型”而后根据得到的答案调用对象的 某个行为一你只管调用该行为就是了,其他的一切多态机制都会为你安排妥当。

——《重构:改善既有代码的设计》

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class A  
{
public:
virtual void foo() {
cout<<"A::foo() is called"<<endl;
}
};
class B:public A
{
public:
void foo()
{
cout<< "B::foo() is called" <<endl;
}
};
int main(void)
{
A *a = new B();
a->foo(); // 在这里,a虽然是指向A的指针,但是被调用的函数(foo)却是B的!
return 0;
}

这个例子是虚函数的一个典型应用,通过这个例子,也许你就对虚函数有了一些概念。它虚就虚在所谓“推迟联编”或者“动态联编”上,一个类函数的调用并不是在编译时刻被确定的,而是在运行时刻被确定的。由于编写代码的时候并不能确定被调用的是基类的函数还是哪个派生类的函数,所以被成为“虚”函数。

​ 虚函数只能借助于指针或者引用来达到多态的效果。

作者:wuxinliulei

链接:https://www.zhihu.com/question/23971699/answer/69592611

来源:知乎

C++ 编译器必须为每一个多态类至少创建一个【虚函数表(vtable)】,其本质是一个【函数指针数组】,其中存放着这个类所有的【虚函数的地址】及该类的类型信息,其中也包括那些【继承但未改写(Override)的虚函数】。

——林锐博士《高质量程序设计指南第三版》

如何证明C++创建了虚函数表?

该标题下为CSDN博主「annjeff」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/annjeff/article/details/106734773

这是一个C++空类

1
2
3
4
5
6
7
8
9
10
#include <iostream>
using namespace std;

class A{};

int main(int argc, char const *argv[])
{
cout << sizeof(A) << endl; //1
return 0;
}

为什么 C++ 空类大小是 1 ?

C++ 不允许任何一个对象大小为 0 ,因为这样无法为该变量分配存储空间。
当类为空时,C++ 编译器会向其中插入一个字节的数据,因此空类类型大小为 1 字节。

C++ 非空类大小

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
using namespace std;

class A{
int m_a;
};

int main(int argc, char const *argv[])
{
cout << sizeof(A) << endl; //4
return 0;
}

就本例而言,因为此时 A 含有一个 int 型成员变量,因此编译器不会再给 A 类增加 1 个的数据,所以本例 A 的大小为 4。

总体而言,一个不含虚函数的类,类的大小是【大于或等于】类内所有非静态成员变量的总和。因为存在内存对齐问题,因此可能会大于非静态成员总和。

成员函数是否占用类的大小?

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
using namespace std;

class A{
void fun(){}
int m_a;
};

int main(int argc, char const *argv[])
{
cout << sizeof(A) << endl; //4
return 0;
}

由本例可知,非虚成员函数,不占用类的大小。

含虚函数的类的大小

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
using namespace std;

class A{
virtual void vfun(){}
};

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

cout << sizeof(A) << endl; //32 位机器 大小为 4
return 0;
}

本例中,类型 A 的大小为 4 。此时类为空类,按理讲应该是 大小为 1。而此时不为 1 ,说明类中含有别的成员==> 此处正是 指向【虚函数表】的指针 【* __vptr】 所占用的存储空间。

含多个虚函数的类大小

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
using namespace std;

class A{
public:
virtual void vfun1(){}
virtual void vfunc2(){}
virtual void vfunc3(){}
};

int main(int argc, char const *argv[])
{
cout << sizeof(A) << endl; //32 位机器 大小为 4
return 0;
}

此处可以得知,虚函数不占用类对象的存储空间,所以含有一个以上的虚函数的类对象大小与仅含一个虚函数大小相同。因为:针对每个类,只维护一个【虚函数表(函数指针数组数组)】用于存放该类中虚函数的地址,每个【含一个及以上虚函数的对象都会含有一个指向该类虚函数表的指针】

非虚函数例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
using namespace std;

class B{
public:
void fun(){
cout << "B func" << endl;
}
};
class D:public B{
public:
void func(){
cout << "D func" << endl;
}
};

int main(int argc, char const *argv[])
{
B* b = new B;
b->func(); // label1: B func
D* d = new D;
d->func(); // label2: D func
B* pb = new D;
pb->func();// label3: B func
return 0;
}

label1 处:
基类指针指向基类对象,自然调用基类中的函数。

label2 处:
派生类与基类的【函数同名时】,【子类会覆盖掉父类所有的同名函数】。 因此此处调用的是派生类中的同名函数。

label3处:
此时调用基类中的同名成员,因为不存虚函数故而没有动态绑定,在父类作用域下,自然调用父类的同名函数。

虚函数例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
using namespace std;

class B{
public:
virtual void VFun(){
cout << "B vFunc" << endl;
}
};
class D:public B{
public:
void vFunc(){
cout << "D vFunc" << endl;
}
};

int main(int argc, char const *argv[])
{
B* b = new B;
b->vFunc(); // label1: B vFunc
D* d = new D;
d->vFunc(); // label2: D vFunc
B* pb = new D;
pb->vFunc();// label3: D vFunc
return 0;
}

label1 处:基类指针指向基类对象,调用的是基类中的 vFunc

label2 处:
当子类与父类拥有同名的成员函数,子类会隐藏父类中所有版本的同名成员函数。

label3 处:
因为 派生类【覆盖】(重写)了基类虚函数,给出了派生类的版本,此时 派生类中【虚函数表内 vFun 函数的指针替换为派生类 vFun 的函数指针】,故而由基类实现了【动态绑定】调用了子类同名函数。