• C++多态



    什么是多态

    同种消息不同的对象接受后产生不同的状态,知道是这个东西就行,不懂也没有什么问题,看后文就可以。

    多态的定义及实现

    多态是类继承时,对象去调用同一个对象产生不同的行为

    • 要构成多态的条件有两个

    虚函数的重写
    基类的对象或引用调用虚函数

    虚函数的重写

    • 什么是虚函数?

    类的成员函数加上关键字virtual就变成了虚函数。

    • 虚函数重写的条件

    是虚函数,且函数名,返回值的类型,参数类型相同(三同)
    三同,但是只有父类写virtual也构成重写
    特殊情况:

    1. 其他条件相同,返回值的类型为父子对象或指针类型也构成重写——这个也叫做协变
    2. 析构函数的重新:虽然父子的析构函数名不一样,但是在编译看来是相同的,因为它都编译器统一处理为destructor。所以析构函数的重写只需要在基类上加上virtual就可以构成重写。

    为什么对析构函数进行重写呢?

    看下面这段代码:

    class teacher
    {
    public:
    	~teacher()
    	{
    		cout << "~teacher()" << endl;
    	}
    };
    class student:public teacher
    {
    public:
    	~student()
    	{
    		cout << "~student()" << endl;
    	}
    };
    int main()
    {
    	teacher* a = new teacher;//1
    	delete a;
    
    	teacher* b = new student;//2
    	delete b;
    	
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    运行上面的代码:image.png
    我们就会发现,1正常释放,2只是释放了基类的,没有释放父类的,这就会造成内存泄漏。
    当我们写成虚函数virtual ~teacher(),构成多态之后,就可以全部正常的对子类释放(调用子类的析构函数时,先析构子类,再析构父类):image.png

    C++11中的 overridefinal

    final:修饰虚函数,表示该函数不能被重写
    override:检查派生类中虚哈四年有没有被重写,没有被重写就会报错

    抽象类

    包含纯虚函数的类,叫做抽象类。
    纯虚函数——虚函数后面加上一个=0
    抽象类就是抽象,即**不能实例化出来对象。**派生类继承了也不能实例化出来对象,必要要进行重写,才能实例化出来对象。

    class Base
    {
    public:
    	virtual void print() = 0;
    };
    class Exten :public Base
    {
    };
    int main()
    {
    	Base a;
    	Exten b;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    上面代码肯定会报错,image.png
    如果想让派生类Exten可以实例化出来对象,必须重写

    class Exten :public Base
    {
    public:
    	virtual void print()
    	{
    		cout << "可以实例化对象" << endl;
    	}
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    接口继承和实现继承

    虚函数的继承是接口继承,目的是为了重写,接口继承就是函数的声明继承下来,定义不继承,会重写定义。
    实现继承:普通函数的继承就是实现继承,包基类中的函数全部继承下来。

    多态实现的原理

    虚函数表

    那些虚函数都放在哪里呢?虚函数放在虚函数表中,所以的虚函数都放在学函数表中
    类中有个虚函数表的指针,指向这个表,在vs2019中,这个指针为vfptr

    class teacher
    {
    public:
    	virtual void print()
    	{
    		cout << "void print()" << endl;
    	}
    };
    //main
    teacher a;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    image.png

    实现多态后,派生类中的虚函数表

    class teacher
    {
    public:
    	virtual void print()
    	{
    		cout << "teacher void print()" << endl;
    	}
    	virtual void f1()
    	{
    		cout << "teacher void f1()" << endl;
    	}
    };
    class student :public teacher
    {
    public:
    	virtual void print()
    	{
    		cout << "student void print()" << endl;
    	}
    
    };
    int main()
    {
    	teacher a;
    	student b;
    	
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    image.png

    通过上面的代码和调试信息可以看出,在派生类中,虚表中的print被重写成studen类中的。

    • 那么多态的特性是怎么实现的

    还是上面的代码,测试不一样

    int main()
    {
    	teacher a;
    	student b;
    
    	teacher& x = a;
    	x.print();
    	
    	teacher& y = b;
    	y.print();
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    运行结果:

    image.png

    • 分析

    image.png

    x调用print直接去基类的虚表中找
    y调用print去派生类的虚表中找,此时的虚表中的print已经重写。
    这也是为什么说,指向哪里调哪里——父类的指针或引用指向父类调父类,指向子类调子类。

    动态绑定,静态绑定

    • 静态绑定:

    编译的时候就确定地址,比如:函数重载,模板

    • 动态绑定

    运行的时候去找地址,比如多态

    显然上述的代码就是动态绑定,在程序运行起来之后,去找print的地址。
    要想观察这个调用print是什么方式的,需要看一下汇编代码。

    单继承虚函数表

    上面那个代码就是单继承,但是上面那个代码中,派生类没有写自己的虚函数,只是不继承的虚函数重写了。我们知道只要是虚函数都会放在虚函数表中,但是vs的窗口不能显示出来。

    class student :public teacher
    {
    public:
    	virtual void print()
    	{
    		cout << "student void print()" << endl;
    	}
    	virtual void f2()
    	{
    		cout << "student void f2()" << endl;
    	}
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    image.png

    我们看不见派生类中的f2函数,但是它确实咋虚函数表里面,下面我们写一个程序把它打印出来。
    想打印出来它,就要先取到他的地址,然后还要知道它是什么类型?

    1. 取到它的地址
      直接取对象的地址就可以,虚表的指针都放在对象的第一个位置
    2. 什么类型的?

    虚表的指针它是一个函数指针数组指针,什么意思呢?——它是一个指针,它指向一个数组,数组的每个元素都是一个函数指针。

    typedef void(*VF)();
    void printvf(VF* arr)
    {
    	for (int i = 0; i < 3; i++)
    	{
    		printf("%p", arr + i);
    		arr[i]();	
    	}
    }
    //main中调用
    printvf((VF*)*((int*)(&b)));
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    image.png

    从打印的结果上看,就可以证明上面我说的了。
    当我们调换派生类中printf2的位置的时候也是打印相同的结果;说明虚表中先继承基类的虚函数然后再放自己的虚函数。基类的虚函数是按声明的顺序储存在虚表中。

    多继承虚函数表

    我们思考派生类中没有重写的虚函数是单独放在一个虚表中,还是放在哪个继承的虚表中
    下面我们用代码测试一下

    class A
    {
    public:
    	virtual void fA()
    	{
    		cout << "void fA()" << endl;
    	}
    };
    class B:public A
    {
    public:
    	virtual void fB()
    	{
    		cout << "void fB()" << endl;
    	}
    };
    class C:public A
    {
    public:
    	virtual void fC()
    	{
    		cout << "void fC()" << endl;
    	}
    };
    class D :public B, public C
    {
    public:
    	virtual void fD()
    	{
    		cout << "void fD" << endl;
    	}
    	virtual void fun()
    	{
    		cout << "void fun" << endl;
    	}
    };
    typedef void(*VF)();
    void printvf(VF* arr,int n)
    {
    	for (int i = 0; i < n; i++)
    	{
    		printf("%p", arr + i);
    		arr[i]();
    	}
    }
    int main()
    {
    	cout<<"测试是否在第一个虚表" << endl;
    	D d;
    	printvf((VF*)*(int*)(&d), 3);//有的话就是3个
    	
    	cout << "测试是否在第二个虚表" << endl;
    	C* c = &d;
    	printvf((VF*)*(int*)c, 3);
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57

    直接看结果:image.png
    可以看出多继承有多个虚表,子类没有重写的函数放在第一个虚表中

    面试常见的问题

    1. inline函数可以是虚函数吗?
    2. 静态成员可以是虚函数吗?
    3. 构造函数,拷贝构造,赋值运算符的重载可以是虚函数吗?
    4. 析构函数可以是虚函数吗?
    5. 对象访问普通函数快还是虚函数快
    6. 虚函数表在什么阶段产生的,存在哪里?
    1. inline可以是虚函数,inline只是建议编译器把函数当作内联函数,但是,内联函数在编译的时候就展开了,没有函数栈帧的开辟,而虚函数在要在运行的时候去虚函数表中去早该函数的地址。
    2. 静态的成员不能是虚函数,静态成员没有*this指针,静态函数只能用类域的方式调用,而虚函数的调用需要在虚函数表在中调用。
    3. 构造函数和拷贝构造函数不能是虚函数。因为虚函数是放在虚函数表中,而虚表指针是在构造函数初始化列表中初始化的。赋值运算符的重载是可以是虚函数的
    4. 析构函数可以是虚函数,虽然析构函数的函数名不一样,但是在编译器看来,都被处理为destructor,上文有解释为什么要把析构函数写成虚函数。
    5. 如果是普通的函数,那么是一样快的,如果构成多态,普通函数快
    6. 虚函数表在编译阶段就生成了,存在内存中的代码段
  • 相关阅读:
    面试算法2:二进制加法
    JavaScript学习总结(内置对象、简单数据类型和复杂数据类型)
    2023国赛数学建模E题思路代码 - 黄河水沙监测数据分析
    【树莓派不吃灰】Linux服务器篇(核心概念)
    项目经理--要具备的能力
    MySQL
    【共享单车数据专题】共享单车数据分析的技术要点
    ChatGPT实战100例 - (18) 用事件风暴玩转DDD
    H5 简约四色新科技风引导页源码
    Python数据攻略-Pandas进行CSV和Excel文件读写
  • 原文地址:https://blog.csdn.net/m0_60598323/article/details/128065041