了解虚函数

来源:互联网 发布:金十数据沥青直播室 编辑:程序博客网 时间:2024/05/17 09:44
一、认识虚函数
虚函数(Virtual Function):在基类中声明为 virtual 并在一个或多个派生类中被重新定义的成员函数。
作用:
C++  “虚函数”的存在是为了实现面向对象中的“多态”,即父类类别的指针(或者引用)指向其子类的实例,然后通过父类的指针(或者引用)调用实际子类的成员函数。通过动态赋值,实现调用不同的子类的成员函数(动态绑定)。正是因为这种机制,把析构函数声明为“虚函数”可以防止在内存泄露。

简单的示例:
class bass
{
public:
bass() {};
virtual void Func() { std::cout << "bass func" << std::endl; };
};

class derived : public bass
{
public:
derived() {};
virtual void Func() { std::cout << "derived func" << std::endl; };
};

int main()
{
bass * pB = new derived();
pB->Func();
return 0;
}
输出结果:
二、虚函数表
虚函数的调用是通过虚函数表(vitrual tables)和指向这张虚函数表的指针(virtual table pointers)来确定调用的是哪一个对象的函数,此二者通常被简写为vtbls和vptrs。
程序中每一个class凡声明(或继承)虚函数者,都有自己的一个vtbls,而其中的条目就是该class的各个虚函数实现体的指针。
凡是声明有虚函数的class,其对象都含有一个隐藏的数据成员,用来指向该class的vtbl。这个隐藏的数据成员就是vptr,effective C++中的描述是:这个vptr被编译器加入对象的内某个唯有编译器才知道的位置,网上搜的资料说这个数据成员会被放在对象内存布局的第一个位置。具体的可以试验一下!
先假设放在第一个位置。
例如这样一个类:
class c1
{
public:
c1() {};
virtual void f1() {};
virtual void f2() {};
virtual void f3() {};
};
c1的vtbl看起来应该是这样的:
&c1
下面试验一下,vptr是否在对象内存布局的第一个位置。
在f1函数打印一条信息:
virtual void f1() { std::cout << "test func pos"; };
在main函数内添加这些代码
int main()
{
c1 * pc = new c1();
typedef void (*Func)(void);
Func pFun = (Func)*((int*)*(int*)(pc) + 0);
pFun();
return 0;
}

环境是vs2017
输出结果
从输出结果上来看,在vs里指向虚函数表的指针,是存放在对象内存布局的第一个位置,其他编译器由于没有测试不确定是否存放在第一个位置

下边看一下发生继承关系以后,虚函数表的状态
假如有一个类(单继承无覆盖的情况):
class c2 : public c1
{
public:
c2() {};
virtual void f4() { std::cout << "c2::f4()" << std::endl; };
virtual void f5() {};
};
那么c2的虚函数表看起来应该是这样的:
&c2
在写一段代码来测试一下
c1 * pc = new c2();
typedef void (*Func)(void);
Func pFun = (Func)*((int*)*(int*)(pc) + 0);
Func pFun2 = (Func)*((int*)*(int*)(pc)+3);
pFun();
pFun2();

输出结果:
从输出结果上可以看出:
(1)虚函数按照其声明顺序放于表中。
(2)父类的虚函数在子类的虚函数前面。
如果c2继承c1后重写基类的方法c1:f1(),那么根据之前的测试,他的虚函数表应该是这样的:
&c2

多重继承情况下,子类的虚函数表:
继承关系如下:

虚函数表应该是这样的:
&c4

三、虚函数的成本
从上面的分析可以看出:
1、你必须为每一个拥有虚函数的class耗费一个vtbl空间,其大小视虚函数的个数(包括继承而来的)而定。
2、你必须在每一个拥有虚函数的对象内付出“一个额外指针”的代价,包括继承而来的。
3、虚函数不应该inlined,因为inline意味“在编译期,将调用端的调用动作被调用函数的函数本体取代”,而virtual则意味着“等待,直到运行时期才知道哪个函数被调用”,当编译器对某个调用动作,却无法知道哪个函数该被调用时,你就可以了解它们没有能力将该函数调用加以“inlining”了,事实上等于放弃了inlining。(如果虚函数通过对象调用,倒是可以inlined,但是大部分虚函数调用动作是通过对象的指针或references完成的,此类行为无法被inlined。由于此等调用行为是常态,所以虚函数事实上等于无法被inlined)-----摘自more effective C++。

四、安全性
如果父类的虚函数是private或是protected的,但这些非public的虚函数同样会存在于虚函数表中,所以,我们同样可以使用访问虚函数表的方式来访问这些non-public的虚函数,这是很容易做到的。
比如:
class bass
{
public:
bass() {};
private:
virtual void fc() { std::cout << "bass::fc()" << std::endl; };
};
int main()
{
typedef void(*Func)(void);
bass *pBass = new bass();
Func pF = (Func)*((int*)*(int*)(pBass));
pF();
return 0;
}
输出结果:

五、将构造函数与非成员函数虚化(来自more effective C++ 条款25)
第一次面对虚构造函数的时候,似乎不觉得有什么道理可言,并且还有些荒谬,但它们很有用。
比如我有一个函数需要根据获得的输入,来构造不同类型的对象的时候。
假设有一个链表,存储图形或者文字信息:
class common
{
public:
};
class text : public common
{
public:
};
class graphic : public common
{
public:
};
std::list<common*> oCommonInfo;
template<class T>
common* readCommonInfo(T inPut)
{
//根据输入的信息来构造text还是graphic
}

oCommonInfo.push_back(readCommonInfo(inPut));
思考一下,readCommonInfo做了一些什么事,它产生一个新对象,或许是text,也或许是graphic,
视输入的数据而定,由于它产生了新对象,所以行为仿若构造函数,但它能够产生不同类型的对象,
所以我们称它为一个virtual construction。所谓的virtual construction是某种函数,视其获得的输入,可产生不同类型的对象。

还有一种比较特殊的virtual construction,比如virtual copy construction,常见的是类的clone
比如:
class a
{
public:
a() {};
virtual a* clone() const = 0;
};
class b : public a
{
public:
b() {};
virtual b* clone() const {};
};
class c : public a
{
public:
c() {};
virtual c* clone() const {};
};
虚函数在重写的时候,返回类型、函数名称、参数个数、参数类型必须相同,但是当基类虚函数返回基类指针,派生类虚函数返回派生类指针,是允许的。
a *pa = new b或者c;
list.push_back(a.clone());

就像construction无法被真正虚化一样,非成员函数也是一样。
不过可以将非成员函数的行为虚化,
可以写一个虚函数做实际工作,在写一个什么也不做的非虚函数,只负责调用虚函数。
当然为了避免此巧妙安排蒙受函数调用带来的成本,可以将非虚函数inline化。























原创粉丝点击