IT教程 ·

C++ 基础–虚函数

ASP.NET Core MVC 中实现中英文切换

virtual 函数

示例代码以下:

#include <stdio.h>
class base {
public:
    virtual void name(){printf("basen");};
    virtual ~base(){};
};

class plus: public base {
public:
    virtual void name(){printf("plusn");};
};

void fv(base b){
    b.name();
}

void fp(base &b){
    b.name();
}

int main(){
    base b;
    plus p;
    fv(b);
    fv(p);
    fp(b);
    fp(p);
    return 0;
}

程序输出:

    base
    base
    base
    plus

这里涉及到一个c++知识点--向上强迫转换:将派生类援用或指针转换为基类援用或指针。该划定规矩使得公有继续不须要举行显现范例转化,它是is-a 划定规矩的一部份。

相反的历程被称为--向下强迫转换,向下强迫范例转换必需是显现的。因为派生类可能对基类举行拓展,新增的成员变量和函数不能应用于基类。

隐式向上强迫转换使得基类指针或援用能够指向基类对象或派生类对象,因而须要动态联编C++ 运用虚成员函数函数满足这类需求。

动态联编

编译器在编译时要将挪用的函数对应响应的可实行代码,此历程为函数联编(binding),在C++因为函数重载的缘由,须要检察挪用函数名和传入参数才确认是哪一个函数。在编译的时刻能够确认运用哪一个函数的联编被称为静态联编初期联编

同时因为virtual函数的存在,编译事情变得越发庞杂,如示例函数所示,详细运用的哪一个范例对象不能确认。为此编译器必需生成一些代码,使得在程序运转的时刻挑选准确的虚函数,这被称为动态联编,又被称为晚期联编

为了考证上面所述我们能够做一组对比,起首我们用 gnu 东西 nm 来检察 sysbols,能够发明以下的部份:

$ nm virtual.exe | grep -c -E "plus|base"
49

然后我们革新一下上面的代码:

class base {
public:
    void name(){printf("basen");}; // 修正
    virtual ~base(){};
};

class plus: public base {
public:
    void name(){printf("plusn");}; // 修正
};

编译后从新实行nm敕令:

nm virtual_.exe | grep -c -E "plus|base"
45

经由比对后我们会发明修正后缺少了以下symbols:

000000000040509c p .pdata$_ZN4plus4nameEv
0000000000402e00 t .text$_ZN4plus4nameEv
00000000004060a0 r .xdata$_ZN4plus4nameEv
0000000000402e00 T _ZN4plus4nameEv

动态联编在效力上要低于静态联编,在C++ 中默许运用静态联编。C++ 之父strousstup 以为 C++ 指点准绳之一是不要为不运用的特征付出代价(cpu、memory等)。

所以在派生类不须要去重写基类函数时,则不要将其声明为virtual函数。

virtual 函数事情道理

虚函数示意每个运用C++的开发者耳熟能详的东西,有一个道典范的试题以下:

#include <stdio.h>

class base
{
public:
    base(){};
    virtual ~base() { printf("basen"); };
};

class plus : public base
{
public:
    plus(/* args */){};
    virtual ~plus() { printf("plusn"); };
};

class plus2 : public base
{
public:
    plus2(/* args */){};
    ~plus2() { printf("plus2n"); };
};

class plus3 : public base
{
public:
    virtual void name() { printf("plus3"); };
    plus3(/* args */){};
    virtual ~plus3() { printf("plus3n"); };
};

class empty
{
private:
    /* data */
public:
    empty(/* args */){};
    ~empty() { printf("emptyn"); };
};

int main()
{
    base b;
    printf("base: %dn", sizeof(b));

    plus p;
    printf("plus: %dn", sizeof(p));

    plus2 p2;
    printf("plus2: %dn", sizeof(p2));

    plus3 p3;
    printf("plus3: %dn", sizeof(p3));

    empty e;
    printf("empty: %dn", sizeof(e));
}

其终究输出的效果以下:

base: 8
plus: 8
plus2: 8
plus3: 8
empty: 1
empty
plus3
base
plus2
base
plus
base
base

ps: 因为操作体系位数的影响效果可能有更改,在x64位体系中指针内存分派大小为 8 字节,x86 体系中指针内存分派大小为 4。

我们能够清晰的看到,只需存在虚函数不论是成员函数异或是析构函数,是在类中定义或继续都邑有包括一个虚函数表。而这里的8字节就是分派给了虚函数表的指针。

我们能够经由过程gnu tool gdb 指令举行考证,在触发断点以后经由过程info local敕令去检察:

(gdb) info locals
b = {_vptr.base = 0x555555755d20 <vtable for base+16>}
p = {<base> = {_vptr.base = 0x555555755d00 <vtable for plus+16>}, <No data fields>}
p2 = {<base> = {_vptr.base = 0x555555755ce0 <vtable for plus2+16>}, <No data fields>}
p3 = {<base> = {_vptr.base = 0x555555755cb8 <vtable for plus3+16>}, <No data fields>}
e = {<No data fields>}

我们能够看到每个对象内都有一个指针指向vtable。

当一个基类声明一个虚函数后,在建立对象的时刻会将该函数地点到场虚函数列表中,假如派生类重写了该函数,则会用新函数地点替代,假如其定义了新函数,则会将新函数的指针到场虚表中。
示例代码以下:

#include <stdio.h>

class base
{
public:
    base(){};
    virtual const char* feature(){return "test";};
    virtual void name() {printf("basen");}
    virtual ~base() { printf("~basen"); };
};

class plus : public base
{
public:
    plus(/* args */){};
    virtual void name() {printf("plusn");}
    virtual void parant() {printf("basen");}
    ~plus() { printf("plusn"); };
};

int main()
{
    base b;
    printf("base: %ldn", size_t(&b));

    plus p;
    printf("plus: %ldn", size_t(&p));
}

依然用 gdb 来考证,断点后经由过程 info vtbl 敕令检察:

(gdb) info vtbl p
vtable for 'plus' @ 0x555555755d08 (subobject @ 0x7fffffffe010):
[0]: 0x555555554b4a <base::feature()>
[1]: 0x555555554bf8 <plus::name()>
[2]: 0x555555554c30 <plus::~plus()>
[3]: 0x555555554c66 <plus::~plus()>
[4]: 0x555555554c14 <plus::parant()>
(gdb) info vtbl b
vtable for 'base' @ 0x555555755d40 (subobject @ 0x7fffffffe008):
[0]: 0x555555554b4a <base::feature()>
[1]: 0x555555554b5c <base::name()>

当挪用虚函数的时刻,会在虚函数表中寻觅对应的函数地点,因而它每一次挪用动会多做一步婚配,比拟静态联编的非虚函数要越发耗时。

须要注重的是组织函数不能声明为虚函数,而假如一个类作为除非不作为基类,不然发起声明一个虚析构函数。

【WPF学习】第四十七章 WriteableBitmap类

参与评论