C++之虚函数详解

对于学习过C++的人来说,virtual这个关键字并不会陌生,因为它是是程序中的一把利器,可以帮我们实现动态联编。下面我们就详细地了解一下这个关键字。
首先,我们要问C++为什么会引入virtual这个关键字?
原因: C++不允许将一种类型的地址赋给另一种类型的指针,也不允许一种类型的引用指向另一种类型。但是指向基类的引用或指针可以引用派生类对象,而不必进行显式类型转换。将派生类引用或指针转换为基类引用或指针被称为向上强制转换,这使公有继承不需要进行显式类型转换。隐式向上强制转换使基类指针或引用可以指向基类对象或派生类对象,因此需要动态联编。C++使用虚函数(即使用virtual修饰的函数)来满足这种需求。
接下来,我们再了解一下动态联编:
将源代码中的函数调用解释为执行特定的函数代码块被称为函数名联编。在编译过程中进行的联编称为静态联编,又称为早期联编。在运行时进行的联编称为动态联编。为什么需要在运行时进行联编呢?原因是这样的:由于虚函数使得对象的类型在编译器无法确定,所以只能在运行时刻动态地进行选择。从而实现了动态联编!

C++中的函数默认不使用动态绑定。要触发动态绑定,必须满足两个条件:

第一,只有指定为虚函数的成员函数才能进行动态绑定,成员函数默认为非虚函数,非虚函数不进行动态绑定;

第二,必须通过基类类型的引用或指针进行函数调用。

虚函数的工作原理:

编译器给每个对象添加一个隐藏成员。隐藏成员中保存了一个指向函数地址数组的指针。这种数组被称为虚函数表。虚函数表中存储了为类对象进行声明的虚函数的地址。程序中通过基类指针或引用对虚函数的调用语句都会被编译改写成下面这种形式;

(*(p -> _vptr[index]))(p, arg-list);

注:p是基类类型的指针,_vptr是p指向的对戏那个的隐含指针,而index是调用的虚函数在vtable中的编号,这个数组元素的索引号在编译时就确定了下来,
并且不会随着派生层次的增加而改变。

虚函数带来的优势:
如果方法是通过引用或对象调用的,它将确定使用哪一种方法。如果没有使用virtual关键字修饰,程序将根据引用类型和指针类型选择方法;如果使用了virtual,程序将根据引用或指针指向的对象的类型来选择方法

一个常用的实例:通常我们提倡将类的析构函数写成虚函数。这样做的目的:
如果基类的析构函数不是虚函数,那么意味着只调用对应于指针类型的析构函数。即便该指针指向派生类的对象。但是如果基类的析构函数时虚拟的,将调用相应类型的析构函数。也就是说,使用虚拟构造函数可以确保正确的析构函数序列被调用。
但是,并不是说,所以情况下都应该将类的析构函数写成虚函数(当然,这样实现一般不会错!),因为虚函数也有一些缺点:
使用虚函数时,在内存和执行速度方面有一定的成本,包括:
1. 每个对象都将增大,增大量为存储地址的控件;
2. 对每个类,编译器都创建一个虚函数地址表;
3. 每个函数调用都需要执行一步额外的操作,即到表中查找地址。
所以,对于以下情况,我们并不提倡将析构函数定义为虚函数:
当我们的类不是作为基类时,一般不提倡将类的析构函数定义为虚函数,原因:这样会带来额外的内存开销,使对象的体积变大。

下面我们从一个例子来说明这个情况:

class Person
	{

	};
	std::cout << sizeof(Person) << std::endl;	

我们会发现输出的结构是1。是不是觉得很惊讶:明明是一个空类,为什么其大小是1B呢?

原因是这样的:在类的实例化的过程中,编译器需要对其分配空间,即便是空类也会分配空间,但至于分配多少字节,因编译器而异,对于空类
一般都是一个字节吧!
class Person
	{		
		virtual ~Person();
	};
	std::cout << sizeof(Person) << std::endl;
这次我们发现输出的结果竟然是4,是不是又很迷茫?下面我们就一起来揭开这其中神秘的面纱。。。。。。。。
在此之前,我们需要知道:成员函数并不占内存空间。不信你可以试试。那为什么这个类的对象的大小会是4B呢。
由于这个类的析构函数是虚函数,而对于虚函数。对象必须携带某些信息,主要用来在运行期决定 哪一个virtual函数该被调用,这份信息通常是由一个vptr(virtual table pointer)指针指出。vptr指向一个由函数指针构成的数组,称为vtbl(virtual table);每一个带有virtual的函数的class都有一个相应的vbtl。当对象呗调用某一virtual函数,实际被调用的函数取决于该对象的vptr所指的那个vbtl——编译器在其中寻找适合的函数指针。

通过上面的分析,我们会发现,调用一个virtual函数通常由3个内存引用取出:一个从对象取出描述对象类型的表的地址值,一个取出虚函数的地址,还有一个就是在可能的较大范围对象中,取出本对象的偏移量。在这样的实现中,把一个函数变成虚函数需要3倍的执行时间。
所以,在使用虚函数时,我们应该恰当地选择,而不要盲目的将其应用于类的所有成员函数。

在使用虚函数的时候也应该注意一下几点:
1. 派生类重定义虚函数时,可以使用关键字virtual,也可不使用。

2. 友元不能是虚函数,因为友元不是类的成员,而只有类的成员函数才可能是虚函数。

下面我们就通过一个简单的实例来了解虚函数:

virtual.h 文件

class Person
{
public:
	void ShowInfo(void);
	virtual void PrintInfo(void);
};
virtual .cpp文件
#include "virtual.h"
#include <iostream>

void Person::PrintInfo(void)
{
	std::cout << "这是基类的PrintInfo!" << std::endl;
}

void Person::ShowInfo(void)
{
	std::cout << "这是基类的ShowInfo!" << std::endl;
}
derived.h文件

#include "virtual.h"

class PersonChild:public Person
{
	void PrintInfo(void);
	void ShowInfo(void);
};
dervied.cpp文件

#include "derived_class.h"
#include <iostream>

void PersonChild::PrintInfo(void)
{
	std::cout << "这是派生类的PrintInfo!" << std::endl;
}

void PersonChild::ShowInfo(void)
{
	std::cout << "这是派生类的ShowInfo!" << std::endl;
}
main.cpp文件

#include "derived_class.h"

int main(void)
{
	Person *man = new Person;
	Person *woman = new PersonChild;
	man->PrintInfo();
	man->ShowInfo();
	woman->PrintInfo();
	woman->ShowInfo();

	return 0;
}
运行结构:

这是基类的PrintInfo!
这是基类的ShowInfo!
这是派生类的PrintInfo!
这是基类的ShowInfo!
Press any key to continue






  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值