前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >深入了解C++虚函数

深入了解C++虚函数

作者头像
Sky_Mao
发布2020-07-24 10:10:27
5400
发布2020-07-24 10:10:27
举报
一、认识虚函数

虚函数(Virtual Function):在基类中声明为 virtual 并在一个或多个派生类中被重新定义的成员函数。 作用: C++ “虚函数”的存在是为了实现面向对象中的“多态”,即父类类别的指针(或者引用)指向其子类的实例,然后通过父类的指针(或者引用)调用实际子类的成员函数。通过动态赋值,实现调用不同的子类的成员函数(动态绑定)。正是因为这种机制,把析构函数声明为“虚函数”可以防止内存泄露。

简单的示例:

代码语言:javascript
复制
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被编译器加入对象的内某个唯有编译器才知道的位置,网上搜的资料说这个数据成员会被放在对象内存布局的第一个位置。具体的可以试验一下! 先假设放在第一个位置。 例如这样一个类:

代码语言:javascript
复制
class c1
{
public:
    c1() {};
    virtual void f1() {};
    virtual void f2() {};
    virtual void f3() {};
};

c1的vtbl看起来应该是这样的:

&c1

下面试验一下,vptr是否在对象内存布局的第一个位置。 在f1函数打印一条信息:

代码语言:javascript
复制
virtual void f1() { std::cout << "test func pos"; };

在main函数内添加这些代码:

代码语言:javascript
复制
int main()
{
    c1 * pc = new c1();
    typedef void (*Func)(void);
    Func  pFun = (Func)*((int*)*(int*)(pc) + 0);
    pFun();
    return 0;
}
代码语言:javascript
复制
 (Func)*((int*)*(int*)(pc) + 0); 

这行代码可能理解起来比较吃力,我有的时候看起来也比较费劲,这一堆都是啥 玩意儿啊? 我们可以拆开来理解 首先把c1指针类对象强转为int*

代码语言:javascript
复制
int * nPc = (int*)(pc);

然后把类对象的首地址的所指物给强转成int*,首地址存放的应该是虚表指针 vtbls

代码语言:javascript
复制
int * vtabls = (int*)*(nPc);

最后把虚表指针第1个虚函数(从0开始的),转成Func

代码语言:javascript
复制
Func pFun = (Func)*(vtabls + 0);

这样的话是不是好理解多了。 我使用的环境是VS2017

输出结果:

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

下边看一下发生继承关系以后,虚函数表的状态 假如有一个类(单继承无覆盖的情况):

代码语言:javascript
复制
class c2 : public c1
{
public:
    c2() {};
    virtual void f4() { std::cout << "c2::f4()" << std::endl; };
    virtual void f5() {};
};

那么c2的虚函数表看起来应该是这样的:

&c2

在写一段代码来测试一下:

代码语言:javascript
复制
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的虚函数,这是很容易做到的。 比如:

代码语言:javascript
复制
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)

第一次面对虚构造函数的时候,似乎不觉得有什么道理可言,并且还有些荒谬,但它们很有用。 比如我有一个函数需要根据获得的输入,来构造不同类型的对象的时候。 假设有一个链表,存储图形或者文字信息:

代码语言:javascript
复制
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 比如:

代码语言:javascript
复制
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化。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、认识虚函数
  • 二、虚函数表
  • &c1
  • &c2
  • &c2
  • &c4
    • 三、虚函数的成本
      • 四、安全性
        • 五、将构造函数与非成员函数虚化(来自more effective C++ 条款25)
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档