虚函数详解

虚函数表相关知识点:
1.虚函数表存放的内容:类的虚函数的地址
2.虚函数表建立的时间:编译阶段,即程序的编译过程中会将虚函数的地址放在虚函数表中
3.虚表指针保存的位置:虚表指针存放在对象的内存空间中最前面的位置,这是为了保证正确取到虚函数的偏移量

注:虚函数表和类绑定,虚表指针和对象绑定,即类的不同的对象的虚函数表是一样的,但是每个对象都有自己的虚表指针。在编译时,一个类的虚函数表就确定了,这也是为什么它放在了只读数据段中

1.编译器将虚函数表的指针放在类的实例对象的内存空间中,该对象调用类的虚函数时,通过指针找到虚函数表,根据虚函数表中存放的虚函数的地址找到对应的虚函数

2.如果派生类没有重写基类的虚函数A,则派生类的虚函数表中保存的是基类虚函数A的地址,也就是说基类和派生类的虚函数A的地址是一样的

3.如果派生类重写了某个的虚函数B,则派生类的虚函数表中保存的是重写后的虚函数B的地址,也就是说虚函数B有两个版本,分别存放在基类和派生类的虚函数中

4.如果派生类重新定义了新的虚函数C,派生类的虚函数表保存新的虚函数C的地址

多态起手式以及内存分布
假设有一个基类Class A,一个继承了该基类的派生类Class B,并且基类中有虚函数,派生类实现了基类的虚函数。我们在代码中运用多态这个特性时,通常以两种方式起手:1)Class A *a = new Class B(); 2) Class B b;Class A * a = &b,以上两种方式都是用基类指针去指向一个派生类的实例,区别在于第一个用了new关键字而分配在堆上,第二个分配在栈上
在这里插入图片描述
以上两种不同的起手方式仅仅影响了派生类对象实例存在的位置,以左图为例,Class A *a是一个栈上的指针,该指针指向一个在堆上实例化的子类对象,基类如果存在虚函数,那么在子类对象中,除了成员函数与成员变量外,编译器会自动生成一个指向该类的虚函数表(这里是类Class B)的指针,叫做虚函数表指针。通过虚函数表指针,父类指针即可调用该虚函数表中所有的虚函数

没继承的情况
在这里插入图片描述

多重继承情况

class A {
public:
    A() { cout << "A()" << endl; }
    virtual ~A(){ cout << "~A()" << endl; }

    void func1(){ cout << "A::func1()" << endl; }
    void func2(){ cout << "A::func2()" << endl; }

    virtual void vfunc1(){ cout << "A::vfunc1()" << endl; }
    virtual void vfunc2(){ cout << "A::vfunc2()" << endl; }
private:
    int aData;
};

class B :public A {
public:
    B(){ cout << "B()" << endl; }
    virtual ~B(){ cout << "~B()" << endl; }

    void func1(){ cout << "B::func1()" << endl; }
    virtual void vfunc1(){ cout << "B::vfunc1()" << endl; }
private:
    int bData;
};

class C : public B {
public:
    C(){ cout << "C()" << endl; }
    virtual ~C() { cout << "~C()" << endl; }

    void func2(){ cout << "C::func2()" << endl; }
    virtual void vfunc2(){ cout << "C::vfunc2()" << endl; }
private:
    int cData;
};

在继承的情况下,只要基类有虚函数,派生类不管是否实现,都要虚函数表,基类的虚函数表和派生类的虚函数表不是同一张表注意虚函数表是在编译时确定的,属于类而不属于某个具体的实例,虚函数在代码段,仅有一份

在这里插入图片描述
B继承于A,其虚函数表是在A虚函数表基础上有所改动的,变化的仅仅是在子类中重写的虚函数。如果子类没有重写任何父类的虚函数,那么子类的虚函数表和父类的虚函数表在内容上是一致的

A* a = new B();
    a->func1;                      //A::func1() 隐藏了B的func1()
    a->func2();                    //A::func2()
    a->vfunc1();                   //B::vfunc1()重写了A的vfunc1()
    a->vfunc2();                   //A::vfunc2()

这个结果不难想象,看上图,A类型的指针a能操作的范围只能是黑框中的范围,之所以实现了多态是因为子类的虚函数表指针和虚函数表的内容和基类不同,这个结果已经说明了C++的隐藏,重写特性
同理,也就不难推导出C的逻辑结构图了,类的继承情况是C继承B,B继承A,这里是一个多次单继承的情况(多重继承)

在这里插入图片描述

 A* a = new C;
    a->func1();          // A::func1()   隐藏B::func1()               
    a->func2();          // A::func2()	  隐藏C::func2()
    a->vfunc1();	     // B::vfunc1()  B把A::vfunc1()覆盖了
    a->vfunc2();	     // C::vfunc2()  C把A::vfunc2()覆盖了

    B* b = new C;
    b->func1();				// B::func1()	有权限操作时,子类优先
    // 可以通过 b->A::func1() ,这算是派生类隐藏了基类
    b->func2();				// A::func2()	隐藏C::func2()
    b->vfunc1();			// B::vfunc1()	B把A::vfunc1()覆盖了
    b->vfunc2();			// C::vfunc2()	C把A::vfunc2()覆盖了

多继承下的虚函数表(同时继承多个基类)
多继承是指一个类同时继承了多个基类,假设这些基类都有虚函数,也就是说每个基类都有虚函数,那么该子类的逻辑结果和虚函数表是什么样子呢

class A1
{
public:
    A1() { cout << "A1()" << endl; }
    virtual ~A1() { cout << "~A1()" << endl; }

    void func1() { cout << "A1::func1()" << endl; }

    virtual void vfunc1() { cout << "A1::vfunc1()" << endl; }
    virtual void vfunc2() { cout << "A1::vfunc2()" << endl; }
private:
    int a1Data;
};

class A2
{
public:
    A2() { cout << "A2::A2()" << endl; }
    virtual ~A2() { cout << "A2::~A2()" << endl; }

    void func1() { cout << "A2::func1()" << endl; }

    virtual void vfunc1() { cout << "A2::vfunc1()" << endl; }
    virtual void vfunc2() { cout << "A2::vfunc2()" << endl; }
    virtual void vfunc4() { cout << "A2::vfunc4()" << endl; }
private:
    int a2Data;
};

class C : public A1, public A2
{
public:
    C() { cout << "C()" << endl; }
    virtual ~C() { cout << "~C()" << endl; }

    void func1() { cout << "C::func1()" << endl; }

    virtual void vfunc1() { cout << "C::vfunc1()" << endl; }
    virtual void vfunc2() { cout << "C::vfunc2()" << endl; }
    virtual void vfunc3() { cout << "C::vfunc3()" << endl; }
};

在这里插入图片描述
在多继承的情况下,有多少个基类就有多少个虚函数指针,前提是基类要有虚函数才算上这个基类,如图,虚函数表指针01指向的虚函数表是以A1的虚函数表为基础的,子类的C::vfunc1()和vfunc2()的函数指针覆盖了虚函数表01中的虚函数指针01的位置,02的位置。当子类中有多出来的虚函数时,添加在第一个虚函数表中。注意:1.子类虚函数会覆盖每一个父类的每一个同名虚函数。2.父类中没有的虚函数而子类有,填入第一个虚函数表中,且用父类的指针是不能调用。3.父类中有的虚函数而子类没有,则不覆盖,仅子类和父类的指针能调用

 A1* a1 = new C;
    a1->func1();               // "A1::func1()"    隐藏子类同名函数
    a1->vfunc1();              // "C::vfunc1()"    覆盖父类A1虚函数
    a1->vfunc2();              // "C::vfunc2()"    覆盖父类A1虚函数
    //没有a1->vfunc3(),父类没有这个虚函数

    A2* a2 = new C;
    a2->func1();               // "A2::func1()"    隐藏子类同名函数
    a2->vfunc1();              // "C::vfunc1()"    覆盖父类ClassA2虚函数
    a2->vfunc2();              // "C::vfunc2()"    覆盖父类ClassA2虚函数
    a2->vfunc4();              // "A2::vfunc4()"   未被子类重写的父类虚函数

    C* c = new C;
    c->func1();                // "C::func1()"
    c->vfunc1();               // "C::vfunc1()"
    c->vfunc2();               // "C::vfunc2()"
    c->vfunc3();               // "C::vfunc3()"
    c->vfunc4();               // "A2::func4()"

纯虚函数
1.纯虚函数在类中声明时,加上 = 0;
2.含有纯虚函数的类成为抽象类(只要含有纯虚函数这个类就是抽象类),类中只有接口,没有具体的实现方法
3.继承纯虚函数的派生类,如果没有完全实现积累纯虚函数,依然是抽象类,不能实例化对象
说明:
1.抽象类对象不能作为函数的参数,不能创建对象,不能作为函数返回类型
2.可以声明抽象类指针,可以声明抽象类引用
3.子类必须继承父类的纯虚函数,并全部实现后,才能创建子类的对象
4.析构函数最好定义为虚函数,特别是对于含有继承关系的类,析构函数可以定义为传虚函数,此时其所在对象的类为抽象基类,不能创建实例化对象
5.在虚函数和纯虚函数中的定义不能有static标识符,原因很简单,被static修饰的函数在编译时要求前期绑定,然后虚函数却是动态绑定,而且被两者修饰的函数声明周期不一样

抽象类,接口类,聚合类
1.抽象类:含有纯虚函数的类
2.接口类:仅含有纯虚函数的抽象类
3.聚合类:用户可以直接访问其成员,并且具有特殊的初始化语法形式。满足如下特点:
所有成员都是public
没有定义任何构造函数
没有类内初始化
没有基类,也没有virtual函数

构造函数一般不定义为虚函数:
1.从存储空间的角度考虑,构造函数是在实例化对象时候调用,如果此时将构造函数定义为虚函数,需要通过访问该对象所在的内存空间才能进行虚函数的调用(因为需要通过指向虚函数表的指针调用虚函数表,虽然虚函数表在编译时就有了,但是没有虚函数指针,虚函数的指针在创建对象时候才有)但是此时该对象还未创建,无法进行虚函数的调用,所以构造函数不能定义为虚函数

2.从使用的角度考虑:虚函数时基类的指针,指向派生类的对象时,通过指针实现对派生类的虚函数的调用,构造函数是在创建对象时自动调用的

3.从实现上考虑:虚函数的执行依赖于虚函数表,而虚函数表在构造函数中进行初始化工作,即初始化vptr,让他指向正确的虚函数表。而在构造对象期间,虚函数表还没有被初始化,将无法执行

4.从类型上考虑,构造一个对象时,必须知道对象的实际类型,而虚函数行为是在运行期间确定实际类型的,而在构造一个对象时,由于对象还未构造成功。编译器无法直到对象的实际类型是类本身,还是该类的一个派生类,或者是更深层的派生类

析构函数一般定义为虚函数
析构函数定义成虚函数是为了防止内存泄漏,因为当基类的指针或者引用指向或绑定到派生类的对象时,如果未将基类的析构函数定义成虚函数,会调用基类的析构函数,那么只能将基类的成员所占的空间释放掉,派生类中特有的就会无法释放内存空间,导致内存泄漏

为什么C++默认的析构函数不是虚函数
C++默认的析构函数不是虚函数是因为虚函数需要额外的虚函数表和虚表指针,占用额外的内存,而对于不会被继承的类来说,其析构函数如果是虚函数就会浪费内存,因此C++默认的析构函数不是虚函数,而是只有当需要当作父类时,设置为虚函数

虚函数效率分析
虚函数的动态绑定特性虽然很好,但存在内存空间和时间开销,每个支持虚函数的类(基类或派生类)都会有一个包含其所有支持的虚函数和虚函数表指针。每个类对象都会隐含一个虚函数表指针指向其所属类的虚函数表,当通过基类的指针或者引用调用某个虚函数时,系统需要首先定位指针或引用真正对应的对象所隐含的虚函数指针,然后虚函数指针根据虚函数的名称对其所指向的虚函数表进行一个偏移定位,在调用偏移定位处的函数指针对应的虚函数,即动态绑定的解析过程。C++规范只需要编译器能够保证动态绑定的语义,但大多数编译器都采用上述方法实现虚函数
1.每个支持虚函数的类都有一个虚函数表,虚函数表的大小与类拥有的虚函数的多少成正比。一个程序中,每个类的虚函数表只有一个,与类对象的数量无关。支持虚函数的类的每个类对象都有一个指向类的虚函数表的虚函数指针,因此程序运行时指针引起的内存开销和生成的类对象数量成正比。
2.支持虚函数的类生成每个对象时,在构造函数中会调用编译器在构造函数内部插入的初始化代码,来初始化其虚函数指针,使其指向正确的虚函数表。当通过指针或引用调用虚函数时,会根据虚函数指针找到相应类的虚函数表

虚函数与内联函数
内联函数通常可以提高代码运行速度,很多普通函数会根据情况进行内联化,但虚函数无法利用内联化的优势

因为内联是在编译阶段编译器将调用内联函数的位置用内联函数体替代(内联展开),但虚函数本质上时运行期行为。在编译阶段,编译器无法知道某处的虚函数调用在真正执行的时候需要调用哪个具体的实现(即编译阶段无法确定其具体绑定),因此,编译阶段编译器不会对通过指针或引用调用的与函数进行内联化。如果需要利用虚函数的动态绑定的设计优势,必须放弃内联带来的速度优势

如果不使用虚函数,可以通过在抽象基类增加一个类型标识成员用于在运行时识别具体的派生类对象,在派生类对象构造时必须指定具体的类型。继承体系的使用者调用函数时不再需要一次间接地根据虚函数表查找虚函数指针的操作,但在调用前仍然需要使用switch语句对其类型进行识别。

因此虚函数的缺点可以认为只有两条,即虚函数表的空间开销以及无法利用内联函数的速度优势。由于每个含有虚函数的类在整个程序只有一个虚函数表,因此虚函数表因此的空间开销非常小的,所以可以认为虚函数引入的性能缺陷只是无法利用内联函数, 通常,非虚函数的常规设计假如需要增加一种新的派生类型,或者删除一种不再支持的派生类型,都必须修改继承体系所有使用者的所有与类型相关的函数调用代码。对于一个复杂的程序,某个继承体系的使用者会很多,每次对继承体系的派生类的修改都会波及使用者。因此,不使用虚函数的常规设计增加了代码的耦合度,模块化不强,导致项目的可扩展性、可维护性、代码可读性都会降低。面向对象编程的一个重要目的就是增加程序的可扩展性和可维护性,即当程序的业务逻辑发生改变时,对原有程序的修改非常方便,降低因为业务逻辑改变而对代码修改时出错的概率。 因此,在性能和其它特性的选择方面,需要开发人员根据实际情况进行进行权衡和取舍,如果性能检验确认性能瓶颈不是虚函数没有利用内联的优势引起,可以不必考虑虚函数对性能的影响。

哪些函数不能定义为虚函数
经检验下面的几个函数都不能定义为虚函数:
1.友元函数,它不是类的成员函数
2.全局函数
3.静态成员函数,它没有this指针
4.构造函数,拷贝构造函数,以及赋值运算符重载(可以但是一般不建议作为虚函数)

虚函数实现机制
1.当类中存在虚函数,则编译器会在编译期自动的给该类生成一个函数表,并在所有该类的对象中放一个隐式变量vptr,该变量是一个指针变量,它的值指向那个类中的由编译器生成的虚函数表
2.每个类自己的虚函数入口都在这张表中维护,调用方法的时候会隐式的传入一个this指针,然后系统会根据this指针找到对应的vptr,进而找到对应的虚函数表,找到真正方法的地址,然后才去调用这个方法,这可以叫动态绑定
3.虚函数表存放重写的虚函数,当基类的指针指向派生类的对象时,调用虚函数时都会根据vptr来选择虚函数,而基类的虚函数在派生类里已经被改写或者说已经不存在了,所以也只能调用派生类的虚函数版本了

C++虚基类的作用,用法和意义
如果一个派生类有多个直接基类,而这些直接基类又有一个共同的基类(菱形继承问题),则在最终的派生类中会保留该简介共同基类数据成员的多份同名成员(浪费存储空间,存在二义性) 这种现象是人们不希望出现的。C++提供虚基类的方法时,使得在继承间接共同基类时只保留一份成员。虚基类并不是在声明基类时声明的,而是在生命派生类时,按照继承方式声明的因为一个基类可以再生成一个派生类时作为虚基类,而在生成另一个派生类时不作为虚基类

底层实现原理与编译器有关,一般通过虚基类表指针虚基类表 实现,每个虚继承的子类都有一个虚基类指针(占用一个存储空间)和虚基类表(不占用类对象的存储空间),当虚继承的子类被当作父类继承时,虚基类指针也会被继承

实际上vbptr指的是虚基类表指针,该指针指向了一个虚基类表,虚表中记录了虚基类和本类的偏移地址,通过偏移地址,这样就找到了虚基类成员,而虚继承也不用像普通多继承那样维持着公共基类的两份同样拷贝,节省了存储空间

虚继承,虚函数对比
相同之处:都利用了虚指针(均占用类的存储空间)和虚表(均不占用类的存储空间)
不同之处:
1.虚继承
虚基类依旧在继承类中,只占用存储空间
虚基类表存储的是虚基类相对直接继承类的偏移
2.虚函数
虚函数不占用内存空间
虚函数表存储的是虚函数地址

### DeepSeek R1 模型复现代码 GitHub 仓库实现 对于希望获取 DeepSeek R1 模型或项目复现代码的开发者而言,GitHub 是一个重要的资源平台。通常情况下,开源社区会提供官方或其他贡献者的实现版本。 许多研究团队会在论文发布的同时公开相应的源码链接,方便其他研究人员验证实验结果并进一步改进算法。如果 DeepSeek R1 已经发布了对应的预训练模型或是详细的架构描述,则很可能存在配套的开源实现[^1]。 为了找到特定于 DeepSeek R1 的 GitHub 实现库,建议采取以下方式搜索: - 使用关键词 `DeepSeek R1` 结合 `reproduction`, `implementation` 或者 `source code` 进行组合查询; - 查看是否有来自作者所在机构发布的官方存储库; - 浏览 Issues 和 Pull Requests 页面了解最新动态以及潜在问题解决方案; 值得注意的是,并不是所有的研究成果都会立即开放其完整的工程化实现细节。因此,在某些时候可能需要等待一段时间直到更多资料被公布出来,或者是通过阅读原始文献来尝试自行构建类似的系统结构[^2]。 ```python import requests from bs4 import BeautifulSoup def search_github_repos(query): url = f"https://github.com/search?q={query}&type=repositories" response = requests.get(url) soup = BeautifulSoup(response.text, &#39;html.parser&#39;) repos_info = [] items = soup.select(&#39;.repo-list-item&#39;) for item in items[:5]: # 获取前五个匹配项作为示例展示 title = item.h3.a[&#39;href&#39;].split(&#39;/&#39;)[-1] link = "https://github.com" + item.h3.a[&#39;href&#39;] description_tag = item.find(&#39;p&#39;, class_=&#39;mb-1&#39;) desc = description_tag.text.strip() if description_tag else &#39;&#39; repo_data = { "title": title, "link": link, "description": desc } repos_info.append(repo_data) return repos_info search_query = "DeepSeek R1 reproduction OR implementation" results = search_github_repos(search_query) for result in results: print(f"{result[&#39;title&#39;]}\n{result[&#39;link&#39;]}\nDescription: {result[&#39;description&#39;]}\n") ```
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值