C++ 在继承中虚函数、纯虚函数、普通函数,三者的区别

1.虚函数(impure virtual)

  C++的虚函数主要作用是“运行时多态”,父类中提供虚函数的实现,为子类提供默认的函数实现

  子类可以重写父类的虚函数实现子类的特殊化。

  如下就是一个父类中的虚函数:

复制代码
class A
{
public:
    virtual void out2(string s)
    {
        cout<<"A(out2):"<<s<<endl;
    }
};
复制代码

2.纯虚函数(pure virtual)

  C++中包含纯虚函数的类,被称为是“抽象类”。抽象类不能使用new出对象,只有实现了这个纯虚函数的子类才能new出对象。

  C++中的纯虚函数更像是“只提供申明,没有实现”,是对子类的约束,是“接口继承”。

  C++中的纯虚函数也是一种“运行时多态”。

  如下面的类包含纯虚函数,就是“抽象类”:

复制代码
class A
{
public:
    virtual void out1(string s)=0;
    virtual void out2(string s)
    {
        cout<<"A(out2):"<<s<<endl;
    }
};
复制代码

3.普通函数(no-virtual)

  普通函数是静态编译的,没有运行时多态,只会根据指针或引用的“字面值”类对象,调用自己的普通函数

  普通函数是父类为子类提供的“强制实现”。

  因此,在继承关系中,子类不应该重写父类的普通函数,因为函数的调用至于类对象的字面值有关。

4.程序综合实例

复制代码
#include <iostream>
using namespace std;

class A
{
public:
    virtual void out1()=0;  ///由子类实现
    virtual ~A(){};
    virtual void out2() ///默认实现
    {
        cout<<"A(out2)"<<endl;
    }
    void out3() ///强制实现
    {
        cout<<"A(out3)"<<endl;
    }
};

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

int main()
{
    A *ab=new B;
    ab->out1();
    ab->out2();
    ab->out3();
    cout<<"************************"<<endl;
    B *bb=new B;
    bb->out1();
    bb->out2();
    bb->out3();

    delete ab;
    delete bb;
    return 0;
}
复制代码

执行结果:


 -------------------------------------------------------------------------------------------

虚函数为了重载和多态的需要,在基类中是有定义的,即便定义是空,所以子类中可以重写也可以不写基类中的此函数!
纯虚函数在基类中是没有定义的,必须在子类中加以实现,很像java中的接口函数!

虚函数
引入原因:为了方便使用多态特性,我们常常需要在基类中定义虚函数。
class Cman
{
public:
virtual void Eat(){……};
void Move();
private:
};
class CChild : public CMan
{
public:
virtual void Eat(){……};
private:
};
CMan m_man;
CChild m_child;
CMan *p ;//这才是使用的精髓,如果不定义基类的指针去使用,没有太大的意义
p = &m_man ;
p->Eat(); //始终调用CMan的Eat成员函数,不会调用 CChild 的
p = &m_child;
p->Eat();//如果子类实现(覆盖)了该方法,则始终调用CChild的Eat函数
//不会调用CMan 的 Eat 方法;如果子类没有实现该函数,则调用CMan的Eat函数

p->Move(); //子类中没有该成员函数,所以调用的是基类中的


纯虚函数
引入原因:
1、同“虚函数”;
2、在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。
纯虚函数就是基类只定义了函数体,没有实现过程,定义方法如: virtual void Eat() = 0; 不要 
在cpp中定义;纯虚函数相当于接口,不能直接实例化,需要派生类来实现函数定义;
有的人可能在想,定义这些有什么用啊 
,我觉得很有用,比如你想描述一些事物的属性给别人,而自己不想去实现,就可以定义为纯虚函数。说的再透彻一些。比如盖楼房,你是老板,你给建筑公司描述清楚你的楼房的特性,多少层,楼顶要有个花园什么的,建筑公司就可以按照你的方法去实现了,如果你不说清楚这些,可能建筑公司不太了解你需要楼房的特性。用纯需函数就可以很好的分工合作了
虚函数和纯虚函数区别
观点一:
类里声明为虚函数的话,这个函数是实现的,哪怕是空实现,它的作用就是为了能让这个函数在它的子类里面可以被重载,这样的话,这样编译器就可以使用后期绑定来达到多态了
纯虚函数只是一个接口,是个函数的声明而已,它要留到子类里去实现。
class A{
protected:
void foo();//普通类函数
virtual void foo1();//虚函数
virtual void foo2() = 0;//纯虚函数
}
观点二:
虚函数在子类里面也可以不重载的;但纯虚必须在子类去实现,这就像Java的接口一样。通常我们把很多函数加上virtual,是一个好的习惯,虽然牺牲了一些性能,但是增加了面向对象的多态性,因为你很难预料到父类里面的这个函数不在子类里面不去修改它的实现
观点三:
虚函数的类用于“实作继承”,继承接口的同时也继承了父类的实现。当然我们也可以完成自己的实现。纯虚函数的类用于“介面继承”,主要用于通信协议方面。关注的是接口的统一性,实现由子类完成。一般来说,介面类中只有纯虚函数的。
观点四:
带纯虚函数的类叫虚基类,这种基类不能直接生成对象,而只有被继承,并重写其虚函数后,才能使用。这样的类也叫抽象类。
虚函数是为了继承接口和默认行为
纯虚函数只是继承接口,行为必须重新定义


<think>我们聚焦于C++中成员函数继承的底层实现原理。核心在于理解虚函数表(vTable)动态绑定机制。以下按层次展开: ### 一、成员函数继承的两种类型 1. **非虚函数(静态绑定)** ```cpp class Base { public: void func() { cout << "Base::func\n"; } // 非虚函数 }; class Derived : public Base { public: void func() { cout << "Derived::func\n"; } // 隐藏基类函数 }; ``` - 编译时确定调用版本 - 通过作用域解析运算符`::`显式指定(如`obj.Base::func()`)[^3] 2. **虚函数(动态绑定)** ```cpp class Base { public: virtual void vfunc() { cout << "Base::vfunc\n"; } // 虚函数 }; class Derived : public Base { public: void vfunc() override { cout << "Derived::vfunc\n"; } // 覆盖基类虚函数 }; ``` - 运行时通过虚函数表(vTable)解析调用 ### 二、虚函数表(vTable)机制 #### 1. 内存结构 每个含虚函数的类在编译时生成唯一的虚函数表: ``` Base类vTable: ┌──────────────┐ │ &Base::vfunc │ → 指向Base::vfunc()的指针 └──────────────┘ Derived类vTable: ┌──────────────────┐ │ &Derived::vfunc │ → 指向Derived::vfunc()的指针 └──────────────────┘ ``` - 派生类vTable会**覆盖**基类虚函数指针 - 多继承时每个基类对应独立vTable子表 #### 2. 对象内存布局 ```cpp Derived d; ``` 对象`d`的内存布局: ``` ┌──────────────┐ │ vPtr │ → 指向Derived类vTable的指针 ├──────────────┤ │ Base成员变量│ ├──────────────┤ │ Derived新增成员│ └──────────────┘ ``` - 每个对象头部隐含`vPtr`指针(指向所属类的vTable) - `vPtr`在构造函数中初始化 ### 三、动态绑定底层原理 考虑以下调用: ```cpp Base* pb = new Derived(); pb->vfunc(); // 输出"Derived::vfunc" ``` 编译器将其转换为: ```cpp (*(pb->vPtr[0]))(pb); // 伪代码 ``` 1. 通过`pb`找到`vPtr`(对象首地址) 2. 通过`vPtr[0]`获取虚函数地址(索引0对应第一个虚函数) 3. 将`pb`作为`this`指针传入函数 ### 四、覆盖(override)与隐藏(hiding)的差异 | 特性 | 覆盖(override) | 隐藏(hiding) | |--------------|--------------------------------|----------------------------------| | **要求** | 基类函数为`virtual` | 函数名相同(无需`virtual`) | | **作用域** | 跨作用域动态绑定 | 作用域隔离 | | **调用表现** | 通过基类指针调用实际派生类版本 | 通过派生类对象只能调用派生类版本 | > 示例说明: > ```cpp > Derived d; > Base* pb = &d; > pb->vfunc(); // 动态绑定→Derived::vfunc (覆盖) > pb->func(); // 静态绑定→Base::func (未覆盖) > d.func(); // 调用Derived::func (隐藏基类版本) > ``` ### 五、多继承场景下的vTable ```cpp class Base1 { virtual void f1(); }; class Base2 { virtual void f2(); }; class Derived : public Base1, public Base2 { void f1() override; // 覆盖Base1::f1 }; ``` 对象内存布局: ``` ┌──────────────┐ │ vPtr1 │ → Derived类中Base1子表的vTable │ Base1成员 │ ├──────────────┤ │ vPtr2 │ → Base2子表的vTable │ Base2成员 │ ├──────────────┤ │ Derived成员 │ └──────────────┘ ``` - 调用`f1`:`(*(vPtr1[0]))(this)` (`this`指向Derived对象起始地址) - 调用`f2`:`(*(vPtr2[0]))(this + offset)` (需调整`this`指针指向Base2子对象) ### 六、关键性能特征 1. **空间开销**: - 每个对象增加一个`vPtr`(32/64位系统增加4/8字节) - 每个类一个vTable(与实例数量无关) 2. **时间开销**: - 虚函数调用比普通函数多两次内存访问(取vPtr→取函数地址) - 无法内联(除非编译器进行去虚拟化优化) --- ### 总结:成员函数继承的本质 - **非虚函数**:编译期名称解析(通过作用域隔离) - **虚函数**:运行时通过vTable动态派发,实现多态 - **vTable核心作用**:解耦调用者与具体实现,支持运行时类型多态 > 动态绑定是C++实现运行时多态性的基石,其代价是可接受的额外间接访问开销。现代编译器通过**去虚拟化(devirtualization)** 优化(如在能确定具体类型时跳过vTable查找)减少开销[^1][^2]。 --- ### 相关问题 1. 虚函数表在多重继承中的内存布局如何?菱形继承问题如何通过虚基类解决? 2. C++11的`override``final`关键字如何影响虚函数表的结构? 3. 纯虚函数虚函数表中如何表示?抽象类的实例化尝试在编译期如何阻止? 4. 构造函数析构函数中调用虚函数为何静态绑定?其vTable切换机制是怎样的? 5. RTTI(运行时类型识别)与虚函数表的关系是什么?`type_info`对象如何存储?[^1][^3]</think>### C++继承中成员函数的底层实现原理 #### 一、核心机制:虚函数表(vTable) 1. **虚函数表结构** - 每个包含虚函数的类在**编译期**生成唯一的虚函数表 - 虚函数表存储该类所有**虚函数函数指针** ```cpp class Base { public: virtual void func1() {} // 虚函数 virtual void func2() {} // 虚函数 void non_virtual() {} // 非虚函数 }; ``` Base类的vTable结构: ``` ┌──────────────┐ │ &Base::func1 │ → 函数指针 ├──────────────┤ │ &Base::func2 │ → 函数指针 └──────────────┘ ``` 2. **对象内存布局** ```cpp Derived d; // 派生类对象 ``` 对象内存结构: ``` ┌──────────────┐ │ vPtr │ → 指向Derived类的vTable ├──────────────┤ │ Base成员变量│ ├──────────────┤ │ Derived成员 │ └──────────────┘ ``` - 每个对象头部隐含`vPtr`指针(通常占4/8字节) - `vPtr`在构造函数中初始化,指向类的vTable[^1] #### 二、函数继承的三种场景 1. **非虚函数继承** ```cpp class Base { public: void foo() { /* 实现 */ } // 非虚函数 }; ``` - 编译期静态绑定:`d.foo()`直接解析为`Base::foo` - 本质:编译器进行名称查找作用域解析[^3] 2. **虚函数覆盖(override)** ```cpp class Derived : public Base { public: void func1() override {} // 覆盖基类虚函数 }; ``` Derived类的vTable变化: ``` ┌──────────────┐ │ &Derived::func1 │ ← 覆盖基类函数指针 ├──────────────┤ │ &Base::func2 │ ← 继承基类函数指针 └──────────────┘ ``` 3. **新增虚函数** ```cpp class Derived : public Base { public: virtual void new_func() {} // 新增虚函数 }; ``` vTable扩展: ``` ┌──────────────┐ │ &Base::func1 │ ├──────────────┤ │ &Base::func2 │ ├──────────────┤ │ &Derived::new_func │ ← 新增函数指针 └──────────────┘ ``` #### 三、动态绑定原理 当通过基类指针调用虚函数时: ```cpp Base* pb = new Derived(); pb->func1(); // 动态调用Derived::func1 ``` 编译器将其转换为: ```cpp // 伪代码 (*(pb->vPtr[n]))(pb); // n是func1在vTable中的索引 ``` 1. 通过`pb`获取`vPtr`(对象首地址) 2. 通过`vPtr[n]`获取函数地址(编译时确定索引n) 3. 将`pb`作为`this`指针传入函数 #### 四、覆盖(override)与隐藏(hiding)的差异 | 特性 | 覆盖(override) | 隐藏(hiding) | |--------------|--------------------------------|----------------------------------| | **要求** | 基类函数为`virtual` | 函数名相同(无需`virtual`) | | **作用域** | 跨作用域动态绑定 | 作用域隔离 | | **调用表现** | 通过基类指针调用实际派生类版本 | 通过派生类对象只能调用派生类版本 | > 示例说明: > ```cpp > class Base { > public: > virtual void vfunc() {} // 虚函数 > void hfunc() {} // 非虚函数 > }; > > class Derived : public Base { > public: > void vfunc() override {} // 覆盖 > void hfunc() {} // 隐藏基类版本 > }; > > Base* pb = new Derived(); > pb->vfunc(); // 调用Derived::vfunc(动态绑定) > pb->hfunc(); // 调用Base::hfunc(静态绑定) > ``` #### 五、多重继承的vTable处理 ```cpp class Base1 { virtual void f1(); }; class Base2 { virtual void f2(); }; class Derived : public Base1, public Base2 { void f1() override; // 覆盖Base1::f1 }; ``` 对象内存布局: ``` ┌──────────────┐ │ vPtr1 │ → Derived类中Base1子表的vTable │ Base1成员 │ ├──────────────┤ │ vPtr2 │ → Base2子表的vTable │ Base2成员 │ ├──────────────┤ │ Derived成员 │ └──────────────┘ ``` - 调用`f1`:`(*(vPtr1[0]))(this)` - 调用`f2`:`(*(vPtr2[0]))(this + offset)`(需调整this指针) #### 六、性能与空间开销 1. **空间开销**: - 每个对象增加一个`vPtr`(32/64位系统增加4/8字节) - 每个类一个vTable(与实例数量无关) 2. **时间开销**: - 虚函数调用比普通函数多两次内存访问(取vPtr→取函数地址) - 无法内联(除非编译器进行去虚拟化优化) > 现代编译器通过**去虚拟化(devirtualization)** 优化:当能确定对象具体类型时(如`Derived d; d.vfunc()`),直接静态调用目标函数,避免vTable查找[^1][^2]。 --- ### 总结:成员函数继承的本质 - **非虚函数**:编译期名称解析(通过作用域隔离) - **虚函数**:运行时通过vTable动态派发,实现多态 - **vTable核心作用**:解耦调用者与具体实现,支持运行时类型多态 > 动态绑定是C++实现运行时多态性的基石,其代价是可接受的额外间接访问开销。实际工程中应遵循:**若需多态则声明虚函数,否则使用非虚函数**。 --- ### 相关问题 1. 虚函数表在多重继承中的内存布局如何?菱形继承问题如何通过虚基类解决? 2. C++11的`override``final`关键字如何影响虚函数表的结构? 3. 纯虚函数虚函数表中如何表示?抽象类的实例化尝试在编译期如何阻止? 4. 构造函数析构函数中调用虚函数为何静态绑定?其vTable切换机制是怎样的? 5. RTTI(运行时类型识别)与虚函数表的关系是什么?`type_info`对象如何存储?[^1][^3]
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值