• 初识C++多态


    认识多态

    大家可能是第一次接触到多态,这里我用一个现实的例子来举例子.在买火车票的时候,我们会发现学生优惠价,军人有优先买票的资格.如果让你写一个购买车票的系统,那么你会怎么做?首先我们已经学过继承了,肯定第一步就是创建几个角色,每一个角色都写一个购买票的函数,反正是可以形成隐藏的,我们先把自己的想法给实现出来.

    #include 
    #include 
    #include 
    using namespace std;
    
    class Person
    {
    public:
    	Person(string& name)
    		:_name(name)
    	{
    
    	}
    	void buyTicket()
    	{
    		cout << _name << " 普通人 票价 100 ¥" << endl;
    	}
    protected:
    	string _name;
    };
    
    class Soldier : public Person
    {
    public:
    	Soldier(string& name)
    		:Person(name)
    	{
    
    	}
    	void buyTicket()
    	{
    		cout << _name << " 军人 优先购买 票价 100 ¥" << endl;
    	}
    };
    
    class Student:public Person
    {
    public:
    	Student(string& name)
    		:Person(name)
    	{
    
    	}
    	void buyTicket()
    	{
    		cout << _name << " 学生 半价 50 ¥" << endl;
    	}
    };
    
    int main()
    {
    	
    	while (true)
    	{
    		cout << "=====================" << endl;
    		cout << "1 学生 2 军人 3 普通人" << endl;
    		int ret = -1;
    		cout << "请选择 " ;
    		cin >> ret;
    		string name;
    		cout << "请输入你的名字: " << endl;
    		cin >> name;
    		switch (ret)
    		{
    		case 1:
    		{
    			Student s(name);
    			s.buyTicket();
    		}
    			break;
    		case 2:
    		{
    			Soldier s(name);
    			s.buyTicket();
    		}
    			break;
    
    		case 3:
    		{
    			Person s(name);
    			s.buyTicket();
    		}
    			break;
    		default:
    			cout << "输入错误,请重新输入" << endl;
    			break;
    		}
    	}
    
    	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
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91

    买票

    什么是多态

    我们已经完成的这个程序,那么你有没有感觉自己的代码存在些不妥.前面我们谈过,子类的对象是可以赋值给父类的,我们想想,反正父类和子类都存在买票的函数,我们是不是可以通过父类的买票函数来自动调用子类的呢?这是一个很好的思路.也是我们今天认识多态的开端.

    virtual 关键字

    我们已经见识过了virtual关键字了,是在虚继承那里,这里我们要谈它的其他的用法,修饰成员函数.被修饰的成员函数叫做虚函数.

    在谈这个之前,我们先来看看个情况.这个情况就是我们的多态,所谓的多态就是父类的引用或者指针调用虚函数.

    class A
    {
    public:
    	virtual void func()
    	{
    		cout <<"A:: void func()" << endl;
    	}
    };
    
    class B : public A 
    {
    public:
    	virtual void func()
    	{
    		cout << "B:: void func()" << endl;
    	}
    };
    
    int main()
    {
    	B b;
    	A& a = b;
    	a.func();
    	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

    image-20220723202157128

    重写

    前面我们已经谈过重载和隐藏了,今天我们需要在认识一个东西,重写,所谓的重写就是父子类中出现同名的函数,并且这个函数的返回值和参数也是一样的(有两个特殊,后面说.)

    虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的
    返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数

    class A
    {
    public:
    	virtual void func()
    	{
    		cout << "A:: func()" << endl;
    	}
    };
    
    class B : public A
    {
    public:
    	virtual void func()
    	{
    		cout << "B:: func()" << endl;
    	}
    
    	void func(int i)
    	{
    		cout << "B:: func(int i)" << endl;
    	}
    };
    
    int main()
    {
    	B b;
    	A& a = b;
    	a.func();
    	b.func(2);
    	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-20220725093833572

    它们三个的关系是这样的,我们花了一张图片.

    image-20220725094136886

    多态的条件

    是不是父类调用了子类的方法一定会构成多态?不是的,构成多态需要满足下面的两个条件.

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

    我们先来测试一下,先看看现象,至于原理,我们留在后面说,这个虚函数表有关.

    class A
    {
    public:
    	virtual void func()
    	{
    		cout << "A:: func()" << endl;
    	}
    };
    
    class B : public A
    {
    public:
    	virtual void func()
    	{
    		cout << "B:: func()" << endl;
    	}
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    必须是是指针或者是父类放热引用.

    
    
    int main()
    {
    	B b;
    	A& a = b;
    	A* p = &b;
    	p->func();
    	a.func();
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    image-20220725095038983

    如果只是简单的把子类对象给父类,是不会构成多态的.

    int main()
    {
    	B b;
    	A a = b;  // 简单的切片
    	a.func();
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    image-20220725095303189

    virtual 一定要带吗

    这里我先说明一下,父类的虚函数一定要带,否则构成不了多态的条件

    class A
    {
    public:
    	void func1()
    	{
    		cout << "A:: func1()" << endl;
    	}
    private:
    	int _a;
    
    };
    class B : public A
    {
    	virtual void func1()
    	{
    		cout << "B:: func1()" << endl;
    
    	}
    
    };
    
    int main()
    {
    	B b;
    	A& a = b;
    	a.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

    image-20220726095547006

    但是子类重写父类的虚函数可以不带,但是我们建议带上,感觉是C++的一个bug

    class A
    {
    public:
    	virtual void func1()
    	{
    		cout << "A:: func1()" << endl;
    	}
    private:
    	int _a;
    
    };
    class B : public A
    {
    	void func1()
    	{
    		cout << "B:: func1()" << endl;
    
    	}
    
    };
    
    int main()
    {
    	B b;
    	A& a = b;
    	a.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

    image-20220726095730554

    C++11 override 和 final

    这是C++11新增的两个关键字,作用还是可以的

    final 关键字

    final 主要的作用便是禁止重写,它有最终的的意思,也就是被他修饰的函数是不能被子类重写的.

    class A
    {
    public:
    	virtual void func() final
    	{
    		cout << "A:: func()" << endl;
    	}
    };
    
    class B : public A
    {
    public:
    	virtual void func()
    	{
    		cout << "B:: func()" << endl;
    	}
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    image-20220725095905153

    除此之外,final还可以用来修饰类,表明这个类不能够被继承.

    class A final
    {
    public:
    	virtual void func() 
    	{
    		cout << "A:: func()" << endl;
    	}
    };
    
    class B : public A
    {
    public:
    	virtual void func()
    	{
    		cout << "B:: func()" << endl;
    	}
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    image-20220725100124183

    override 关键字

    override更简单,是去检查和看看子类的这个函数再父类里面是否是虚函数.是就没事,不是就报错.

    class A 
    {
    public:
    	virtual void func() 
    	{
    		cout << "A:: func()" << endl;
    	}
    };
    
    class B : public A
    {
    public:
    	virtual void func() override
    	{
    		cout << "B:: func()" << endl;
    	}
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    image-20220725100452433

    如果不是的话,就会出现编译错误.

    class A 
    {
    public:
    	void func() 
    	{
    		cout << "A:: func()" << endl;
    	}
    };
    
    class B : public A
    {
    public:
    	virtual void func() override
    	{
    		cout << "B:: func()" << endl;
    	}
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    image-20220725100539953

    重写的例外

    前面我说了,重写的函数必须函数名,返回值,和参数必须完全一样,但是这里面存在两个例外.

    协变

    大家先看现象,这里面返回值就不一样.

    class A
    {
    };
    class B : public A
    {
    };
    
    class C
    {
    public:
    	virtual	A* func()
    	{
    		cout << "A:: func()" << endl;
    	}
    };
    
    class D : public C
    {
    public:
    	virtual B* func() 
    	{
    		cout << "B:: func()" << endl;
    	}
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    image-20220725102002051

    派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变 注意 协变 不重要 ,而且用处很少.

    析构函数

    这里我直接给出结论,父子类的析构函数也是可以构成重写的,析构函数会被编译器童统一的更名为destructor(),所以可以构成重载.至于的作用,那可就大了去了.

    class Person {
    public:
    	~Person()
    	{
    		cout << "~Person()" << endl;
    	}
    };
    
    class Student : public Person {
    public:
    	virtual ~Student()
    	{
    		cout << "~Student()" << endl;
    		delete[] _name;
    	}
    
    private:
    	char* _name = new char[10]{ 'j', 'a', 'c', 'k' };
    };
    
    int main()
    {
    
    	Person* ptr = new Person;
    	delete ptr; // ptr->destructor() + operator delete(ptr)
    
    	ptr = new Student;
    	delete ptr;  // ptr->destructor() + operator delete(ptr)
    
    	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-20220725135328396

    你会发现出问题了,内存泄露,严格的编译器还会自动的检查出来,原因就是现在两个析构函数构成隐藏,我们又用父类来调用,只能调用到父类的析构函数.如果父类的析构函数是虚函数的话,这样就避免这个问题了.

    class Person {
    public:
    	virtual ~Person()
    	{
    		cout << "~Person()" << endl;
    	}
    };
    
    class Student : public Person {
    public:
    	virtual ~Student()
    	{
    		cout << "~Student()" << endl;
    		delete[] _name;
    	}
    
    private:
    	char* _name = new char[10]{ 'j', 'a', 'c', 'k' };
    };
    
    int main()
    {
    
    	Person* ptr = new Person;
    	delete ptr; // ptr->destructor() + operator delete(ptr)
    
    	ptr = new Student;
    	delete ptr;  // ptr->destructor() + operator delete(ptr)
    
    	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-20220725135601201

    这样的话,构成多态,编译器会自动去子类的析构函数,子类的析构函数同时又会去调用父类的析构函数,这也是 析构函数的函数名会被统一处理的原因. 这里我们建议吧父类的析构函数写成虚函数.

    抽象类

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

    下面这个类就是抽象类,里面存在一个纯虚函数.

    class Person
    {
    public:
    	virtual	void  func() = 0;
    private:
    
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    特性

    抽象类是不能够被继承的,它生来就是被继承的的,而且继承的类必须重写所有的纯虚函数.

    不能被实例化

    int main()
    {
    	Person per;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    image-20220725142702591

    继承的类必须重写纯虚函数

    class Person
    {
    public:
    	virtual	void  func() = 0;
    private:
    
    };
    class Student : public Person
    {
    public:
    	void  func()
    	{
    
    	}
    
    private:
    
    };
    
    int main()
    {
    	Student stu;
    	Person& per = stu; // 允许
    	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

    image-20220725143002723

    接口继承 和 实现继承

    我们先来说一下实现继承

    普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现,简单的理解就是 子类可以调用父类的函数,这是 实现继承,所谓的是实现继承就是 函数名和函数体都被继承了下来.

    class A
    {
    public:
    	void func()
    	{
    		cout << "A:: func()" << endl;
    	}
    };
    
    class B : public A
    {
    
    };
    
    int main()
    {
    	B b;
    	b.func();
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    image-20220725202051302

    虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数.说人话接口继承就是 只继承了函数的声明,没有继承实现

    class A
    {
    public:
        virtual	void func(int a = 10)
    	{
    		cout << "A:: func()" << endl;
    		cout << "a = " << a << endl;
    	}
    };
    
    class B : public A
    {
    	void func(int a = 20)
    	{
    		cout << "B:: func()" << endl;
    		cout << "a = " << a << endl;
    	}
    };
    
    int main()
    {
    	B b;
    	A& a = b;
    	a.func();
    	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-20220725205518342

    大家这里可以看到了吧,接口继承只是继承的函数的名(包含缺省值),所以这一点一定要牢记.

    多态原理

    一般情况下,我们学到上面就可以了,但是我们需要往深处谈点,这样面试官问到的时候就可以提高面试官的印象了.

    虚函数表指针

    在谈这个之前我们需要看一个现象.下面的结果是哈?

    #include 
    using namespace std;
    class A
    {
    public:
    	virtual void func()
    	{
    
    	}
    private:
    	int _a;
    
    };
    
    int main()
    {
    	cout << sizeof(A) << endl;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    image-20220726092354513

    这里我们就疑惑了,里面不就一个整型的成员变量吗?为何它的大小是8,这是由于有一个虚函数,那么就存在一个指针.

    image-20220726092727806

    虚函数表

    这个指针就是指向一个表,这个表就是虚函数表,简称虚表.表里面的存放的是各个虚函数的地址,记住,每一个虚函数都会放到虚函数表中.

    class A
    {
    public:
    	virtual void func1()
    	{
    
    	}
    	virtual void func2()
    	{
    
    	}
    private:
    	int _a;
    
    };
    int main()
    {
    	A a;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    image-20220726093222504

    多态原理

    看到了虚函数表,我们就可以看一下多态的原理,这里面就涉及到虚表.

    大家仔细的看一下子类的虚函数表,这里你就会发现变了,你重写的虚函数表里面村的指针地址变化,这就是多态的原理.我用简单的话来说,子类继承父类的时候把虚函数表也给继承下来了,然后编译查看子类是否存在重写父类里面的虚函数,重写了,就把虚函数里对应的函数指针给改了,这就是多态.

    class A
    {
    public:
    	virtual void func1()
    	{
    
    	}
    	virtual void func2()
    	{
    
    	}
    private:
    	int _a;
    
    };
    class B : public A
    {
    	void func1()
    	{
    
    	}
    };
    
    int main()
    {
    
    	B 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
    • 29

    image-20220726094205026

    虚函数都放在虚函数表中吗

    我们现在有一个疑惑,是不是每一个虚函数都会放在虚函数表中?这里先来看一下监视窗口.

    class A
    {
    public:
    	virtual void func1()
    	{
    
    	}
    	virtual void func2()
    	{
    
    	}
    private:
    	int _a;
    
    };
    class B : public A
    {
    	virtual void func1()
    	{
    
    	}
    	virtual void func2()
    	{
    
    	}
    	virtual void func3()
    	{
    
    	}
    };
    
    int main()
    {
    
    	B 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
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    image-20220726101207647

    这里出问题了,我们子类里面是存在这三个三个虚函数的,为何监视窗口里面只有两个,是不是我们的结论错了.不是的,这是由于编译器的监视窗口是优化过的,这里我们要看的是实际内存.

    class A
    {
    public:
    	virtual void func1()
    	{
    
    	}
    	virtual void func2()
    	{
    
    	}
    private:
    	int _a;
    
    };
    class B : public A
    {
    	virtual void func1()
    	{
    
    	}
    	virtual void func2()
    	{
    
    	}
    	virtual void func3()
    	{
    
    	}
    };
    
    int main()
    {
    
    	B 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
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    image-20220726101835484

    现在我们已经确定了虚函数表里面存在三个指针,而且这三个指针很相似,最关键的是,他们中有两个已经确定了,我们有理由怀疑这第三个也是一个是一个虚函数指针.我们现在再来验证一下,这里的指针会发生变化,毕竟我又重新编译了一下,不过不要担心,大家还是可以看懂的.

    注意这是给大家验证的,里面的内容看懂更好,不懂也行,不过这里面用的都是我们学过的,函数指针和强制类型类型转化…

    class A
    {
    public:
    	virtual void func1()
    	{
    
    	}
    	virtual void func2()
    	{
    
    	}
    
    private:
    	int _a;
    
    };
    
    // 一个 函数指针
    typedef void (*ViPointer)();
    
    class B : public A
    {
    	virtual void func1()
    	{
    		cout << "func1()" << endl;
    	}
    	virtual void func2()
    	{
    		cout << "func2()" << endl;
    	}
    	virtual void func3()
    	{
    		cout << "func3()" << endl;
    	}
    };
    
    void printViPointer(ViPointer* f)
    {
    	int i = 0;
    	while (f[i] != nullptr)
    	{
    		printf("[%d] : %p\n", i, f[i]);
    		f[i]();
    		i++;
    	}
    }
    
    int main()
    {
    
    	B b;
    	printViPointer((ViPointer*)(*(int*)(&b))); 
    	
    	// &b                        VS 编译器 虚表指针在头部取地址 
    	// (int*)(&b))               强制类型转化 int* 因为是 32 ,所以指针 4个字节 
    	// *(int*)(&b)                解引用  得到 第一个 存放 虚函数的地址 的 地址
    	// (ViPointer*)(*(int*)(&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
    • 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
    • 58
    • 59

    image-20220726110802585

    这里就证明了每一个虚函数都会把地址放在虚表中,同时也证明了VS的监视的出窗口是被优化过的

    虚表存在哪里

    在看这个之前,大家可以去看看之前的C程序虚拟地址空间,认识一下简单的内存分布(虚拟的),我们简单的说一下.

    image-20220726141716567

    在作测验之前,我们先来推理一下,首先不可能在栈区,要知道栈区主要是函数栈帧的,开辟和释放很频繁,虚表面指向的函数地址一般是不会变的,所以虚表一会销毁就不太好.

    同一个类型的对象共用一个虚表.

    class A
    {
    public:
    	virtual void func()
    	{
    
    	}
    };
    int main()
    {
    	A a1;
    	A a2;
    	printf("虚表指针  %p\n", (*(int*)&a1));
    	printf("虚表指针  %p\n", (*(int*)&a2));
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    image-20220726142537927

    从这里我们看出,也不可能在堆上,什么时候在堆上开辟,有什么时候销毁,你知道一个类型会实例化几个对象吗?销毁的时候是不是还要引用计数,这些都是问题.

    剩下的两个我排除不了的,具体看我们打印的结果.

    #include 
    #include 
    class A
    {
    public :
    	virtual void func()
    	{
    
    	}
    };
    
    int g_val1 = 1; // 已初始化
    int g_val2;     // 未初始化
    int main()
    {
    	// 栈区
    	int a = 10;
    
    	// 堆区
    	int* p1 = new int;
    
    	// 字符常量区
    	const char* ptr = "abdve";
    
    	printf("代码区              %p\n", main);
    	printf("字符常量区     %p\n", ptr);
    	printf("已初始化全局变量区    %p\n", &g_val1);
    	printf("未初始化全局变量区    %p\n", &g_val2);
    	printf("堆区                %p\n", p1);
    	printf("栈区                %p\n", &a);
    
    	// 打印 一下 虚函数表指针
    	A aa;
    	printf("虚表指针  %p\n", (*(int*)&aa));
    	delete p1;
    	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

    image-20220726141208231

    从这里可以看,虚表大概实在常量区,不同的编译器可能不一样,不过大差不差.

    切片会拷贝虚表吗

    还有一个问题,如果我们把子类对象切片给父类,里面的虚表会被拷贝下来吗?不会的,你想,如果他拷贝下来了,那么父类对象调用虚函数的是时候,不会发生混淆吗.这也是只有父类的引用或者指针才能构成多态的原因.

    class A
    {
    public:
    	virtual void func1()
    	{
    
    	}
    };
    
    class B : public A
    {
    	virtual void func1()
    	{
    
    	}
    	virtual void func2()
    	{
    
    	}
    };
    
    int main()
    {
    	B b;
    	A a = 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-20220726143725714

    编译时决议 & 运行时决议

    所谓的编译时决议就是在编译的时候就已经确定了调用哪个函数了,它和对象无关,只和类型有关.运行时决议就是多态的原理,它先去虚表里面找有没有和调用函数相匹配的,找到了就用,找不到就去其他函数找,没有找到的话就报错,也就是说编译器开始的时候也不知到自己究竟调用的的是哪个函数.

    class Person {
    public:
    	virtual void BuyTicket() { cout << "买票-全价" << endl; }
    
    	void Buy() { cout << "Person::Buy()" << endl; }
    };
    
    class Student : public Person {
    public:
    	virtual void BuyTicket() { cout << "买票-半价" << endl; }
    
    	void Buy() { cout << "Student::Buy()" << endl; }
    };
    
    void Func1(Person* p)
    {
    	// 跟对象有关,指向谁调用谁 -- 运行时确定函数地址
    	p->BuyTicket();
    	// 跟类型有关,p类型是谁,调用就是谁的虚函数  -- 编译时确定函数地址
    	p->Buy();
    }
    
    int main()
    {
    	Person p;
    	Student s;
    
    	Func1(&p);
    	Func1(&s);
    
    	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

    image-20220726150533930

    编译时决议

    这里我们看一下反汇编就可以了,这就是普通函数的调用原理.

    image-20220726151159011

    运行时决议

    这里编译器回去虚表里面找,然后一步一步来.

    image-20220726151317048


    多继承的虚表

    到最难的那部分了,这里面有太多问题可以和大家分享了,这里先不说菱形继承,只说简单的多继承.现在我们要看一个代码,这里需要解释一下.

    class Base1 {
    public:
    	virtual void func1() { cout << "Base1::func1" << endl; }
    	virtual void func2() { cout << "Base1::func2" << endl; }
    private:
    	int b1;
    };
    
    class Base2 {
    public:
    	virtual void func1() { cout << "Base2::func1" << endl; }
    	virtual void func2() { cout << "Base2::func2" << endl; }
    private:
    	int b2;
    };
    
    class Derive : public Base1, public Base2 {
    public:
    	virtual void func1() { cout << "Derive::func1" << endl; }
    	virtual void func3() { cout << "Derive::func3" << endl; }
    private:
    	int d1;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    我们知道了Derive继承了两个类,而且这两个类都有虚表,也就是说Derive会有两个虚表,其中重写的虚函数就会被修改,这里我先画一个图.

    image-20220726155508684

    出现问题了,现在我们需要看看func3虚函数放在了哪个虚表中,还是这这两个虚表都放?

    int main()
    {
    	Derive d;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    image-20220726160854821

    还是这样的,监视窗口确实在这里欺骗了我们,还还是需要打印函数的地址.

    typedef void(*VFPTR) ();
    void PrintVTable(VFPTR vTable[])
    {
    	// 依次取虚表中的虚函数指针打印并调用。调用就可以看出存的是哪个函数
    	cout << " 虚表地址>" << vTable << endl;
    	for (int i = 0; vTable[i] != nullptr; ++i)
    	{
    		printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
    		VFPTR f = vTable[i];
    		f();
    	}
    	cout << endl;
    }
    
    int main()
    {
    	Derive d;
    
    	PrintVTable((VFPTR*)(*(int*)&d));
    	PrintVTable((VFPTR*)(*((int*)((char*)&d + sizeof(Base1)))));
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    image-20220726160532275

    现在我们就可以确定了,它在Base1,准确的来说,它是在第一个被继承的类里面.

    image-20220726161229285

    为何func1的地址不一样

    我们解决了一个问题,现在又出现了一个问题,为何func1的的地址不一样?要知道它,它们可是同一个函数啊.

    image-20220726161552987

    在这个问题之前,我们需要看看是不是所有的编译器这个func1的地址都不一样,我们用g++试试,很抱歉,我这里好象也不一样,不过没关系,我们来分析一下就明白了.

    image-20220726162759443

    原因

    为何不一样,因为他们不是真正函数的地址,或者至少有一个不是,是经过再一次封装过的,这里需要看看真实的地址.

    int main()
    {
    	Derive d;
    
    	PrintVTable((VFPTR*)(*(int*)&d));
    	PrintVTable((VFPTR*)(*((int*)((char*)&d + sizeof(Base1)))));
    	printf(" 真实的地址 0X%x", &Derive::func1);
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    image-20220726164204798

    好了现在你就会发现现在每一个虚表里面的地址和真实的地址都不一样,这里就可以得出结论了,编译器为了安全,在真实的函数地址做了一层封装,调用函数的时候需要经过一系列的转化才可以真正的调用.

    那么现在就可以看一下汇编语言了,看一看编译器是如何去调用到真实的函数的.

    Base1虚表中 寻找func1 汇编

    我们先来看Base1 的虚表里面是如何调用func1这个函数的.

    第一步 找到 ebp - 20(里面的 是 16 进制)所在的指针,里面的内容就是Base1虚表中存储 func1的地址

    image-20220726172016777

    第一步 call 这个地址,发现func1函数的地址,你会发现这个和我们打印出的真实地址还不一样,或许我们所谓的"真实"也是被封装过的

    image-20220726172608702

    第三步 跳转到这个地址,调用func1函数,看来这个jmp的地址才最有可能是是真正的函数地址.

    image-20220726173030300

    Base2虚表中 寻找func1 汇编

    这个就麻烦了,我们还是一步一步来,最后再来一个总结.

    第一步 找到 ebp - 20(里面的 是 16 进制)所在的指针,里面的内容就是Base2虚表中存储 func1的地址

    image-20220726173713800

    call 这个地址,找到一个另外的func1地址,

    image-20220726174027244

    调到这个地址后,ecx里面的值减8,把结果放在ecx中,原本的ecx值是啥先等下说,下一步 跳转到ecx所指向的地方.

    image-20220726174321481

    找到了func1的真实地址,和Base1的一摸一样,下面就是jmp和调用了

    image-20220726174505991

    总结

    现在我们可以下一个总结了.Base2寻找func1多了一段过程,但是最终还是和Base1寻找的一样,那么这就有点意思了.

    image-20220726180134731

    多的过程中最主要的就只有一点,ecx = ecx - 8,现在我们就可以看看ecx中存在什么东西了.我直接给答案把,ecx存放的是Derive里面的 Base2地址,ecx-8就是Base1的地址,这就来到了原点,和Base1寻找func1是一样的.

    image-20220726181200464

    菱形继承、菱形虚拟继承

    实际中我们不建议设计出菱形继承及菱形虚拟继承,一方面太复杂容易出问题,另一方面这样的模型,访问基类成员有一定得性能损耗。所以菱形继承、菱形虚拟继承我们的虚表我们就不看了,一般我们也不需要研究清楚,因为实际中很少用.


    虚继承和多态

    这里我们要谈一下虚继承和多态,我们连在一起说一下.注意这里只是了解,不要求掌握.

    D必须重写A的虚函数

    D多继承后必须重写A里面的虚函数,这是由于如果B和C虚继承了A,那么A只有一块空间,对于A这块空间的虚表不知道放B和C里面哪个重写A中的虚函数的地址,我们直接把D重写了,直接放D的就可以了.

    class A
    {
    public:
    	virtual void func()
    	{}
    public:
    	int _a;
    };
    
    class B : virtual public A
    {
    public:
    	virtual void func()
    	{}
    public:
    	int _b;
    };
    
    class C : virtual public A
    {
    public:
    	virtual void func()
    	{}
    public:
    	int _c;
    };
    
    class D : public B, public C
    {
    
    public:
    	int _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

    image-20220726203723724

    注意,这里就可以看到公共区A那里存在一个虚表指指针了.

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

    image-20220726205225931

    虚基表指针指向的位置为0

    我们先来看看一个现象,先看看虚基表指针指向的内容.你会里面第一个还是0,这里是我们前面没有解释的.

    class A
    {
    public:
    	virtual void func()
    	{}
    public:
    	int _a;
    };
    
    class B : virtual public A
    {
    public:
    	virtual void func()
    	{}
    
    public:
    	int _b;
    };
    
    class C : virtual public A
    {
    public:
    	virtual void func()
    	{}
    public:
    	int _c;
    };
    
    class D : public B, public C
    {
    public:
    	virtual void func()
    	{}
    public:
    	int _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

    image-20220726205549659

    这个我们们来解决一下菱形虚拟继承那里为何虚基表指针指向的第一个位置为0,我们在B那里添加一个虚函数.一起看看它的内存分布.

    class B : virtual public A
    {
    public:
    	virtual void func()
    	{}
    
    	virtual void func1()
    	{}
    public:
    	int _b;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    image-20220726210403934

    这里我们会发现B比C多了一个指针,很容易就可以判断出这是B里面的虚表指针,先来看第二个虚基表,里面都一个也是一个偏移量,是-4,这里从当前位置向上找4个字节,找到的就是虚表指针,我们在来看这个虚表.

    image-20220726210812866

    我们还发现,这里面好象只有一个虚函数指针,可是我们B是继承了A的,是不是虚继承的话这这里就不把它放在单独区域了,只放在公共区域,我们来验证一下.我们的想法是对的.

    typedef void(*VFPTR) ();
    void PrintVTable(VFPTR vTable[])
    {
    	cout << " 虚表地址>" << vTable << endl;
    	for (int i = 0; vTable[i] != nullptr; ++i)
    	{
    		printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
    		VFPTR f = vTable[i];
    		f();
    	}
    	cout << endl;
    }
    
    int main()
    {
    	D d;
    	PrintVTable((VFPTR*)(*(int*)&d));
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    image-20220726211503962

    问题

    现在我们存在一些问题,这里需要我们完成解析.

    虚函数可以是内联函数吗

    这里先给一个结论,要是普通调用,内联函数函数还是展开的要是多态调用怎不再是内联函数了,他的地址要放在虚表中,内联函数是没有地址的.

    class A
    {
    public:
    	A()
    	{
    		_a = 1;
    	}
    
    	virtual inline void f1()
    	{
    		cout << "A::f1()" << endl;
    	}
    
    	virtual void f2();
    
    private:
    	int _a;
    };
    
    class B : public A
    {
    public:
    	virtual void f1()
    	{
    		cout << "B::f1()" << endl;
    	}
    
    	virtual void f2();
    };
    
    void A::f2()
    {
    	cout << "A::f2()" << endl;
    }
    
    void B::f2()
    {
    	cout << "B::f2()" << endl;
    }
    
    // 多态 调用
    void Func1(A* ptr)
    {
    	ptr->f1();
    	ptr->f2();
    }
    
    // 普通调用
    void Func2(A ptr)
    {
    	ptr.f1();
    	ptr.f2();
    }
    
    int main()
    {
    	A aa;
    	B bb;
    	Func1(&aa);
    	Func1(&bb);
    
    	Func2(aa);
    	Func2(bb);
    
    	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
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66

    image-20220726185032626

    静态成员函数可以似乎虚函数吗

    抱歉,静态成员函数不能是虚函数,要知道静态成员函数是无法完成重写的,要知道静态函数是没有this指针的,这就是虚函数表无法存储的原因.

    class A
    {
    public:
       static virtual	void func()
    	{}
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    image-20220726200636148

    构造函数可以是虚函数吗

    不能,我们先来说下虚表实在何时初始化的,实在构造函数初始化列表的那里,这就构成的先有鸡还是先有蛋的问题.

    class A
    {
    public:
    	A()
    	{
    
    	}
       virtual	void func()
       {}
    };
    int main()
    {
    	A a;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    image-20220726201218494

  • 相关阅读:
    PTA L1-003 个位数统计(用java写的)
    neo4j网页无法打开,启动一会儿后自动关闭,查看neo4j status显示Neo4j is not running.
    鸿蒙OS应用开发初体验
    d的dub构建配置
    MATLAB——一维离散小波的单层分解
    GBase 8c V3.0.0数据类型——网络地址函数和操作符(cidr和inet操作符)
    Java基于SpringBoot的校园交友网站的设计与实现
    uni-app:实现picker下拉列表的默认值设置
    华为开源自研AI框架昇思MindSpore应用案例:消噪的Diffusion扩散模型
    tokenizers pre_tokenizers模块
  • 原文地址:https://blog.csdn.net/m0_61334618/article/details/126022714