• C++多态


    1. 多态

    多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同类型的对象去完成时会产生出不同的状态。

    举个栗子:比如买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人买票时是优先买票。

    2. 模拟买票场景

    2.1 Person、Student、Soldier

    要玩出这个多态呢,首先要写一个基类,然后还要写一个买票的虚函数

    class Person {
    public:
    	Person(const char* name)
    		:_name(name)
    	{}
    
    	// 虚函数
    	virtual void BuyTicket() { cout << _name << "Person:买票-全价 100¥" << endl; }
    
    protected:
    	string _name;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    学生、军人买票:

    class Student : public Person {
    public:
    	Student(const char* name)
    		:Person(name)
    	{}
    
    	virtual void BuyTicket() { cout << _name << " Student:买票-半价 50 ¥" << endl; }
    };
    
    class Soldier : public Person {
    public:
    	Soldier(const char* name)
    		:Person(name)
    	{}
    
    	virtual void BuyTicket() { cout << _name << " Soldier:优先买预留票-88折 88 ¥" << endl; }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    在父类中,BuyTicket如果不是虚函数,它与子类中的BuyTicket构造隐藏关系,就不能直接调用父类的BuyTicket。

    如果是虚函数且函数名、参数、返回值都相同,那么它们就是重写(覆盖)关系。

    所以我们说Student和Soldier的BuyTicket重写或覆盖了Person的BuyTicket函数。

    2.2 菜单界面

    首先多态的条件是必须用父类的指针或引用去调用每一个父类或者子类的BuyTicket函数,这就要涉及赋值兼容的转换,父类的指针/引用可以指向父类或者子类的对象:

    void Pay(Person* ptr)
    {
    	ptr->BuyTicket();
    }
    
    • 1
    • 2
    • 3
    • 4

    2.3 多态的两个要求

    1. 子类虚函数重写的父类虚函数 (重写:函数名、参数、返回值都相同+虚函数)
    2. 父类指针或者引用去调用虚函数。
    int main()
    {
    	int option = 0;
    	cout << "=======================================" << endl;
    	do 
    	{
    		cout << "请选择身份:";
    		cout << "1、普通人 2、学生 3、军人" << endl;
    		cin >> option;
    		cout << "请输入名字:";
    		string name;
    		cin >> name;
    		switch (option)
    		{
    		case 1:
    		{	// 加{}才能创建对象
    				  Person p(name.c_str());
    				  Pay(p);
    				  break;
    		}
    		case 2:
    		{
    				  Student s(name.c_str());
    				  Pay(s);
    				  break;
    		}
    		case 3:
    		{
    				  Soldier s(name.c_str());
    				  Pay(s);
    				  break;
    		}
    		default:
    			cout << "输入错误,请重新输入" << endl;
    			break;
    		}
    		cout << "=======================================" << endl;
    	} while (option != -1);
    
    	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

    怎么让不同的用户去调用BuyTicket函数?ok,大家看我是一个父类的指针或者引用,不同的对象都可以传给我,父类和子类都可以传给我。

    2.4 虚函数重写的两个例外

    class A{};
    class B : public A {};
    
    // 虚函数重写对返回值要求有一个例外:协变,父子关系指针和引用
     
    class Person {
    public:
    	virtual A& f() { 
    		cout << "virtual A* Person::f()" << endl;
    		return A(); 
    	}
    };
    
    class Student : public Person {
    public:
    	// 子类虚函数没有写virtual,f依旧时虚函数,因为先继承了父类函数接口声明
    	// 重写父类虚函数实现
    	// ps:我们自己写的时候子类虚函数也写上virtual
    	// B& f() { 
    	virtual B& f() {
    		cout << "virtual B* Student::f()" << endl;
    		return B(); 
    	}
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    大家看,这里的返回值是不同的,但是也构成多态。虚函数重写对返回值要求有一个例外,叫作协变。协变的这个返回值类型必须是父子关系的指针或者引用。

    子类虚函数没有写virtual,f依旧时虚函数,因为先继承了父类函数接口声明,重写的是父类虚函数实现,我们自己写的时候子类虚函数最好也写上virtual。

    2.5 一道经典题

    以下程序输出结果是什么()

    class A
    {
    public:
    	virtual void func(int val = 1){ std::cout<<"A->"<< val <"<< val <test();
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    A: A->0 B: B->1 C: A->1 D: B->0 E: 编译出错 F: 以上都不正确

    子类的指针去调用test,因为test被public继承下来了。但是即使被继承下来的this指针还是A*的this,这里又涉及了赋值兼容的转换,因为B的指针要调用成员函数得把B的指针传给A*的this。子类传给父类,发生了切片,相当于test里面的this指向了new出来的B对象。

    所以这里也符合多态的两个条件,参数是相同的,因为只看类型,不看缺省值。

    子类继承重写父类虚函数是接口继承,会把缺省值也继承下来。重写的是函数的实现。

    image-20220721091834034

    2.6 C++11 override和final

    C++11多增加了两个关键字:override和final

    (1)final:修饰虚函数,表示该虚函数不能再被重写

    修饰类,表示该类是最终类不能被继承

    class Car
    {
    public:
    	virtual void Drive() final {}
    };
    class Benz :public Car
    {
    public:
    	virtual void Drive() {cout << "Benz-舒适" << endl;}
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    (2)override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错

    class Car{
    public:
    	virtual void Drive(){}
    };
    class Benz :public Car {
    public:
    	virtual void Drive() override {cout << "Benz-舒适" << endl;}
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    2.7重载、覆盖(重写)、隐藏(重定义)的对比

    image-20220721100922090

    同名成员变量也是隐藏关系。

    2.8 接口继承和实现继承

    普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。

    3. 析构函数的特殊性

    如果父子类的析构函数不是虚函数,那它们的关系默认是隐藏关系,因为析构函数的函数名会被统一处理为destructor。所以如果不加virtual,子类析构会调用一遍Person,父类析构又会调用一遍Person。

    加了virtual就从重定义(隐藏)关系 -> 重写(覆盖)关系,满足虚函数+三同的条件。而且也可以满足多态调用的需求。

    class Person {
    public:
    	virtual ~Person() 
    	{ 
    		cout << "~Person()" << endl;
    	}
    };
    
    class Student : public Person {
    public:
    	// Person析构函数加了virtual,关系就变了
    	// 重定义(隐藏)关系 -> 重写(覆盖)关系
    	virtual ~Student() 
    	{ 
    		cout << "~Student()" << endl;
    		delete[] _name;
    		cout << "delete:" << (void*)_name << endl;
    	}
    
    private:
    	char* _name = new char[10]{'j','a','c','k'};
    };
    
    int main()
    {
    	// 对于普通对象是没有影响的
    	//Person p;
    	//Student s;
    
    	// 期望delete ptr调用析构函数是一个多态调用
    	// 如果设计一个类,可能会作为基类,其析构函数最好定义为虚函数
    	Person* ptr = new Person;
    	delete ptr; // ptr->destructor() + operator delete(ptr)
    
    	ptr = new Student;
    	delete ptr;  // ptr->destructor() + operator delete(ptr):实际上是free
    
    	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

    image-20220721094216050

    4. 抽象类

    在虚函数的后面写上 =0 ,则这个函数为纯虚函数。**包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。**派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

    所以这里基类不能定义对象,但是还是可以定义被子类赋值转换的指针或者引用的。

    class Car
    {
    public:
    	virtual void Drive() = 0;
    };
    class Benz :public Car
    {
    public:
        virtual void Drive()
        {
        cout << "Benz-舒适" << en
        }
    };
    class BMW :public Car
    {
    public:
        virtual void Drive()
        {
        cout << "BMW-操控" << endl;
        }
    };
    
    void Test()
    {
        Car* pBenz = new Benz;
        pBenz->Drive();
        Car* pBMW = new BMW;
        pBMW->Drive();
    }
    
    • 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

    纯虚函数是基类的纯虚函数,它的主要作用是让纯虚函数的抽象类不能实例化对象,它还带有一种间接功能:要求子类必须重新纯虚函数才能实例化出对象。

    5 多态底层原理

    5.1 虚函数表

    // 这里常考一道笔试题:sizeof(Base)是多少?
    class Base
    {
    public:
        virtual void Func1()
        {
        cout << "Func1()" << endl;
        }
    private:
    	int _b = 1;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    通过观察测试我们发现b对象是8bytes,**除了_b成员,还多一个__vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。**一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。那么派生类中这个表放了些什么呢?image-20220721105043636

    // 针对上面的代码我们做出以下改造
    // 1.我们增加一个派生类Derive去继承Base
    // 2.Derive中重写Func1
    // 3.Base再增加一个虚函数Func2和一个普通函数Func3
    class Base
    {
    public:
        virtual void Func1()
        {
            cout << "Base::Func1()" << endl;
        }
        virtual void Func2()
        {
            cout << "Base::Func2()" << endl;
        }
        void Func3()
        {
            cout << "Base::Func3()" << endl;
        }
    private:
    	int _b = 1;
    };
    
    class Derive : public Base
    {
    public:
        virtual void Func1()
        {
            cout << "Derive::Func1()" << endl;
        }
    private:
    	int _d = 2;
    };
    
    int main()
    {
        Base b;
        Derive d;
        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

    通过调试观察,我们可以画出基类和派生类的存储模型:

    image-20220721105816810

    (1) 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在部分的,另一部分是自己的成员。
    (2) 基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
    (3) 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会放进虚表。
    (4) 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr
    (5) 总结一下派生类的虚表生成:

    a.先将基类中的虚表内容拷贝一份到派生类虚表中

    b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数

    c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
    (6) 这里还有一个童鞋们很容易混淆的问题:虚函数存在哪的?虚表存在哪的? 答:虚函数存在虚表,虚表存在对象中。注意上面的回答的错的。但是很多童鞋都是这样深以为然的。注意虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。那么虚表存在哪的呢?实际我们去验证一下会发现vs下是存在代码段的。

    5.2多态的原理

    那多态的底层原理到底是什么?现在大家应该多多少少摸到了一点门道了。

    多态到底要去调用哪个函数不是按类型去定的,是去查虚函数表,有可能查的是父类对象,也有可能是子类切片赋值转换的对象,反正都是父类的指针或者引用。

    image-20220721111112064

    大家再看,假设我子类也写了一个非虚函数Func3,那么再用父类指针调用子类的Func1和Func3有什么不同。

    class Derive : public Base
    {
    public:
        virtual void Func1()
        {
            cout << "Derive::Func1()" << endl;
        }
        void Func3()
        {
            cout << "Derive::Func3()" << endl;
        }
    private:
    	int _d = 2;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    image-20220721112316652

    大家看,为什么调不到子类的Func3?

    (1)Func3不是虚函数,没有放入虚函数表。

    (2)从汇编角度看,普通调用是编译式决议,多态调用是运行时决议。

    所以这里是编译时决议了,直接去call了Base的Func3。

    编译器只要发现你符合多态调用,就会运行时决议。

    image-20220721113237448

    5.3 对象赋值为什么实现不了多态

    子类赋值给父类对象,也可以切片。为什么实现不了多态?

    调用的时候编译时就确定了。

    image-20220721115639507

    很简单因为对象切片的时候,子类只会拷贝成员给父类对象,不会拷贝虚表指针。

    image-20220721115528627

    5.4 动态绑定与静态绑定

    (1) 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
    (2) 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态
    image-20220721121115820

    6 虚表、虚函数

    6.1 虚函数存在哪?

    假设我在子类中又写了一个虚函数Func4,Func4会进虚表吗?

    class Derive : public Base
    {
    public:
        virtual void Func1()
        {
            cout << "Derive::Func1()" << endl;
        }
        void Func3()
        {
            cout << "Derive::Func3()" << endl;
        }
        virtual void Func4()
        {
            cout << "Derive::Func4()" << endl;
        }
    private:
    	int _d = 2;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    image-20220721121302592

    Func4呢?怎么没见?ok,我明确告诉大家只要是虚函数就会进虚表。

    我们来看看内存:

    image-20220721121613475

    我们猜002d150a就是Func4的地址,我们来验证一下:

    // 取内存值,打印并调用,确认是否是func4
    //typedef void(*)() V_FUNC; // 不支持
    typedef void(*V_FUNC)();
    
    • 1
    • 2
    • 3

    image-20220721122840521

    6.2 虚表存在哪?

    int main()
    {
    
    	//虚表,一个类型,一个虚表,所以这个类型对象都存这个虚表指针
    	Base b1;
    	Base b2;
    	Base b3;
    	Base b4;
    	PrintVFTable((V_FUNC*)(*((int*)&b1)));
    	PrintVFTable((V_FUNC*)(*((int*)&b2)));
    	PrintVFTable((V_FUNC*)(*((int*)&b3)));
    	PrintVFTable((V_FUNC*)(*((int*)&b4)));
    
    	int a = 0;
    	static int b = 1;
    	const char* str = "hello world";
    	int* p = new int[10];
    	printf("栈:%p\n", &a);
    	printf("静态区/数据段:%p\n", &b);
    	printf("静态区/数据段:%p\n", &c);
    	printf("常量区/代码段:%p\n", str);
    	printf("堆:%p\n", p);
    	printf("虚表:%p\n", (*((int*)&b4)));
        
        // 成员函数取地址都得这么玩
    	printf("函数地址:%p\n", &Derive::Func3);
    	printf("函数地址:%p\n", &Derive::Func2);
    	printf("函数地址:%p\n", &Derive::Func1);
    
    	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

    image-20220721123610545

    我们发现虚表和函数的地址更接近常量区/代码段,这已经说明了虚表存在常量段,也说明了虚函数和普通函数都是放在常量段,只是虚函数又要进虚表而已。

  • 相关阅读:
    各大AI模型训练成本大比拼
    Go语言入门【3】条件语句
    Git+py+ipynb Usage
    MATLAB SAC算法reward震荡问题
    爱心熊 Game Jam 来啦,30,000 SAND等你们来赢取!
    基础算法之递归
    梦幻西游手游:工坊进阶考试题目攻略—考古、乐艺篇
    Kafka 集群安装
    IMZA120R040M1HXKSA1,IMW65R107M1H规格 MOSFET - 单个晶体管
    记一次RestTemplate消息类型不匹配的BUG定位
  • 原文地址:https://blog.csdn.net/iwkxi/article/details/126403299