• 详解C++三大特性之多态


    1.多态概念

    概念

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

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

    2.多态的定义及实现

    虚函数

    即被virtual修饰的类成员函数称为虚函数。

    虚函数重写:

    派生类中有一个跟基类完全相同的虚函数,包括返回值类型、函数名、参数列表都相同,称子类虚函数重写了父类虚函数。
    注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用。

    构成多态2个要求

    多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了Person。Person对象买票全价,Student对象买票半价。

    1、子类虚函数重写父类虚函数 (重写要求虚函数 + 函数名/参数/返回值都相同)
    只要有一个不同,就会直接去调用父类的。

    2、必须是父类指针、引用去调用虚函数

    class Person
    {
    protected:
    	string _name;
    	//int _id;
    public:
    	Person(const char* name)
    		:_name(name)
    	{}
    
    	// 虚函数
    	virtual void BuyTicket() { cout << _name << " Person: 买票-全价 100元" << endl; };
    };
    
    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; }
    };
    
    // 多态2个要求
    // 1、子类虚函数重写父类虚函数 (重写要求虚函数 + 函数名/参数/返回值都相同)
    // 2、必须是父类指针、引用去调用虚函数
    
    void Pay(Person* ptr)
    {
    	ptr->BuyTicket();
    }
    
    int main()
    {
    	int option = 0;
    	cout << "================================================" << endl;
    	do
    	{
    		cout << "请选择身份:";
    		cout << "1、普通人 2、学生 3、军人" << endl;
    		cin >> option;
    		cout << "请输入名字:" << endl;
    		string name;
    		cin >> name;
    		switch (option)
    		{
    		case 1:
    			// 需要转化为const char* 类型字符串
    			// 不考虑内存泄露,直接new
    			Pay(new Person(name.c_str()));
    			break;
    		case 2:
    			Pay(new Student(name.c_str()));
    			break;
    		case 3:
    			Pay(new Soldier(name.c_str()));
    			break;
    		default:
    			cout << "输入错误,请重新输入" << endl;			break;
    		}
    		cout << "================================================" << endl;
    	} while (option != -1);
    }
    
    • 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

    在这里插入图片描述

    换成引用试试。

    void Pay(Person& ptr)
    {
    	ptr.BuyTicket();
    }
    
    int main()
    {
    	int option = 0;
    	cout << "================================================" << endl;
    	do
    	{
    		cout << "请选择身份:";
    		cout << "1、普通人 2、学生 3、军人" << endl;
    		cin >> option;
    		cout << "请输入名字:" << endl;
    		string name;
    		cin >> name;
    		switch (option)
    		{
    		case 1:
    		{
    			// case里面不能直接定义对象,需要加域{}
    			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);
    }
    
    • 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

    传的是对象就不构成多态了。

    // 如果传的是对象,不构成多态了。
    void Pay(Person ptr)
    {
    	ptr.BuyTicket();
        // 调用的都是父类的那个
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    在这里插入图片描述

    虚函数重写2个例外

    1. 协变
      基类与派生类虚函数返回值类型不同。但要求他们必须是父子关系,可以是指针或者引用。
    class A{};
    class B : public A {};
    
    // 虚函数重写对返回值要求有一个例外:协变,要求返回值是父子关系,指针或者引用都行
    class Person {
    public:
    	virtual A* f() { 
    		cout << "virtual A* Person::f()" << endl;
    		return nullptr;
    	}
    };
    
    class Student : public Person {
    public:
    	virtual B* f() {
    		cout << "virtual B* Student::f()" << endl;
    		return nullptr;
    	}
    };
    
    int main()
    {
    	Person p;
    	Student s;
    	Person* ptr = &p;
    	ptr->f();
    
    	ptr = &s;
    	ptr->f();
    
    	return 0;
    }
    
    // virtual A* Person::f()
    // virtual B* Student::f()
    
    • 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

    在这里插入图片描述

    // 引用的情况。
    class A{};
    //class B{};
    class B : public A {};
    
    // 虚函数重写对返回值要求有一个例外:协变,要求返回值是父子关系,指针或者引用都行
    class Person {
    public:
    	virtual A& f() { 
    		cout << "virtual A& Person::f()" << endl;
    		return (A&)A();
    	}
    };
    
    class Student : public Person {
    public:
    	virtual B& f() {
    		cout << "virtual B& Student::f()" << endl;
    		return (B&)B();
    	}
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    1. 析构函数重写

      如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor

    class Person {
    public:
    	virtual ~Person() { cout << "~Person()" << endl; }
    };
    class Student : public Person {
    public:
    	// Person析构函数加了virtual,关系就变了
    	// 由原来的重定义(隐藏) -->  重写(覆盖)关系
    	// 不过这种变化对于普通对象没有影响
    	 ~Student() { cout << "~Student()" << endl; }
    };
    
    int main()
    {
    	Person p;
    	Student st;
    	// 派生类的析构函数调用完成后会自动调用基类的析构函数
    	return 0;
    }
    
    //~Student()
    //~Person()
    //~Person()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    class Person {
    public:
    	~Person() { cout << "~Person()" << endl; }
    };
    class Student : public Person {
    public:
    	 ~Student() { 
    		 cout << "~Student()" << endl;
    		 delete[] _name;
    		 // 如果以char*打印,打印的是字符串,直到\0才截止
    		 // 强转成void*后,打印的是地址
    		 cout << "delete: " << (void*)_name << endl;
    	 }
    
    private:
    	char* _name = new char[10]{ 'j', 'a', 'c', 'k' };
    };
    
    int main()
    {
    	//父类指针,可以指向父类对象,也可以指向子类对象
    	Person* ptr = new Person;
    	delete ptr; // 调用的是ptr->destructor() + operator delete(ptr)
    	// 不构成多态的话,这只是一个普通调用,类型是Person,就会去调用Person的析构函数
    
    	ptr = new Student;
    	delete ptr;
    	// 这里期望调用子类的析构函数,但还是调用了父类,子类有资源没被释放,内存泄露了
    	return 0;
    }
    // ~Person()
    // ~Person()
    // 这里只是普通调用而不是多态调用
    
    • 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

    因此我们希望delete ptr 调用析构函数是一个多态调用
    而多态调用需要的是子类虚函数的重写,且让父类指针或引用去调用。

    class Person {
    public:
    	virtual ~Person() { cout << "~Person()" << endl; }
    };
    class Student : public Person {
    public:
    	~Student() {
    		cout << "~Student()" << endl;
    		delete[] _name;
    		cout << "delete: " << (void*)_name << endl;
    	}
    
    private:
    	char* _name = new char[10]{ 'j', 'a', 'c', 'k' };
    };
    
    int main()
    {
    	//父类指针,可以指向父类对象,也可以指向子类对象
    	Person* ptr = new Person;
    	delete ptr; // 调用的是ptr->destructor() + operator delete(ptr)
    	// 不构成多态的话,这只是一个普通调用,类型是Person,就会去调用Person的析构函数
    
    	ptr = new Student;
    	delete ptr;
    	return 0;
    }
    // 父类析构函数加上virtual 变成多态调用。
    // ~Person()
    // ~Student()
    //	delete: 000001D126A93CD0
    // ~Person()
    
    • 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

    不过子类析构函数最好还是加上virtual。

    举个例子

    class A
    {
    public:
        virtual void func(int val = 1){ std::cout<<"A->"<< val <<std::endl;}
        virtual void test(){ func();}
    };
    
    class B : public A
    {
    public:
        void func(int val=0){ std::cout<<"B->"<< val <<std::endl; }
    };
    
    int main(int argc ,char* argv[])
    {
        B*p = new B;
        p->test();
        return 0;
    }
    // 输出结果?
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    在这里插入图片描述

    注意,一个是继承调用,一个是多态调用。

    怎么说呢,这样设计其实不太好。还不如就设计成子类不写virtual就报错,父类子类参数不一致也报错。

    如果p直接调用func呢

    int main(int argc ,char* argv[])
    {
        B*p = new B;
        p->func(); // B->0
        // 这里只是普通调用,不符合多态的条件,p不是父类的指针或引用。
        
        A* p2 = new B;
        p2->func(); // 这才是多态调用 B->1
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    C++11 override 和 final

    C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失。
    因此:C++11提供了override和final两个关键字,可以帮助用户检测是否重写。

    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

    在这里插入图片描述

    此外,借助final,还能写出不能被继承的类。

    // C++11 final修饰父类,表示该类不能被继承
    class Car final
    {
    public:
    private:
    };
    class Benz :public Car
    {
    public:
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    在这里插入图片描述

    // C++98中,只能将父类构造函数私有化来达到间接不能继承的效果
    class Car
    {
    public:
    private:
    	Car()
    	{}
    };
    class Benz :public Car
    {
    public:
    };
    
    int main()
    {
    	Benz bz; //err
    	return 0; 
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    在这里插入图片描述

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

    class Car {
    public:
    	virtual void Drive() {}
    };
    // override 写在子类中,要求严格检查是否完成重写,没完成会报错。
    class Benz :public Car {
    public:
    	virtual void Drive() override { cout << "Benz-舒适" << endl; }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    在这里插入图片描述

    仅仅是参数不同也能检查出来没完成重写。

    在这里插入图片描述

    重载、重写、重定义

    1. 重载
      • 2个函数在一个作用域
      • 函数名相同、参数不同(包括数量、类型、顺序)
    2. 重写(覆盖)
      • 2个函数分别在基类和派生类的作用域
      • 函数名、参数、返回值都必须相同(斜变例外)
      • 2个函数必须是虚函数
    3. 重定义(隐藏)
      • 2个函数分别在基类和派生类的作用域
      • 函数名相同
      • 2个基类和派生类的同名函数不构成重写就是重定义
      • 同名的成员变量也是隐藏关系

    3.抽象类

    概念

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

    // 抽象类 -- 在现实中一般没有具体对应的实体
    // 不能实例化出对象
    class Car
    {
    public:
        // 纯虚函数
    	virtual void Drive() = 0;
    };
    
    int main()
    {
    	Car c; //err 无法实例化抽象类
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    class Car
    {
    public:
    	
    	virtual void Drive() = 0;
    };
    
    class BMw :public Car
    {
    
    };
    int main()
    {
    	BMw bw; // err 也无法实例化出对象
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    class Car
    {
    public:
    	
    	virtual void Drive() = 0;
    };
    
    class BMw :public Car
    {
    	virtual void Drive()
    	{
    		cout << "BMW-操控" << endl;
    	}
    };
    int main()
    {
    	BMw bw; // 子类重写之后就能实例化出对象了。
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    定义指针

    抽象类基类虽然无法实例化对象,但是可以定义出指针。

    class Car
    {
    public:
    	
    	virtual void Drive() = 0;
    };
    
    class BMw :public Car
    {
    	virtual void Drive()
    	{
    		cout << "BMW-操控" << endl;
    	}
    };
    int main()
    {
    	Car* pBMw = new BMw;
    	pBMw->Drive();
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    注意:纯虚函数的实现没有意义,也就是纯虚函数可以有函数体,只不过没有意义。
    因为没有人可以调用他,只是为了把这个接口继承下去而已。

    接口继承和实现继承

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

    4.多态原理

    虚函数表

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

    在这里插入图片描述

    除了_b成员,还有一个__vfptr放在对象的前面,对象中的这个指针我们叫做虚函数表指针(v代 表virtual,f代function)。一个含有虚函数的类中都至少有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。

    注意:这个指针指向的是虚函数表,表中存有指向虚函数的指针。

    class Base
    {
    public:
    	virtual void Func1()
    	{
    		cout << "Func1()" << endl;
    	}
    	void Func2()
    	{
    		cout << "Func2()" << endl;
    	}
    	virtual void Func3()
    	{
    		cout << "Func3()" << endl;
    	}
    private:
    	int _b = 1;
    	char _ch;
    };
    
    int main()
    {
    	Base b;
    	cout << sizeof(Base) << endl; //12
    	// b中有一个_b _ch __vfptr
    	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
    class Base
    {
    public:
    	virtual void Func1()
    	{
    		cout << "Func1()" << endl;
    	}
    	void Func2()
    	{
    		cout << "Func2()" << endl;
    	}
    	virtual void Func3()
    	{
    		cout << "Func3()" << endl;
    	}
    private:
    	int _b = 1;
    };
    
    class Derive : public Base
    {
    public:
    	virtual void Func1()
    	{
    		cout << "Derive : virtual void Func1()" << endl;
    	}
    private:
    	int _d = 2;
    };
    
    int main()
    {
    	Base b;
    	cout << sizeof(Base) << endl; // 8
    	Derive d;
    	cout << sizeof(Derive) << endl; // 12
    
    	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

    在这里插入图片描述

    派生类对象d由2部分构成,一部分是父类继承下来的成员,虚表指针也就是存在这一部分,另一部分就是派生类自己的成员。

    总结一下派生类的虚表生成:
    a.先将基类中的虚表内容拷贝一份到派生类虚表中
    b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数
    c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后

    多态原理

    int main()
    {
    	Base b;
    	cout << sizeof(Base) << endl; // 8
    	Derive d;
    	cout << sizeof(Derive) << endl; // 12
    
        // prt指向父类对象时,ptr->Func1() 在b的虚表中找到的虚函数是 Base::Func1()
    	Base* ptr = &b;
    	ptr->Func1();
    	ptr->Func2();
        // 当ptr指向子类对象时,ptr->Func1() 在d的虚表中找到的是虚函数是 Derive::Func1()
    	ptr = &d;
    	ptr->Func1();
    
    	Base* ptr2 = &b;
    	ptr2->Func2();
    	ptr2 = &d;
    	ptr2->Func2();
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    这样就实现出了不同对象去完成同一行为时,展现出不同的形态。

    在这里插入图片描述

    查看反汇编也可得知,多态调用是在运行时才确定调用函数的地址的。

    在这里插入图片描述

    引用也有一样的效果:

    int main()
    {
    	Base b;
    	Derive d;
    	
        // 引用也有一样的效果
    	Base& r1 = b;
    	r1.Func1();
    	r1.Func2();
    
    	Base& r2 = b;
    	r2.Func1();
    	r2.Func2();
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    在这里插入图片描述

    派生类对象也可以赋值给基类对象,完成切片,但为什么这样不能实现多态调用。

    int main()
    {
    	Base b;
    	Derive d;
    	
    	Base& r1 = b;
    	r1.Func1(); // Base :virtual void Func1()
    	r1.Func2(); // Base :void Func2()
    
    	Base& r2 = b;
    	r2.Func1(); // Base :virtual void Func1()
    	r2.Func2(); // Base :void Func2()
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    编译器只会去检查是否符合多态的条件,符合就运行时去找函数地址,不符合就编译时去找函数地址。

    对象切片的时候,子类只会拷贝成员给父类对象,不会也不能拷贝虚表指针。
    因为如果把虚表指针也拷贝过去,这样父类的虚表指针就也是指向子类的虚表了,调用时就会混乱了。

    但是指针、引用切片就不一样了。不存在拷贝,就不会发生上述的混乱情况。
    在这里插入图片描述

    动态绑定与静态绑定

    1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态。
      比如:函数重载(有些书也把函数重载叫做静态的多态)
      这也就是编译时决议。
    int main()
    {
      int x = 1, y = 0;
      double a = 1.1, b = 0.0;
      swap(x, y);
      swap(a, b);
      // 感觉好像是同一个函数,其实不是,实际是编译链接时根据函数名修饰规则找到不同的函数。
      return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    1. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。
      这也就是运行时决议。

    5.单继承和多继承中的虚函数表

    单继承

    继续利用上面的例子,当我们给子类自己增加了一个虚函数,也能在虚函数表中找到吗?

    class Base
    {
    public:
    	virtual void Func1()
    	{
    		cout << "Base :virtual void Func1()" << endl;
    	}
    	void Func2()
    	{
    		cout << "Base :void Func2()" << endl;
    	}
    	virtual void Func3()
    	{
    		cout << "Base :virtual void Func3()" << endl;
    	}
    private:
    	int _b = 1;
    };
    
    class Derive : public Base
    {
    public:
    	virtual void Func1()
    	{
    		cout << "Derive :virtual void Func1()" << endl;
    	}
    	virtual void Func4()
    	{
    		cout << "Derive :virtual void Func4()" << 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
    • 41

    没有发现Func4的?
    在这里插入图片描述

    借助内存窗口观察一下:
    在这里插入图片描述

    通过虚表指针,找到虚表,缺省发现虚表中有3个指针存在,前面2个都是继承下来的虚函数。第三个难道就是子类自己的虚函数指针吗?

    如何确认呢?
    可以再写一个类去继承Derive。

    class Base
    {
    public:
    	virtual void Func1()
    	{
    		cout << "Base :virtual void Func1()" << endl;
    	}
    	void Func2()
    	{
    		cout << "Base :void Func2()" << endl;
    	}
    	virtual void Func3()
    	{
    		cout << "Base :virtual void Func3()" << endl;
    	}
    private:
    	int _b = 1;
    };
    
    class Derive : public Base
    {
    public:
    	virtual void Func1()
    	{
    		cout << "Derive :virtual void Func1()" << endl;
    	}
    	virtual void Func4()
    	{
    		cout << "Derive :virtual void Func4()" << endl;
    	}
    private:
    	int _d = 2;
    };
    
    class DDerive : public Derive
    {
    public:
    	virtual void Func4()
    	{
    		cout << "DDerive :virtual void Func4()" << endl;
    	}
    private:
    	int _dd = 3;
    };
    int main()
    {
    	Base b;
    	Derive d;
    	DDerive dd;
    
    	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

    好像并没有用。

    在这里插入图片描述

    没办法了,只能尝试去打印虚表。

    class Base
    {
    public:
    	virtual void Func1()
    	{
    		cout << "Base :virtual void Func1()" << endl;
    	}
    	void Func2()
    	{
    		cout << "Base :void Func2()" << endl;
    	}
    	virtual void Func3()
    	{
    		cout << "Base :virtual void Func3()" << endl;
    	}
    private:
    	int _b = 1;
    };
    
    class Derive : public Base
    {
    public:
    	virtual void Func1()
    	{
    		cout << "Derive :virtual void Func1()" << endl;
    	}
    	virtual void Func4()
    	{
    		cout << "Derive :virtual void Func4()" << endl;
    	}
    private:
    	int _d = 2;
    };
    
    // 想办法打印虚表,取内存值,打印并调用,确认是否存在Func4
    // 虚表指针指向的是个指针数组,而且全都是函数指针
    typedef void(*V_FUNC)();
    // 等价于 typedef void(*)() V_FUNC; 必须把typedef的名字放在中间
    
    //void PrintVFTable(V_FUNC a[])
    void PrintVFTable(V_FUNC* a)
    {
    	printf("vfptr:%p\n", a);
    	// VS下虚表指针最后是有一个nullptr的 或者清楚有几个虚函数也行
    	for (size_t i = 0; i < 3; ++i)
    	{
    		printf("[%d]:%p\n", i, a[i]);
    		// 有了函数地址就可以调用函数
    		V_FUNC f = a[i];
    		f();
    	}
    }
    int main()
    {
    	Base b;
    	Derive d;
    	// 取前四个字节就是虚表指针
    	//PrintVFTable((*(int*)&d)); // 这样解引用后是一个int不符合,还需再转一次
    	PrintVFTable((V_FUNC*)(*((int*)&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
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62

    在这里插入图片描述

    在这里插入图片描述

    因此我们也能确认,监视窗口将Func4隐藏了。

    注意:

    虚表存的只是虚函数的指针,虚函数和普通函数一样都是存在代码段里的。

    一个类型就有一个虚表,所有这种类型的对象都存相同的虚表指针,也就是相同类型的对象公用一个虚表。

    在这里插入图片描述

    这也就要求虚表要放在一个永久的区域,那就只剩静态区或者常量区了。

    如何进一步确定?

    int c = 0;
    int main()
    {
        Base b1;
        Base b2;
        Base b3;
        PrintVFTable((V_FUNC*)(*((int*)&b1)));
        PrintVFTable((V_FUNC*)(*((int*)&b2)));
        PrintVFTable((V_FUNC*)(*((int*)&b3)));
    
        // 验证虚表存在常量区还是静态区
        int a = 0;
        static int b = 1;
        const char* str = "hello";
        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", (V_FUNC*)*((int*)&b3));
        printf("函数地址:%p\n", &Derive::Func1);
        printf("函数地址:%p\n", &Derive::Func2);
        printf("函数地址:%p\n", &Derive::Func3);
        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

    在这里插入图片描述

    通过比对,可以发现,虚表是存在于代码段,而函数也是存在代码段里的。

    并且对象存的也只是虚表指针__vfptr。

    多继承

    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;
    };
    
    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()
    {
    	//printf("%p\n", &Derive::func1);
    
    	Derive d;
    	PrintVTable((VFPTR*)(*(int*)&d));
    	// 取出d的地址后,类型是Derive* +1跳过的是sizeof(Derive)
    	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
    • 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

    在这里插入图片描述

    在这里插入图片描述

    我们可以发现,同样是func1函数,但为啥虚表1和虚表2里面的func1函数地址不同呢?
    哪个才是真正的func1呢?

    int main()
    {
    	//printf("%p\n", &Derive::func1);
    
    	Derive d;
    	PrintVTable((VFPTR*)(*(int*)&d));
    	// 取出d的地址后,类型是Derive* +1跳过的是sizeof(Derive)
    	PrintVTable((VFPTR*)(*(int*)((char*)&d+sizeof(Base1))));
    
    	printf("func1 : %p\n", &Derive::func1);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    在这里插入图片描述

    结论就是,VS下将真正的地址封装起来了,通过多次跳转才能真正找到真正的func1。

    在这里插入图片描述

    int main()
    {
    	Derive d;
    	Base1* ptr1 = &d;
    	Base2* ptr2 = &d;
    	// 调用的都是Derive::func1 但是是在2个虚表中找到的覆盖的func1
    	ptr1->func1(); // Derive::func1
    	ptr2->func1(); // Derive::func1
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    调用Base2虚表中的func1时,是ptr2去调用,Base2不是第一个继承的类,ptr2发生切片指针偏移,ptr2指向的不是Derive对象的地址,而是指向的Base2,要找到Base1,就 需要修正存储this指针ecx的值。
    在这里插入图片描述

    6.细节

    内联函数可以是虚函数吗

    可以,但是只能是普通调用才会正常展开inline,如果是多态调用则会忽略内联属性。

    注意,要想查看inline的展开过程还需要配置一番:
    在这里插入图片描述

    在这里插入图片描述

    class A
    {
    public:
    	virtual inline void f1()
    	{
    		cout << "A::f1()" << endl;
    	}
    
    	virtual void f2();
    private:
    	int _a;
    };
    
    class B : public A
    {
    public:
    	virtual inline 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

    静态成员可以是虚函数吗

    不能,静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,因此静态成员函数无法放进虚函数表。

    在这里插入图片描述

    构造函数可以是虚函数吗

    不可以,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。
    多态的调用是调用之前就要去虚表里面去找,构造函数之前,虚表都还没初始化,自然没法找的。

    在这里插入图片描述

    析构函数可以是虚函数吗

    可以,并且最好把基类的析构函数定义成虚函数。

    对象访问普通函数快还是虚函数快

    如果是普通对象,是一样快。

    如果是指针对象或者引用对象,调用普通函数快,调用虚函数时还需要去虚函数表里面找。

    虚函数表什么时候生成

    虚函数表在编译阶段就生成了,一般情况下存在代码段的。
    注意,虚函数表和虚函数表指针不同,构造函数初始化列表时才初始化好虚函数表指针,是让__vfptr指向早就生成好的虚函数表。

    尾声

    🌹🌹🌹

    写文不易,如果有帮助烦请点个赞~ 👍👍👍

    Thanks♪(・ω・)ノ🌹🌹🌹

    😘😘😘

    👀👀由于笔者水平有限,在今后的博文中难免会出现错误之处,本人非常希望您如果发现错误,恳请留言批评斧正,希望和大家一起学习,一起进步ヽ( ̄ω ̄( ̄ω ̄〃)ゝ,期待您的留言评论。
    附GitHub仓库链接

  • 相关阅读:
    docker&docker-copose_限制容器cpu和内存
    电商平台APP商品详情源数据接口代码分享
    底层全部重构,小米澎湃OS完整系统架构公布
    dc9靶机攻略
    关于金融英文翻译,专业的翻译公司如何选择
    ChatGPT教我用200行代码写一个简版Vue框架 - OpenTiny
    Java面试题(JVM篇)
    UI设计工具都哪些常用的,推荐这5款
    铅掺杂PEG钝化石墨烯量子点荧光探针/磷脂包覆银石墨烯量子点多功能纳米粒表征
    c/c++指针从浅入深介绍——基于数据内存分配的理解(上)
  • 原文地址:https://blog.csdn.net/a2076188013/article/details/126420806