C++ 多态和虚函数

一. 先搞清override overload overwrite的区别

 

1. overload(重载)(不是多态)

在C++程序中,可以将语义、功能相似的几个函数用同一个名字表示,但参数不同(包括类型、顺序不同),即函数重载。
(1)相同的范围(在同一个类中);
(2)函数名字相同;
(3)参数不同;
(4)virtual 关键字可有可无。

2. override(覆盖)(运行时多态、虚函数)

是指派生类函数覆盖基类函数,特征是:
(1)不同的范围(分别位于派生类与基类);
(2)函数名字相同;
(3)参数相同;
(4)基类函数必须有virtual 关键字。

3. overwrite(重写)(编译时多态)

是指派生类的函数屏蔽了与其同名的基类函数,规则如下:
(1)如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。
(2)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)。

 

二. 静态联编和动态联编(运行时多态和编译时多态)

  • 在C++中,多态性的实现和联编这一概念有关。一个源程序经过编译,链接,成为可执行文件的过程是把可执行代码连接在一起的过程。
  • 在编译过程中进行联编被称为静态联编(static binding),编译器生成的能够在程序运行时选择正确的虚方法的代码,被称为动态联编(dynamic binding)。
  • 静态联编也称为编译时多态性,主要通过函数重写实现。动态联编也称为运行时多态性。主要通过继承和虚函数来实现。
  • 编译器对非虚方法使用静态联编,一个父类指针指向一个子类时,静态联编调用的是父类函数。
    编译器对虚方法使用动态联编,运行时程序才确定对象的类型,此时一个父类指针指向一个子类,调用的是子类的函数。

 

三. 动态联编的工作原理(虚函数表)

  • 编译器处理虚函数的方法是:给每个对象添加一个隐藏成员,用于保存一个指向函数地址数组的指针。这个数组称为“虚函数表”。
  • 没有虚函数的C++类,是不会有虚函数表的。
  • 虚函数表(virtual function table,vtbl):存储了为类对象进行声明的虚函数的地址。

1. 虚表工作原理

虚函数表实质是一个指针数组,里面存的是虚函数的函数指针。

调用虚函数的时候,程序将查看存储在对象中的虚表地址(也就是说按照实例所属的类来看的,而不是按照指针类型。同一类共用一张虚表,只是每一个对象都一个指向该虚表的指针)【前几天面试遇到一个面试官,对方坚持说是一个实例里面一张虚表,就感觉很奇怪,后来问了一些大佬,回复是看编译器实现,两种都可能。不过多数还是一个指针,共用一张虚表的。】,然后转向相应的函数地址表(放在类中,一个类只有一张)。如果使用类声明中定义的第一个虚函数,则程序将使用数组中的第一个函数地址;如果使用类声明中的第三个虚函数,程序将使用地址为数组中第三个元素的函数。

而这张虚表,是会根据你是否有override、是否有overwrite来决定每个函数指针指向的位置的。

 

2. 举个栗子(看完再回去1理解原理可能更好理解)

  • 当使用了虚函数override时,可以明显看到,两个虚表指针 _vfptr 不一样,同时指向两个函数的位置也不一样(红圈)。
  • 在找函数时候都是找虚表中的 [2],猜测[0]和[1]是默认的构造和析构函数。
#include<iostream>
using namespace std;

class father_test {
public:
	virtual void test() { cout << "test1" << endl; }
};

class son_test :public father_test {
public:
	virtual void test() { cout << "test2" << endl; }

};


int main() {
	son_test son;
	father_test father;
	father_test *p1,*p2;
	p1 = &son;
	p2 = &father;
	p1->test();
	p2->test();
	for (int i = 0; i < 10; i++);

	cout << endl;
	getchar();
	return 0;
}
  • 当使用了vitrual声明,但没有进行override的时候,可以看到还是有两个虚表 _vfptr 的指针,只是这时函数指向的地址一样(红圈)。

#include<iostream>
using namespace std;

class father_test {
public:
	virtual void test() { cout << "test1" << endl; }
};

class son_test :public father_test {
public:
	//virtual void test() { cout << "test2" << endl; }

};


int main() {
	son_test son;
	father_test father;
	father_test *p1,*p2;
	p1 = &son;
	p2 = &father;
	p1->test();
	p2->test();
	for (int i = 0; i < 10; i++);

	cout << endl;
	getchar();
	return 0;
}
  • 为了验证上面对[2]的猜测,在test函数前面,多加了一个test2函数。果然可以看到这时调用test变成[3]

#include<iostream>
using namespace std;

class father_test {
public:
	virtual void test2() {}
	virtual void test() { cout << "test1" << endl; }
	father_test(){}
	~father_test(){}
};

class son_test :public father_test {
public:
	virtual void test2(){}
	virtual void test() { cout << "test2" << endl; }
	son_test(){}
	~son_test(){}
};


int main() {
	son_test son;
	father_test father;
	father_test *p1,*p2;
	p1 = &son;
	p2 = &father;
	p1->test();
	p2->test();
	for (int i = 0; i < 10; i++);

	cout << endl;
	getchar();
	return 0;
}
  • 再来看虚表什么时候真正初始化的

运行到①的时候,此时有指针p1和p2,但显示“无法读取内存”

运行到②,p1指针指向一个实例对象,此时可以看到_vfptr了,p2还是没有

运行到③,两个指针的虚表都有了内容。

  • 那么问题又来了,是什么时候有的虚表。指针初始化的时候才有吗?显然不是,根据前面的理论可以知道,其实在对象初始化的时候就有。
    所以回到 son_test son; 这句语句执行完,而father_test father; 还没执行之前
    可看到,对象son里面已经有虚表了,但father的虚表还是一堆问号

 

四. 多重继承时的虚函数表

  • 多重继承情况下,和上面类似。只是有几个父类,就有几个vtbl和_vfptr

  • 10
    点赞
  • 56
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值