深入C++虚函数表

本文深入探讨了面向对象编程中的多态性,包括编译时和运行时多态的区别,重点讲解了运行时多态(晚绑定)及虚函数的概念和作用。通过实例展示了虚函数如何实现动态绑定,以及虚函数表的原理和内存布局。同时,对比了重载、覆盖和隐藏的区别,并分析了虚函数在继承和多态中的关键角色。最后,讨论了虚函数表在单一继承和多继承情况下的不同布局和覆盖情况。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

文章目录

什么是多态?

多态的分类:

 运行时的多态

 虚函数的定义:

运行时的多态(晚绑定)

虚函数注意:

虚函数:

前提知识:函数指针的判别

重载、覆盖(重写)、隐藏(重定义)的对比 

 虚函数表

单一继承,无虚函数覆盖: 

单一继承,有覆盖:

多继承下,无覆盖:

多继承下,有覆盖:


什么是多态?

        多态性是面向程序设计的关键技术之一。若程序设计语言不支持多态性,不能称为面向对象的语言。

        多态性是考虑在不同层次的类中,以及在同一类中,同名的成员的关系问题。    

        说白了就是指在父类中定义的属性和方法被子类继承之后,可以具有不同的数据类型或表现出不同的行为,这使得同一个属性或方法在父类及其各个子类中具有不同的含义。

多态的分类:

        多态又分为编译时的多态性(静态的多态性),函数重载,运算符的重载都属于编译时的多态,泛型编程模板也属于静多态

        以类的虚成员函数为基础的运行时的多态

 运行时的多态

 虚函数的定义:

        虚函数是一个类的成员函数,定义格式如下:

        virtual返回类型 函数名 (参数表)

        关键字virtual指明该成员函数为虚函数。virtual仅用于类定义中,如果虚函数在类外定义,则不可加virtual。当某一个类的一个类成员函数被定义为虚函数,则由该类派生出来的所有派生类中该函数始终保持虚函数的特征。

        注意:运行时的多态性:公有继承+基类和子类有的同名同参的虚函数+基类的指针或者引用指向基类对象或者派生类对象

运行时的多态(晚绑定)

定义父类A,子类B继承A,编写测试用例父类对象传入指针,引用

class A
{
public:
	virtual void fn()
	{
		cout << "A::fn" << endl;
	}
};
class B :public A
{
public:
	virtual void fn()
	{
		cout << "B::fn" << endl;
	}
};
void test1(A a)
{
	a.fn();
}
void test2(A& pa)
{
	pa.fn();
}
void test(A* pa)
{
	pa->fn();
}
void main()
{
	A a;
	B b;
	a.fn();//a直接调用fn
	b.fn();//b直接调用fn
	test(&a);//传入a对象地址,找到A下虚表
	test(&b);//传入b对象地址,找到B下虚表
	test2(a);//同理,找到相应类下虚指针,指向所对应虚表,调用对应虚函数
	test2(b);
}

虚函数注意:

虚函数的默认参数是静态绑定的,在重新定义虚函数时候,不重新定义继承而来的参数值
除非在调用时候实际的传递想要的参数

class Parent
{
public:
	virtual void fn(int a = 10)
	{
		cout << "parent fn a = " << a << endl;
	}
};
class Child :public Parent
{
public:
	virtual void fn(int b = 20)
	{
		cout << "child fn b = " << b << endl;
	}
};
void main()
{
	Child cc;
	Parent* p = &cc;
	p->fn();      //child fn b=10 默认参数静态绑定
	p->fn(100);    //child fn b=100
}

给一个例题看看输出结果: 

class Parent
{
public:
	void print()
	{
		cout << "parent print" << endl;
		test();
	}
	virtual void test()
	{
		cout << "parent test" << endl;
	}
};
class Child :public Parent
{
public:
	void show()
	{
		cout << "Child show" << endl;
		print();
	}
	virtual void test()
	{
		cout << "child test" << endl;
	}
};
void main()
{
	Child cc;
	cc.show();
}
理解函数调用顺序
运行结果:
Child show
parent print
child test

虚函数:

注意以下几点:

  1. 派生类中定义虚函数必须和基类中的虚函数同名外,还必须同参数表,同返回类型。否则被认为是重载,而不是虚函数。如基类中返回基类指针,派生类中返回派生类指针是允许的,这是一个例外
  2. 只有类的成员函数才能说明为虚函数,这是因为虚函数仅仅适用于有继承关系的类对象。友元函数和全局函数不能作为虚函数
  3. 静态成员函数,是所有同一类对象共有,不受限于某个对象,不能作为虚函数。
  4. 内联函数每个对象一个拷贝,无映射关系,不能作为虚函数。
  5. 构造函数和拷贝构造函数不能作为虚函数。构造函数和拷贝构造函数是设置虚表指针。
  6. 析构函数可以定义为虚函数,构造函数不能定义为虚函数,因为在调用函数构造时对象还没有完成实例化(虚表指针没有设置)。在基类中及其派生类中都有动态分配的内存空间时,必须把析构函数定义为虚函数,实现撤销对象时的多态性。
  7. 实现动态多态性时,必须使用基类类型的指针变量或引用,使该指针向基类的不同派生类的对象,并通过该指针指向虚函数,才能实现动态的多态性。
  8. 函数执行速度要稍微慢一些,为了实现多态性,每一个派生类中均要保存相应虚函数的入口地址表,函数的调用机制也是间接实现。所以多态性总要是付出一定代价,但通用性是一个更高的目标。
  9. 如果定义放在类外,virtual只能加在函数声明前面,不能加在函数定义前面,正确的定义必须不包括virtual。

注意:

要养成好习惯:

  • 一个类如果定义了虚函数,则应该将析构函数也定义成虚函数;

  • 或者,一个类打算作为基类使用,也应该将析构函数定义成虚函数。

  • 注意:构造函数不能定义成虚构造函数。

前提知识:函数指针的判别

void ( *signal( int, void (*func) ( int, int )) ) ( int );
//signal是个函数名,往左看,是个*,说明返回值是个指针,向右看,右边是个(int),说明这个指针指向一个函数,再往左看,左边是函数的返回值类型
//函数指针函数

void fn()
{
	cout << "fn" << endl;
}
void main()
{
	void (*p)(); //函数指针---右左法则
	void (*q[3])();//找到q,向右看是个数组,所以q是个数组名,往左看,左边是个*(说明是指针),也就是说数组q中有3个指针,再继续向右看,右边是个(),说明是函数
	//函数指针数组 q[0],q[1],q[2]
	fn(); //地址
}
int max(int a, int b)
{
	return a > b ? a : b;
}
int min(int a, int b)
{
	return a < b ? a : b;
}
int Add(int a, int b)
{
	return a + b;
}
void main()
{
	int (*pf[3])(int, int) = { max,min,Add };
	//pf[0]指向的是max,pf[1]指向的是min,pf[2]指向的是Add
	for (int i = 0; i < 3; i++)
		cout << pf[i](2, 4) << endl;;
}

重载、覆盖(重写)、隐藏(重定义)的对比 

 虚函数表

当类中存在虚函数时,就会产生虚函数指针vfptr和虚函数表vbtable

有以下概念:

  1. 一个类一张虚表,所有对象共享虚函数表vftable。
  2. 为了实现共享 :每一个对象中有一个指针,称为虚函数指针vfptr,指向虚函数表,这样就可以实现虚函数表的共享。系统会在每一个类中提供虚函数指针vfptr,我们看不见,所以一旦有virtual关键字,那么类的大小就会多加4个,多出来的就是虚函数指针的大小。
  3. 类中的布局:虚函数指针在前,其他成员变量在后,因为虚函数指针的布局优先级最高(在没有虚继承时)
  • 虚函数表结构,三部分:
  • RTTI:run-time type infornation运行时类型信息,在运行阶段提取出来的类型,可以使用typeid()函数获取。
  • 偏移:虚函数指针相对于整体作用域的偏移,用整体作用域-vfptr的位置得到。
  • 虚函数入口地址

单一继承,无虚函数覆盖: 

示例1:

/*只要有虚函数,不管有多少个虚函数,都只多了4个字节,多了一个Vfptr
vfptr虚指针指向了一个vftable,vftable中存储了当前类的虚函数的入口地址
*/
#if 0
class A
{
public:
	virtual void fa() { cout << "A::fa" << endl; }
	virtual void fb() { cout << "A::fb" << endl; }
	virtual void fc() { cout << "A::fc" << endl; }
	void fd() { cout << "A::fd" << endl; }
private:
	int m_i;
};
void main()
{
		 cout<<sizeof(A)<<endl;
		 A a;
		 a.fa();
		 a.fb();
	//A a;
	//typedef void (*Fun)();
	//Fun pf = NULL; //函数指针pf可以指向类A中的fa,fb,fc
	//pf = (Fun) * ((int*)(*(int*)(&a)));
	//pf(); //A::fa

	//pf = (Fun) * ((int*)*(int*)(&a) + 1);
	//pf(); //A::fb
	//pf = (Fun) * ((int*)*(int*)(&a) + 2);
	//pf(); //A::fc
}

示例2:

class A
{
public:
	virtual void fa() { cout << "A::fa" << endl; }
	virtual void ga() { cout << "A::ga" << endl; }
	virtual void ha() { cout << "A::ha" << endl; }
};
class B :public A
{
public:
	virtual void fb() { cout << "B::fb" << endl; }
	virtual void gb() { cout << "B::gb" << endl; }
};
void main()
{
	cout << sizeof(B) << endl;
	B b;
	typedef void (*Fun)();
	Fun pf = NULL; //函数指针pf可以指向类B中的fa,fb,fc,fb,gb
	pf = (Fun) * ((int*)(*(int*)(&b)));
	pf(); //A::fa
	pf = (Fun) * ((int*)*(int*)(&b) + 1);
	pf(); //A::ga
	pf = (Fun) * ((int*)*(int*)(&b) + 2);
	pf(); //A::ha
	pf = (Fun) * ((int*)*(int*)(&b) + 3);
	pf(); //B::fb
	pf = (Fun) * ((int*)*(int*)(&b) + 4);
	pf(); //B::gb
}

根据看监视:
每个类里面只有一个虚指针,指向一个虚标
发现B中的虚表中有5个虚函数,前三个是从A继承过来的虚函数的入口地址,后面两个是B类自己的虚函数 

内存分布如下

单一继承,有覆盖:

class A
{
public:
    virtual void fa() { cout << "A::fa" << endl; }
    virtual void fb() { cout << "A::fb" << endl; }
    virtual void fc() { cout << "A::fc" << endl; }
};
class B :public A
{
public:
    /*
    覆盖 子类重写了基类的同名同参的virtual函数,在B类的对象模型中,在继承下来虚表的同一处地址修改成B的重写的函数
    */
    virtual void fa() { cout << "B::fa" << endl; }
    virtual void gb() { cout << "B::gb" << endl; }
private:
    int m_i;
    int m_j;
};
void main()
{
    B b; 
}

多继承下,无覆盖:

对于多继承,每个父类都有自己的虚表
将最终子类的虚函数放在第一个父类的虚表中
这样做解决了不同的父类类型的指针指向比较清晰

class A
{
public:
	virtual void fa() { cout << "A::fa" << endl; }
	virtual void ga() { cout << "A::ga" << endl; }
};
class B
{
public:
	virtual void fb() { cout << "B::fb" << endl; }
	virtual void gb() { cout << "B::gb" << endl; }
};
class C
{
public:
	virtual void fc() { cout << "C::fc" << endl; }
	virtual void gc() { cout << "C::gc" << endl; }
};
class D :public A, public B, public C
{
public:
	virtual void fd() { cout << "D::fd" << endl; }
	virtual void gd() { cout << "D::gd" << endl; }
};

void main()
{
	D d;
	cout << sizeof(D) << endl;
}

监视其内存:

多继承下,有覆盖:

不相同的子类虚函数会贴在第一个父类虚表后面的位置。

相同的全部覆盖。

class A
{
public:
	virtual void f() { cout << "A::f" << endl; }
	virtual void ga() { cout << "A::ga" << endl; }
};
class B
{
public:
	virtual void f() { cout << "B::f" << endl; }
	virtual void gb() { cout << "B::gb" << endl; }
};
class C
{
public:
	virtual void f() { cout << "C::f" << endl; }
	virtual void gc() { cout << "C::gc" << endl; }
};
class D :public A, public B, public C
{
public:
	virtual void f() { cout << "D::f" << endl; }
	virtual void gd() { cout << "D::gd" << endl; }
};

void main()
{
	D d;
	cout << sizeof(D) << endl;
	//函数指针

	typedef void(*Fun)();
	//第一个虚表
	Fun pf = (Fun) * (((int*)*(int*)(&d)));
	pf();//D:f
	pf = (Fun) * (((int*)*(int*)(&d)) + 1);
	pf();//A:ga
	pf = (Fun) * (((int*)*(int*)(&d)) + 2);
	pf();//D:gd
	//第二个虚表
	pf = (Fun) * ((int*)*(int*)((int*)(&d) + 1));
	pf();//D:f
	pf = (Fun) * ((int*)*(int*)((int*)(&d) + 1) + 1);
	pf();//B:gb
	//第三个虚表
	pf = (Fun) * ((int*)*(int*)((int*)(&d) + 2));
	pf();//D:f
	pf = (Fun) * ((int*)*(int*)((int*)(&d) + 2) + 1);
	pf();//c:gc

}

 内存分布:

日常巩固复习

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

BearPot

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值