虚函数-动态绑定-多态

本文深入探讨了C++中的虚函数和多态性。详细解释了虚函数的作用,包括它们如何实现动态绑定,以及虚函数表(vtable)的工作原理。讨论了虚函数对类的影响,如对象内存布局、类型识别(RTTI)和析构函数的使用。此外,还阐述了构造函数和静态成员函数不能成为虚函数的原因,并分析了虚析构函数的重要性。最后,文章通过示例展示了虚函数调用的动态绑定特性及其在多态中的应用。
摘要由CSDN通过智能技术生成

看看下面代码,思考输出:

当函数不是虚函数的时候


class Base {
public:
    Base(int data = 10):ma(data){}
    void show() { std::cout << "Base::show()"; }
    void show(int) { std::cout << "Base::show(int)"; }
private:
    int ma;
};
class Derived :public Base {
public:
    Derived(int data= 20):Base(data),mb(data){}
    void show() { std::cout << "Derived::show()"; }
private:
    int mb;
};
int main() {
    Derived d(50);
    Base* pb = &d; //基类指针指向派生类对象
//静态(指:编译时期)绑定(指:函数调用)
//查看反汇编:call Base::show 和call Base::show(..),所以下面二个是在编译阶段就已经确定好的函数调用,称静态绑定

    pb->show();//Base::show()
    pb->show(10);//Base::show(int)

    std::cout << typeid(pb).name << std::endl; //pb的类型是:class Base*
    std::cout << typeid(*pb).name << std::endl;
    //pb指针指向的类型:class Base(注意不是class Derive,因为没有虚函数typeid打印的是静态类型)
    std::cout << "sizeof(Base): " << sizeof(Base) << std::endl;//4
    std::cout << "sizeof(Derived): " << sizeof(Derived) << std::endl;//8
    return 0;
}

把上面代码中的函数前面加上virtual关键字修饰看看:

一个类添加了虚函数,对这个类有什么影响呢?

1.如果类中定义了虚函数,那么在编译阶段,编译器会给这个类类型产生一个唯一的虚函数表vtable; 虚函数表中存放:支持RTTI的type_info信息虚函数的地址;

虚函数表第一项是:&RTTI,就是指向typid_info的一个指针,RTTI(run-time type information)理解为一个常量字符串;

第二项是一个偏移量,大部分时候是0;

第三项之后开始就是当前类中虚函数的地址了;

程序中可能有很有有虚函数的类,这些类每个都产生自己的虚函数表,这是在编译阶段生成的;当程序运行时候, 每一张虚函数表都会加载到内存的.rodata区域(也就是说虚函数表跟常量字符串一样都放在常量区);

2.一个类中定义了虚函数,那么这个类定义的对象,在对象的起始部分多了一个vptr虚函数表指针指向这个类的虚函数表vtable中虚函数的起始地址;一个类型定义的多个对象,他们的虚函数表指针vptr都指向 同一张虚函数表;

3.一个类里面虚函数的数量不影响对象内存大小,影响的是虚函数表的大小

4:如果派生类中定义的方法和基类继承来的某个方法在返回值、函数名和参数列表都相同, 而且基类的方法是virtual虚函数,那么派生类的这个方法就自动处理成虚函数;这是覆盖>,即派生类 重写了基类的虚函数;(覆盖是指虚函数表中虚函数地址的覆盖)

class Base {
public:
    Base(int data = 10):ma(data){}
    virtualvoid show() { std::cout << "Base::show()"; }
    virtual void show(int) { std::cout << "Base::show(int)"; }
//一个类添加了虚函数,对这个类有什么影响呢?
/*1.如果类中定义了虚函数,那么在<编译阶段>,编译器会给这个类类型产生一个唯一的虚函数表vtable;
虚函数表中存放:支持RTTI的type_info信息和虚函数的地址;
虚函数表第一项是:&RTTI,就是指向typid_info的一个指针,RTTI(run-time type information)理解为一个常量字符串;
    第二项是一个偏移量,大部分时候是0;
     第三项之后开始就是当前类中虚函数的地址了;

程序中可能有很有有虚函数的类,这些类每个都产生自己的虚函数表,这是在编译阶段生成的;当程序运行时候,
每一张虚函数表都会加载到内存的.rodata区域(也就是说虚函数表跟常量字符串一样都放在常量区);
2.一个类中定义了虚函数,那么这个类定义的对象,在对象的起始部分多了一个vptr虚函数表指针,
指向这个类的虚函数表vtable中虚函数的起始地址;一个类型定义的多个对象,他们的虚函数表指针vptr都指向
同一张虚函数表;
3.一个类里面虚函数的数量不影响对象内存大小,影响的是虚函数表的大小;
*/

private:
    int ma;
};
class Derived :public Base {
public:
    Derived(int data= 20):Base(data),mb(data){}
/*总结4:如果派生类中定义的方法和基类继承来的某个方法在返回值、函数名和参数列表都相同,
而且基类的方法是virtual虚函数,那么派生类的这个方法就自动处理成虚函数;这是<覆盖>,即派生类
重写了基类的虚函数;(覆盖是指虚函数表中虚函数地址的覆盖)

*/
    void show() { std::cout << "Derived::show()"; }
private:
    int mb;
};
int main() {
    Derived d(50);
    Base* pb = &d; //基类指针指向派生类对象

    pb->show(); //pb是指针调用虚函数show,所以执行动态绑定,pb的动态类型是Derive,所以这里执行Derive的show函数
//mov eax, dword ptr[pb];
//mov ecx, dword ptr[ecx]
//call ecx (虚函数地址)   动态(指:运行时期)绑定(指:函数调用)

    pb->show(10);//原理同上,知识这个虚函数在派生类中没有重写
/* 先看pb的类型Base里面有没有虚函数:
如果Base里面没有虚函数,那么*pb识别的就是编译时期的类型,即静态类型,*pb->Base类型
如果Base里面有虚函数,那么*pb识别的就是运行时期的类型,RTTI运行时类型识别;
因为指针指向派生类对象,然后派生类对象的前4四节存放着派生类虚函数表地址,然后在派生类虚函数表中找到RTTI,即Derive类型

//std::cout << typeid(pb).name << std::endl; //class Base
//std::cout << typeid(*pb).name << std::endl;//class Derive
   


std::cout << "sizeof(Base): " << sizeof(Base) << std::endl;//8,对象里面多个虚函数表指针
std::cout << "sizeof(Derived): " << sizeof(Derived) << std::endl;//12
return 0;
}

         也可以这样解释:静态绑定编译器在编译阶段已经知道调用那个函数了,在汇编代码层面,直接call 具体函数地址
        而动态绑定直到运行时才能根据指针或者引用的动态类型决定调用哪个函数,在汇编代码层面,是call 寄存器,而寄存器直到运行时才知道存放的是哪个虚函数

派生类的构造过程:1.先调用基类的构造函数初始化基类部分。2:调用派生类构造函数执行派生类部分的初始化

虚函数:

  1. 虚函数的地址会存放在虚函数表中;在类中有虚函数的情况下,对象的前4字节一般是虚函数表表指针vptr,指向类的虚函数表中第一个虚函数的起始地址;虚函数表前面是RTTI运行时类型识别的指针,然后是一个偏移量(通常是0),接下来就是类的虚函数地址了;
  2. 对象必须存在(对象对象才能有vptr,通过vptr找到vtable,在vtable里面知道虚函数地址,进而调用虚函数)

问题1:哪些函数不能实现成虚函数?(构造函数,静态成员函数)

  1. 因为构造函数执行完毕才产生对象,所以构造函数不能是virtal,而且在构造函数中对其他虚函数的调用都是静态绑定;(即:构造函数不能成为虚函数,构造函数里面不会发生动态绑定)
  2. 静态成员函数不能是虚函数,因为静态函数不依赖对象调用的,而虚函数的调用是依赖对象里面的虚函数表指针
  3. 但是析构函数是可以成为虚函数的;(且基类的析构函数最好是虚函数,否则在继承层次上的类对象释放可能出现内存泄露);

        析构函数调用的时候对象是存在的,那么析构函数可以成为虚函数,然后将析构函数的地址放到虚函数表中,通过对象的虚函数表指针访问虚函数表去调用虚析构函数;

问题2:虚析构函数:

什么时候把基类的析构函数定义为虚析构函数呢?(一个类只要可能成为基类,那么最好基类的析构函数定义为virtual的)

        基类的指针(引用)指向堆内存中new出来的派生类对象时候,delete 基类指针,会调用析构函数必须发生动态绑定,否则会导致派生的析构函数无法调用,只调用基类的析构函数

虚函数的调用一定是动态绑定吗?不是

  • 类的构造函数中对虚函数的调用是静态绑定;(构造函数执行过程中正在创建对象,而对象的前4字节是虚函数表指针,编译器会在构造函数中安插代码给vptr赋值指向虚函数表,因为构造函数不能是虚函数,因为此时对象还没创建完成呢)
  • 只有通过指针或者引用调用虚函数才是动态绑定;(指针和引用才有静态类型和动态类型,而动态绑定是根据指针和引用的动态类型决定调用那个函数的,普通对象只有静态类型没有动态类型)

看看如下代码的执行结果就知道了

class Base
{
public:
    Base(int data = 0):ma(data){}
    virtual void show() { cout << "Base::show()" << endl; }
protected:
    int ma;
};
class Derive : public Base
{
public:
    Derive(int data=0):Base(data),mb(data){}
    void show()override { cout << "Derive::show()" << endl; }//编译器自动加上virtual成为虚函数;
//因为派生类show与基类show函数:在返回值,函数名,参数列表都相同,所以是覆盖关系;
//派生类重写了基类的虚函数(覆盖)
private:
    int mb;
};
int main() {
    Base* pb = new Derive(10);
    pb->Base::show(); //通过作用域运算符强制调用基类show方法
    pb->show();//动态绑定,调用派生类的show方法
    Base b;
    Derive d;
//通过对象调用虚函数是静态绑定;只有通过指针或者引用调用虚函数才会发生动态绑定;
    b.show();//静态绑定,编译器直接生成代码:call Base::show
    d.show();//静态绑定,编译器直接生成代码: call Derive::show
}

C++中的多态:

  • 静态(编译期)多态:程序编译时就确定调用哪个函数;

函数重载:即函数名相同,但是函数形参列表类型和个数不同,最终调用哪个函数是根据函数实参来匹配的;所以就是一个函数名展现出了多种形态,但是根据函数实参可以在编译期就确定调用哪个版本的函数;

模板(函数模板和类模板):可以写一套模板用于任何类型,用哪个类型就去实例化哪个类型的函数代码;

模板的实例化是发生在编译阶段的,编译阶段实例化具体类型的函数后,才能调用这个函数;

  • 动态(运行期)多态:程序运行时才知道调用哪个函数;

继承结构中,基类指针(引用)指向派生类对象,通过该指针(引用)调用同名覆盖方法(虚函数),基类指针指向那个派生类对象,就会调用那个派生类对象的覆盖方法,称为多态;多态底层是通过动态绑定实现的,看起来是基类指针调用一个函数代码,当基类指针指向派生类时候,就会调用相应派生类的方法;基类指向指向那个对象,就会访问那个对象的虚函数表指针,进而找到虚函数表,从虚函数表中存放的就是类的虚函数;

继承的好处是什么?

1.可以做代码复用

2.在基类中提供统一的虚函数接口,让派生类重写,就可以使用多态了

抽象类和普通类有什么区别?

1.普通类是用来抽象一个实体的类型;而抽象类并不是抽象某个实体的类型;抽象类主要是为所有实体定义共有的属性和方法,然后派生类就可以直接继承从而复用属性,也为所有派生类保留统一的接口,后面通过虚函数覆盖可以实现多态

2.拥有纯虚函数(virutal func=0)的类叫做抽象类,抽象类不能实例化对象,但是可以定义指针和引用;

看看下面代码,思考输出:

class Animal
{
public:
    Animal(string name) :_name(name) {}
    virtual void bark() = 0;// 纯虚函数
protected:
    string _name;
};
class Cat : public Animal
{
public:
    Cat(string name) :Animal(name) {}
    void bark() override { cout << _name << " bark:miao miao" << endl; }
};
class Dog : public Animal
{
public:
    Dog(string name):Animal(name){}
    void bark() override { cout << _name << " bark: wang wang" << endl; }
};
int main()
{
    Animal* p1 = new Cat("猫");
    Animal* p2 = new Dog("狗");
//类中含有虚函数,那么类对象的前4字节就是指向虚函数表的虚函数表指针;
    int* p11 = (int*)p1;
    int* p22 = (int*)p2;
    //下面交换了二个对象的虚函数表指针的值
    int temp = p11[0]; //p11[0]访问了Cat的前4个字节,即vptr
    p11[0] = p22[0];
    p22[0] = temp;
//下面通过指针调用虚函数发生了动态绑定;
    p1->bark();//p1所指对象的前4字节的虚函数表指针已经改变了,指向了Dog的虚函数表,所以动态绑定调用的是Dog的bark函数
    p2->bark();//与上式同理
    delete p1;
    delete p2;
    return 0;
}

虚函数中形参默认值是静态绑定的:

#include <iostream>
#include <string>

using namespace std;
class Base
{
public:
    virtual void show(int i = 10)
    {
        cout << "call Base::show i:" << i << endl;
    }
};
class Derive : public Base
{
public:
    void show(int i = 20)override {
        cout << "call Derive::show i:" << i << endl;
    }
};
int main()
{
    Base* p = new Derive();
    Derive* pd = dynamic_cast<Derive*>(p);
    通过指针(引用)调用虚函数是动态绑定,通过p所指对象中的虚函数表指针找到虚函数表,进而找到要调用的那个虚函数
    但是:虚函数中的形参默认值是静态绑定的
    原理:调用一个函数的时候,先压参数参数(从右到左-对于带默认值的参数,如果用户提供了那就压入用户提供的实参,否则就把
    形参的默认值压入栈中);而动态绑定调用show方法是在运行阶段才发生,所以在编译时编译器只能看到静态类型的那个类中show函数的默认值;
    p->show(); //Derive::show  i:10
    pd->show(); //Derive::show i: 20
    delete p;
}

看看下面代码,思考输出:成员访问限定符对虚函数的调用有影响吗?

class Base
{
public:
    virtual void show()
    {
        cout << "call Base::show  "  << endl;
    }
};
class Derive : public Base
{

private:
    void s(){}
    void show() override
    {
        cout << "call Derive::show" << endl;
    }
};
int main()
{
    Base* p = new Derive();
    p->show();//派生类的show方法是私有的,这里仍然可以调用;
//因为指针调用虚函数发生动态绑定,是在运行时期才确定调用哪个函数;
//而成员函数能不能调用,权限是不是public,是在编译阶段确定的;成员限定符是在编译阶段看用户代码是否符合语言规范;
//编译阶段编译器看到p类型是Base,会去看Base里面的show函数是不是public,是就可以调用;而最终调用基类还是派生类的
//show函数,要看动态绑定中p实际指向的对象类型;
    delete p;
    return 0;
}

派生类对象中的虚函数表指针可能被多次赋值:

class Base
{
public:
    Base()
    {//【编译器添加代码:给虚函数表指针vptr赋值,指向虚函数表】
        cout << "call Base()" << endl;
        clear();
    }
    void clear()
    {
        memset(this, 0, sizeof(*this));
//从this地址开始把sizeof(*this)这么大的空间全部赋值给0
    }
    virtual void show()
    {
        cout << "call Base::show()" << endl;
    }
};
class Derive : public Base
{
public:
    Derive()
    {//编译器添加代码:调用基类的构造函数;给vptr赋值;  
        cout << "call Derive()" << endl;
    }
    void show()override
    {
        cout << "call Derive::show()" << endl;
    }
};
int main()
{
//解释下面2个代码的运行情况:
    1.有问题
    Base* pb1 = new Base();//基类指针指向基类对象
//在Base的构造函数用户代码之前编译器会给vptr赋值让它指向虚函数表的地址,但是在Base的构造函数之内,
//用户代码把内存内存的前4字节清0了,也就是把vptr清0;然后这里动态绑定,需要从对象中取虚函数表指针访问
//虚函数表,0地址不能读也不能写,所以出错了;
    pb1->show();
    delete pb1;
    2.正常
    Base* pb2 = new Derive();//基类指针指向派生类对象
//当我们new一个派生类对象时候,先调用基类构造函数,再调用派生类的构造函数;
//当调用基类的构造函数时候,在构造函数的用户代码之前vptr会被编译器赋值指向Base的虚函数表,之后再Base的构造函数内vptr被清0;
//之后调用派生类的构造函数,在构造函数用户代码之前vptr会被编译器赋值指向Derive的虚函数表,之后发生动态绑定,
//因为pd2指向Derive对象,所以从Derive对象中的vptr找到vtable,从而调用Derive::show,没有问题;
    pb2->show();
    delete pb2;
}

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值