• 【C++】多态学习


    多态的概念与定义

    多态的概念

    多态就是多种形态,简单理解就是不同的对象去执行某个行为时会产生出不同的状态表现。
    多态表现在继承关系中,继承关系的类对象去调用同一函数,会产生不同的状态行为表现。
    例如,在买票体系中,普通人(Person)买票是全价,学生(Student)买票是半价。

    构成多态的两个条件

    1. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数重写。
    2. 必须是通过基类的指针或者引用调用虚函数。

    虚函数与重写

    虚函数:被virtual关键字修饰的类成员函数
    虚函数的重写:
    重写也叫覆盖。重写要满足三同条件,三同条件也是建立在虚函数的基础上。
    三同条件要求派生类虚函数与基类虚函数的返回值类型、函数名、参数列表完全相同。

    class Person
    {
    public:
    	virtual void BuyTicket()
    	{
    		cout << "买票-全价" << endl;
    	}
    };
    
    class Student : public Person
    {
    public:
    	/* 
    	* 注意:子类虚函数不加virtual,依旧构成重写
    	* 因为继承后基类的虚函数被继承下来在派生类依旧保持虚函数属性
    	* 但实际最好加上virtual,否则写法不是很规范
    	*/
    	void BuyTicket()
    	{
    		cout << "买票-半价" << endl;
    	}
    };
    
    void Func(Person& p)
    {
    	p.BuyTicket();
    }
    void Test1()
    {
    	Person p;
    	Student st;
    
    	Func(p);
    	Func(st);
    }
    
    • 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

    要实现多态,那多态的两个条件必须严格遵守,任何一个条件不符合规则,或任何一个条件下的小条件不满足,都无法成功实现多态。

    重写的两个特例
    1. 协变
      派生类重写基类虚函数时,基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用。
    class A
    {};
    
    class B : public A
    {};
    
    class Person
    {
    public:
    	//virtual Person* BuyTicket()
    	virtual A* BuyTicket()
    	{
    		cout << "买票-全价" << endl;
    		//return this;
    		return nullptr;
    	}
    };
    
    class Student : public Person
    {
    public:
    	// 重写的协变:返回值可以不同,要求必须是父子关系的指针或者引用
    	// 这里满足父子关系即可,不一定非要某类父子关系
    	virtual B* BuyTicket()
    	{
    		cout << "买票-半价" << endl;
    		//return this;
    		return nullptr;
    	}
    };
    
    void Func(Person& p)
    {
    	p.BuyTicket();
    }
    void Test2()
    {
    	Person p;
    	Student st;
    
    	Func(p);
    	Func(st);
    }
    
    • 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
    1. 析构函数的重写
      一眼看去,基类与派生类中析构函数的重写似乎不满足三同中的函数名相同,其实不然,可以理解为编译器对析构函数的名称做了特殊处理,在程序编译后析构函数的名称统一处理成了destructor
      所以,只要基类的析构函数是虚函数,此时派生类的析构函数只要定义,都与基类的析构函数构成重写。
      而且一般建议,将继承体系中析构函数定义成虚函数。下面的例子可以帮助参考。
    class Person
    {
    public:
    	//~Person()
    	virtual ~Person()
    	{
    		cout << "~Person()" << endl;
    	}
    };
    
    class Student : public Person
    {
    public:
    	//~Student()
    	virtual ~Student()
    	{
    		cout << "~Student()" << endl;
    	}
    };
    
    void Test3()
    {
    	Person* p1 = new Person;
    	delete p1;
    
    	Person* p2 = new Student;
    	delete p2;
    }
    
    • 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

    在这里插入图片描述

    final 和 override

    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
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    final也可以修饰类,表示该类不能被继承。

    1. override
      用于检查派生类虚函数是否重写了基类某个虚函数,如果没有重写会编译报错。
    class Car
    {
    public:
    	virtual void Drive()
    	{}
    };
    
    class Benz : public Car
    {
    public:
    	// override 检查子类虚函数是否完成重写
    	virtual void Drive() override
    	{
    		cout << "Benz" << endl;
    	}
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

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

    在这里插入图片描述
    两个基类和派生类的同名函数不构成重写,就是构成重定义。

    抽象类

    虚函数的后面加上=0,则表示这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(接口类)。
    抽象类无法直接实例化出对象。抽象类被派生类继承后,派生类如果不重写纯虚函数,派生类也不能实例化出对象。
    纯虚函数规范了派生类必须进行重写,体现了接口继承。

    class Car
    {
    public:
    	virtual void Drive() = 0;
    };
    
    class Benz : public Car
    {
    public:
    	virtual void Drive()
    	{
    		cout << "Benz" << endl;
    	}
    };
    
    void Test4()
    {
    	Benz b;
    	b.Drive();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    多态的原理

    class Base
    {
    public:
    	virtual void Func1()
    	{
    		cout << "Func1()" << endl;
    	}
    private:
    	int _b = 0;
    };
    
    void Test5()
    {
    	cout << "sizeof Base: " << sizeof Base << endl;
    	Base b;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    在这里插入图片描述
    在这里插入图片描述
    从上面结果可以看出,b对象中,除了_b成员,还多了一个_vfptr的指针(虚函数表指针,v代表virtual,f代表function)。
    一个含有虚函数的类中都至少有一个虚函数表指针,而虚函数的地址被放到虚函数表(简称虚表)中。
    Test5的代码改造一下,进一步观察。

    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 = 0;
    };
    
    class Derive : public Base
    {
    public:
    	virtual void Func1()
    	{
    		cout << "Derive::Func1()" << endl;
    	}
    private:
    	int _d = 0;
    };
    
    void Test6()
    {
    	Base b;
    	Derive d;
    }
    
    • 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

    在这里插入图片描述
    通过观察,可以知道基类b对象和派生类d对象的虚表是不一样的。因为Func1完成了重写,所以d对象的虚表中存的是重写的Derive::Func1(),这也是重写被叫做覆盖的道理,即覆盖就是虚表中虚函数的覆盖。(重写是语法层的叫法,覆盖是原理层的叫法)
    其实,派生类虚表是从基类虚表拷贝过来的,如果派生类重写了基类的某个虚函数,就用派生类自己的虚函数覆盖虚表中基类的虚函数,派生类自己新增加的虚函数按其在派生类中的声明次序,依次增加到派生类虚表的最后。
    下面再通过之前买票的例子Test1帮助阐述多态的原理。

    class Person
    {
    public:
    	virtual void BuyTicket()
    	{
    		cout << "买票-全价" << endl;
    	}
    };
    
    class Student : public Person
    {
    public:
    	virtual void BuyTicket()
    	{
    		cout << "买票-半价" << endl;
    	}
    };
    
    void Func(Person& p)
    {
    	p.BuyTicket();
    }
    void Test7()
    {
    	Person Mike;
    	Student Allen;
    
    	Func(Mike);
    	Func(Allen);
    }
    
    • 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

    MikeAllen通过Func传给p
    p指向Mike时,就是在Mike的虚表中找到虚函数Person::BuyTicket
    p指向Allen时,就是在Allen的虚表中找到虚函数Student::BuyTicket
    这样就实现了不同对象去执行同一行为时,展现出不同形态的情况。
    多态的本质总结:
    对象多态成员函数调用时,会到对象的虚表中找到对应的虚函数地址,进行调用。

    静态绑定与动态绑定

    1. 静态绑定又称前期绑定/早绑定。
      是指在程序编译期间就确定了程序的行为。也称静态/编译时多态。
      像重载,或是普通类成员函数的调用(直接call函数地址)。
      在这里插入图片描述
    2. 动态绑定又称后期绑定/晚绑定。
      是指在程序运行过程中,需要根据具体情况确定程序的具体行为。也称动态/运行时多态。
      在这里插入图片描述

    单继承与多继承关系下的虚函数表(派生类)

    单继承中的虚函数表查看

    class Base
    {
    public:
    	virtual void func1()
    	{
    		cout << "Base:func1" << endl;
    	}
    
    	virtual void func2()
    	{
    		cout << "Base:func2" << endl;
    	}
    
    private:
    	int _b = 0;
    };
    
    class Derive : public Base
    {
    public:
    	virtual void func1()
    	{
    		cout << "Derive::func1" << endl;
    	}
    
    	virtual void func3()
    	{
    		cout << "Derive::func3" << endl;
    	}
    
    	virtual void func4()
    	{
    		cout << "Derive::func4" << endl;
    	}
    
    private:
    	int _d = 1;
    };
    
    void Test8()
    {
    	Base b;
    	Derive d;
    }
    
    • 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

    Test8测试代码调试时看到的虚表可能不完整,可以通过下面函数对虚表进行打印。

    // VFPTR是一个函数指针,指向的函数参数为void,返回值为void
    typedef void(*VFPTR)();
    
    void PrintVFTable(VFPTR table[])
    {
    	for (size_t i = 0; table[i] != nullptr; ++i)
    	{
    		printf("vft[%d]:%p\n", i, table[i]);
    		table[i](); // 函数回调
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    对于PrintVFTable函数的调用如下。

    /*
    * 1.先取对象的地址,强转成int*,可以拿到头四个字节的地址
    * 2.再解引用取到的是虚函数表的指针,强转成VFPTR*,就可以进行传递
    */
    PrintVFTable((VFPTR*)(*(int*)&b));
    cout << endl;
    PrintVFTable((VFPTR*)(*(int*)&d));
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    在这里插入图片描述

    多继承中的虚函数表查看

    class Base1
    {
    public:
    	virtual void Func1() { cout << "Base1::Func1" << endl; }
    	virtual void Func2() { cout << "Base1::Func2" << endl; }
    
    private:
    	int _b1 = 1;
    };
    
    class Base2
    {
    public:
    	virtual void Func1() { cout << "Base2::Func1" << endl; }
    	virtual void Func2() { cout << "Base2::Func2" << endl; }
    
    private:
    	int _b2 = 2;
    };
    
    class Derive : public Base1, public Base2
    {
    public:
    	virtual void Func1() { cout << "Derive::Func1" << endl; }
    	virtual void Func3() { cout << "Derive::Func3" << endl; }
    
    private:
    	int _d = 3;
    };
    
    void Test9()
    {
    	cout << "sizeof Derive: " << sizeof Derive << endl;
    	Derive d;
    }
    
    • 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

    在这里插入图片描述
    对内存的查看:
    在这里插入图片描述
    d对象继承自两个父类,具有两张虚表。
    下面通过PrintVFTable对两张表中的内容进行查看。

    // 第一个虚表的查看
    PrintVFTable((VFPTR*)(*(int*)&d));
    cout << endl;
    // 第二个虚表的查看 - 方法一
    PrintVFTable((VFPTR*)(*(int*)((char*)&d + sizeof(Base1))));
    // 第二个虚表的查看 - 方法二
    Base2* pb = &d;
    PrintVFTable((VFPTR*)(*(int*)(pb)));
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    在这里插入图片描述
    可以看到多继承派生类的未重写的虚函数放在第一个所继承基类部分的虚函数表中。
    其实子类有几个父类,如果父类有虚函数,则就会有几张虚表,子类自己的虚函数只会放到第一个父类的虚表后面。
    这里深入查看,发现两张虚表中虽然存的都是Derive::Func1,但调用时所用的地址却是不一样的,这是如何做到的?下面通过查看汇编来看看。

    Derive d;
    
    Base1* pb1 = &d;
    Base2* pb2 = &d;
    
    d.Func1(); // 普通函数调用
    
    pb1->Func1(); // 多态调用
    pb2->Func1(); // 多态调用
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    在这里插入图片描述
    通过汇编的查看可以发现,虽然最初的地址不同,但最后都能跳到同一处进行函数调用,即Deriver::Func1

    菱形继承与菱形虚拟继承

    菱形继承

    class A
    {
    public:
    	virtual void func1() { cout << "A::func1" << endl; };
    public:
    	int _a;
    };
    class B : public A
    {
    public:
    	virtual void func1() { cout << "B::func1" << endl; };
    	virtual void func2() { cout << "B::func2" << endl; };
    public:
    	int _b;
    };
    class C : public A
    {
    public:
    	virtual void func1() { cout << "C::func1" << endl; };
    	virtual void func2() { cout << "C::func2" << endl; };
    public:
    	int _c;
    };
    class D : public B, public C
    {
    public:
    	int _d;
    };
    
    void Test10()
    {
    	D d;
    	d.B::_a = 1;
    	d.C::_a = 2;
    	d._b = 3;
    	d._c = 4;
    	d._d = 5;
    }
    
    • 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

    在这里插入图片描述

    菱形虚拟继承

    class A
    {
    public:
    	virtual void func1() { cout << "A::func1" << endl; };
    public:
    	int _a;
    };
    class B : virtual public A
    {
    public:
    	virtual void func1() { cout << "B::func1" << endl; };
    	virtual void func2() { cout << "B::func2" << endl; };
    public:
    	int _b;
    };
    class C : virtual public A
    {
    public:
    	virtual void func1() { cout << "C::func1" << endl; };
    	virtual void func2() { cout << "C::func2" << endl; };
    public:
    	int _c;
    };
    class D : public B, public C
    {
    public:
    	// 此时D必须对func1进行重写
    	virtual void func1() { cout << "D::func1" << endl; };
    public:
    	int _d;
    };
    
    void Test11()
    {
    	D d;
    	d.B::_a = 1;
    	d.C::_a = 2;
    	d._b = 3;
    	d._c = 4;
    	d._d = 5;
    }
    
    • 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

    D必须对func1进行重写,因为B和C都有fun1,虚拟继承为了解决数据冗余和二义性,D的虚表里面只能存放一个,就无法确定存哪一个。
    在这里插入图片描述
    通过内存查看,菱形虚拟继承的关系可以如下表示。
    在这里插入图片描述

    继承与多态一些常见问题

    1. inline函数可以是虚函数吗?
      可以,当一个函数是虚函数,在多态调用中,inline就失效了。
    2. static函数可以是虚函数吗?
      不可以,static成员函数都是在编译时进行地址确定。虚函数是为了实现多态,需要运行时去虚表进行地址确定,static函数是virtual的话没有意义,因为本来就不会去虚表。
    3. 构造函数可以是虚函数吗?
      不可以,对象中的虚表指针都是构造函数初始化列表阶段才进行初始化的,所以构造函数是虚函数是没有意义的。
    4. 析构函数可以是虚函数吗?
      可以,并且建议基类的析构函数定义成虚函数。
    5. 拷贝构造函数可以是虚函数吗?
      不可以,拷贝构造函数也是构造函数。
    6. 赋值函数可以是虚函数吗?
      语法上可以,但是没有什么实际价值。
    7. 对象访问普通函数快还是虚函数快?
      虚函数不构成多态,是一样快;
      虚函数构成多态调用,普通函数更快。因为多态调用是运行时去虚函数表中找虚函数地址。
    8. 虚函数表是什么时候生成的?存在哪的?
      虚函数表是编译阶段就生成好的,存在于代码段(常量区)。所以一个类的不同对象共享该类的虚表。
      (构造函数初始化列表阶段初始化的是虚函数表指针,对象中存的也是虚函数表指针)
  • 相关阅读:
    Minio + Nginx 实现静态资源对外访问
    查看svn当前版本信息
    深入理解Java虚拟机:Java内存区域与内存溢出异常
    Java学习笔记3.9.1 Lambda表达式 - Lambda表达式入门
    一文讲解Linux Scheduler之rt选核流程
    网络安全运维工程师(NISP-SO)需要掌握那些知识点
    VBA技术资料MF84:判断文件夹是否存在并创建
    《大数据分析-数据仓库项目实战》--阅读笔记
    关于队里面最菜的在博客打卡第六十二天这件事
    数据仓库建模实践
  • 原文地址:https://blog.csdn.net/weixin_62172209/article/details/132575572