(一)C++中虚函数的作用和虚函数的工作原理

1 C++中虚函数的作用和多态

虚函数: 实现类的多态性

关键字:虚函数、虚函数的作用;多态性;多态公有继承;动态联编

C++中的虚函数的作用主要是实现了多态的机制。基类定义虚函数,子类可以重写该函数;在派生类中对基类定义的虚函数进行重写时,需要在派生类中声明该方法为虚方法。

当子类重新定义了父类的虚函数后,当父类的指针指向子类对象的地址时,[即B b; A a = &b;] 父类指针根据赋给它的不同子类指针,动态的调用子类的该函数,而不是父类的函数(如果不使用virtual方法,请看后面),且这样的函数调用发生在运行阶段,而不是发生在编译阶段,称为动态联编。而函数的重载可以认为是多态,只不过是静态的。注意,非虚函数静态联编,效率要比虚函数高,但是不具备动态联编能力。

★如果使用了virtual关键字,程序将根据引用或指针指向的 对 象 类 型 来选择方法,否则使用引用类型或指针类型来选择方法。

下面的例子解释动态联编性:

class A1 {
private:
	int i;
public:
	A1() {};
	A1(int num) :i(num) {};
	virtual void fun1() { cout << "A1::fun1()" << endl; };
	virtual void fun2() { cout << "A1::fun2()" << endl; };

};

class B1 : public A1 {
private:
	int j;
public:
	B1(int num) :j(num) {};
	virtual void fun2() { cout << "B1::fun2()" << endl; };// 重写了基类的方法
};

void test1()
{
	// 为方便解释思想,省略很多代码
	A1 a(1);
	B1 b(2);
	A1 *a1_ptr = &a;
	A1 *a2_ptr = &b;

	// 当派生类“重写”了基类的虚方法,调用该方法时
	// 程序根据 指针或引用 指向的  “对象的类型”来选择使用哪个方法
	a1_ptr->fun2();// call A::fun2();
	a2_ptr->fun2();// call B::fun2();
				   // 否则
				   // 程序根据“指针或引用的类型”来选择使用哪个方法
	a1_ptr->fun1();// call A::fun1();
	a2_ptr->fun1();// call A::fun1();
}

2. 虚函数的底层实现机制

实现原理:虚函数表+虚表指针

关键字:虚函数底层实现机制;虚函数表+虚表指针

编译器处理虚函数的方法是:为每个类对象添加一个隐藏成员,隐藏成员中保存了一个指向函数地址数组的指针。隐藏成员指针称为虚表指针(vptr),虚函数地址数组称为虚函数表(virtual function table, vtbl),即:每个类使用一个虚函数表,每个类对象用一个虚表指针。

举个例子:基类对象包含一个虚表指针,指向基类中所有虚函数的地址表。派生类对象也将包含一个虚表指针,指向派生类虚函数表。看下面两种情况:

如果派生类重写了基类的虚方法,该派生类虚函数表将保存重写的虚函数的地址,而不是基类的虚函数地址。

如果基类中的虚方法没有在派生类中重写,那么派生类将继承基类中的虚方法,而且派生类中虚函数表将保存基类中未被重写的虚函数的地址。注意,如果派生类中定义了新的虚方法,则该虚函数的地址也将被添加到派生类虚函数表中。

下面的图片体现了上述的底层实现机制:

 

编译器处理虚函数的方法是:
给每个对象添加一个指针,存放了指向虚函数表的地址,虚函数表存储了为类对象进行声明的虚函数地址。比如基类对象包含一个指针,该指针指向基类所有虚函数的地址表,派生类对象将包含一个指向独立地址表的指针,如果派生类提供了虚函数的新定义,该虚函数表将保存新函数的地址,如果派生类没有重新定义虚函数,该虚函数表将保存函数原始版本的地址。如果派生类定义了新的虚函数,则该函数的地址将被添加到虚函数表中,注意虚函数无论多少个都只需要在对象中添加一个虚函数表的地址。

使用虚函数后的变化:
(1) 对象将增加一个存储地址的空间(32位系统为4字节,64位为8字节)。
(2) 每个类编译器都创建一个虚函数地址表
(3) 对每个函数调用都需要增加在表中查找地址的操作。

虚函数的注意事项

总结前面的内容
(1) 基类方法中声明了方法为虚后,该方法在基类派生类中是虚的。
(2) 若使用指向对象的引用或指针调用虚方法,程序将根据对象类型来调用方法,而不是指针的类型。
(3)如果定义的类被用作基类,则应将那些要在派生类中重新定义的类方法声明为虚。
构造函数不能为虚函数。
基类的析构函数应该为虚函数。
友元函数不能为虚,因为友元函数不是类成员,只有类成员才能是虚函数。
如果派生类没有重定义函数,则会使用基类版本。
重新定义继承的方法若和基类的方法不同(协变除外),会将基类方法隐藏;如果基类声明方法被重载,则派生类也需要对重载的方法重新定义,否则调用的还是基类的方法。

3、为什么基类的析构函数应该为虚函数?

如果不使用父类指针指向子类对象时,不需要声明为虚函数,避免不需要的内存开销(对象虚函数指针,及虚函数表的内存开销),如果使用父类指针,指向之类对象就需要将析构函数声明为虚函数。

基类的析构函数要生命为虚函数,不然用基类指针删除派生类对象时只会调用基类的析构函数,而不会调用派生类的析构函数,从而发生内存泄露。

1. 析构函数跟普通成员没有什么不同,只是编译器在会在特定的时候自动调用析构函数(离开作用域或者执行delete操作);

2. 对于一个成员函数调用(不论是通过对象obj.func还是通过对象指针obj->func),到底是直接调用还是通过虚函数表调用,在编译的时候是确定的,取决于这个函数是不是虚函数;

3. 综上,如果析构不声明为虚函数,那么delete pBase,就被编译器解释为 Base::~Base,否则被编译器解释为 this->virtual_function_table[#析构在虚函数表的偏移]

参考:

https://blog.csdn.net/iFuMI/article/details/51088091

https://blog.csdn.net/HuYingJie_1995/article/details/88085213

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值