【初阶与进阶C++详解】第十四篇:多态(虚函数+重写(覆盖)+抽象类+单继承和多继承)

🏆个人主页企鹅不叫的博客

​ 🌈专栏

⭐️ 博主码云gitee链接:代码仓库地址

⚡若有帮助可以【关注+点赞+收藏】,大家一起进步!

💙系列文章💙

【初阶与进阶C++详解】第一篇:C++入门知识必备

【初阶与进阶C++详解】第二篇:C&&C++互相调用(创建静态库)并保护加密源文件

【初阶与进阶C++详解】第三篇:类和对象上(类和this指针)

【初阶与进阶C++详解】第四篇:类和对象中(类的六个默认成员函数)

【初阶与进阶C++详解】第五篇:类和对象下(构造+static+友元+内部类

【初阶与进阶C++详解】第六篇:C&C++内存管理(动态内存分布+内存管理+new&delete)

【初阶与进阶C++详解】第七篇:模板初阶(泛型编程+函数模板+类模板+模板特化+模板分离编译)

【初阶与进阶C++详解】第八篇:string类(标准库string类+string类模拟实现)

【初阶与进阶C++详解】第九篇:vector

【初阶与进阶C++详解】第十篇:list

【初阶与进阶C++详解】第十一篇:stack+queue+priority_queue

【初阶与进阶C++详解】第十二篇:模板进阶(函数模板特化+类模板特化+模板分离编译)

【初阶与进阶C++详解】第十三篇:继承



💎一、多态的概念

多态: 从字面意思来看,就是事物的多种形态。不同的对象去完成同一个行为会产生不同的效果。

💎二、多态的定义及实现

🏆1.多态构成条件

  1. 必须有基类的指针或引用调用

  2. 被调用的函数必须是虚函数,其派生类必须对基类的虚函数进行重写

    原因1:基类和派生类都有自己的虚表,如果通过值传递的话,基类可能会拿到派生类的虚表

    原因2:有虚函数就有虚函数表,对象当中就会存放一个虚基表指针,通过虚基表指针指向的内容来访问对应的函数。若子类没有重写父类的虚函数内容,则子类也会调用父类的函数。

    下面是演示

    //满足多态的条件:成员函数调用与对象类型无关,指向那个对象就调用哪个的虚函数
    //不满足多态的条件:成员函数的调用与对象类型有关,是哪个对象类型就调用哪个对象的虚函数。
    class Person
    {
    public:
    	virtual void BuyTicket()
    	{
    		cout << "买票全价" << endl;
    	}
    };
    
    class Student : public Person
    {
    public:
    	virtual void BuyTicket() 
    	{
    		cout << "买票半价" << endl;
    	}
    };
    
    void Func1(Person& p) { p.BuyTicket(); }
    void Func2(Person* p) { p->BuyTicket(); }
    void Func3(Person p) { p.BuyTicket(); }
    
    int main()
    {
    	Person p;
    	Student s;
    	//输出买票全价和买票半价
    	cout << "基类的引用调用:" << endl;
    	Func1(p);
    	Func1(s);
    	//输出买票全价和买票半价
    	cout << "基类的指针调用:" << endl;
    	Func2(&p);
    	Func2(&s);
    	//两个都输出买票全价
    	cout << "基类的对象调用:" << endl;
    	Func3(p);
    	Func3(s);
    
    	return 0;
    }
    
    

🏆2.虚函数

虚函数:virtual关键字修饰的类成员函数叫做虚函数,虚函数会放到虚表当中,虚表当中会存放虚函数指针

class Person
{
public:
	// 虚函数
	virtual void BuyTicket()
	{
		cout << "买票-全价" << endl;
	}
};

  1. 不能定义虚构造函数,不能用派生类的构造来重写基类的构造
  2. 不能定义静态多态,因为静态函数是属于整个类的,不是属于某个对象

🏆3.虚函数重写(覆盖)

3.1重写概念

派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同,有例外,称子类的虚函数重写了基类的虚函数。 (重写是对函数体进行重写)

虚函数重写(语法层概念)派生类对继承基类虚函数进行重写

虚函数覆盖(原理层概念)子类的虚表拷贝父类的虚表进行修改,覆盖重写对应父类的虚函数

class Person
{
public:
	virtual void func()
	{
		cout << "普通人->正常买票" << endl;
	}
};

class Student : public Person
{
public:
	//子类必须重写父类的虚函数
    //只要父类有virtual,不管子类有没有virtual都算完成重写了
	virtual void func()
	{
		cout << "学生->半价买票" << endl;
	}
};
//必须是父类的指针或引用去调用虚函数
//这里的参数类型不能是对象,否则是一份临时拷贝,则无法构成多态
void F(Person& ps)
{
	ps.func();
}

int main()
{
	Person ps;
	Student st;
	F(ps);
	F(st);
	return 0;
}

3.2补充

虚函数的重写只重写函数实现,不重写缺省值。

class Base1{
public:
    virtual void Show(int n = 10)const{    //提供缺省参数值
        std::cout << "Base1:" << n << std::endl;
    }
};
 
class Base2 : public Base1{
public:
    virtual void Show(int n = 30)const{     //重新定义继承而来的缺省参数值
        std::cout << "Base2:" << n << std::endl;
    }
};
 
int main(){
 
    Base1* p1 = new Base2;        
    p1->Show();           
 
    return 0;
}

输出的是Base2:10,

因为如果子类重写了缺省值,此时的子类的缺省值是无效的,使用的还是父类的缺省值。原因是因为多态是动态绑定,而缺省值是静态绑定。对于P1,他的静态类型也就是这个指针的类型是Base1,所以这里的缺省值是Base1的缺省值,而动态类型也就是指向的对象是Base2,所以这里调用的虚函数则是Base2中的虚函数,所以这里就是Base2中的虚函数,Base1中的缺省值,也就是Base2:10。

3.3重写例外

1.协变:基类和派生类的虚函数的返回类型不同即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。(也就是基类虚函数的返回类型和派生类的虚函数的返回类型是父子类型的指针或引用)

//指针
class A {};
class B :public A {};
class Person
{
public:
	virtual A* func()
	{
		cout << "virtual A* func()" << endl;
		return new A;
	}
};
class Student : public Person
{
public:
    //这里即使派生类虚函数没有写virtual,func依旧是虚函数,因为先继承了父类函数接口声明
    //重写父类虚函数实现
    //但是我们自己要记得写上
	virtual B* func()
	{
		cout << "virtual B* func()" << endl;
		return new B;
	}
};
--------------------------------------------------------
  //引用
  class Human
{
public:
	virtual Human& print()
	{
		cout << "i am a human" << endl;
		
		return *this;
	}
};

class Student : public Human
{
public:
	virtual Student& print()
	{
		cout << "i am a student" << endl;

		return *this;
	}
};

2.析构函数的重写 基类与派生类的析构函数的函数名不同

class Person
{
public:
	virtual ~Person()
	{
		cout << "~Person()" << endl;
	}
};
class Student: public Person
{
public:
    //派生类可以不用写virtual
    //给基类的析构函数加上virtual修饰,这样只要传派生类的对象给基类的指针/引用,就可以直接调用派生类对应的析构函数
	virtual ~Student()
	{
		cout << "~Student()" << endl;
	}
};
int main()
{
	Person* p = new Person;
  // 基类不加virtual,不构成多态,父类指针只会根据类型去调用对于的析构函数
  // 基类加了virtual,构成多态,父类指针会根据指向的对象去调用他的析构函数
	Person* ps = new Student;

	delete p;
	delete ps;

	return 0;
}

​ 不加virtual关键字时,两个析构函数不构成多态,所以调用析构函数时是与类型有关的,因为都是都是父类类型,所以只会调用父类的析构函数,会导致内存泄漏的问题。加了virtual关键字时,因为两个析构函数被编译器处理成同名函数了,所以完成了虚函数的重写,且是父类指针调用,所以此时两个析构函数构成多态,所以调用析构函数时是与类型无关的,因为父类指针指向的是子类对象,所以会调用子类的析构函数,子类调用完自己的析构函数又会自动调用父类的析构函数来完成对父类资源的清理。

🏆4.C++11 final和override

1.final

使用final修饰的虚函数不能被重写。(还可以修饰类,表示该类不可以被继承)

//Car:Drive不能被Benz重写了
class Car
{
public:
	// final  表示该虚函数不能被重写  也可以修饰类,表示该类不可以被继承
	virtual void Drive() final {}
};
class Benz :public Car
{
public:
	virtual void Drive() { cout << "Benz-舒适" << endl; }
};

2.override

override写在子类种是用来检测派生类虚函数是否构成重写的关键字,如果子类没有重写父类就会报错

class Car
{
public:
    //如果父类没有virtual就会报错
	virtual void Drive() {}
};
class Benz :public Car
{
public:
    // override 检测派生类是否对虚函数进行了重写
    //只要父类有virtual,不管子类有没有virtual都算完成重写了
	virtual void Dirve() override { cout << "Benz-舒适" << endl; }
};

🏆5.重载、覆盖、隐藏对比

在这里插入图片描述

💎三、抽象类

🏆1.概念

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

意义:强制子类完成父类虚函数的重写

class Car
{
public:
	virtual void Drive() = 0;
};
class Benz : public Car
{
public:
	virtual void Drive()
	{
		cout << "Benz" << endl;
	}
};


class BMW : public Car
{
public:
	virtual void Drive () override
	{
		cout << "BMW" << endl;
	}
};

int main()
{
	Car* pBenZ = new Benz;
	pBenZ->Drive();

	Car* pBMW = new BMW;
	pBMW->Drive();

	delete pBenZ;
	delete pBMW;
	return 0;
}

🏆2.接口继承和实现继承

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

💎四、多态的原理(重点)

🏆1.虚函数表

一个含有虚函数的类中至少有一个虚函数指针,这个指针指向了一张表——虚函数表(简称虚表),这张表中存放了这个类中所有的虚函数的地址。

class human
{
public:
	virtual void print()
	{
		cout << "i am a human" << endl;
	}
    virtual void liangzai()
	{
		cout << ".." << endl;
	}
	void test()
	{
		cout << "1test1" << endl;
	}
	int _age;
};

class student : public human
{
public:
	virtual void print()
	{
		cout << "i am a student" << endl;
	}
	int _stunum;
};

int main()
{
	human man;
	student st;
	cout << sizeof(man) << endl;

	cout << sizeof(st) << endl;
	return 0;
}

上面输出答案是8和12

在这里插入图片描述

指针 _vfptr,这个指针指向了一个函数指针数组,这个函数指针数组也就是虚函数表,其中的每一个成员指向的都是之前我们实现的虚函数,这个 _vfptr也被称为虚函数表指针

可以看出,两个虚函数地址是不一样的,其实子类会先把父类的虚表拷贝一份下来,如果子类重写了虚函数,那么子类的虚函数的地址将会覆盖虚表中的地址,如果没有重写,那么将不覆盖。

小结:

  1. 一个类的所有对象,共享一张虚表
  2. 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr
  3. 子类对象由两部分构成,一部分是父类继承下来的成员,虚表指针指向的虚表有父类的虚函数,也有子类新增的虚函数
  4. 子类完成父类虚函数的重写其实是对继承下来的虚表的中重写了的虚函数进行覆盖,把地址更换了,语法层是称为覆盖
  5. 虚表生成的过程:先将基类中的虚表内容拷贝一份到派生类虚表中 ,如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 ,派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后

1.1虚表存放的位置和虚表指针存放的位置

虚表指针肯定是存在类中的,从上面的类对象模型中可以看出。其次虚表存放的是虚函数的地址,这些虚函数和普通函数一样,都会被编译器编译成指令,然后放进代码段。虚表也是存在代码段(常量区)的,因为同类型的对象共用一张虚表

验证代码:

class Base
{
public:
	virtual void func1() { cout << "Base::func1" << endl; }
	virtual void func2() { cout << "Base::func2" << endl; }
	virtual void func3() { cout << "Base::func3" << endl; }
	void func() {}

	int b = 0;
};

class Derive :public Base
{
public:
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func2() { cout << "Derive::func2" << endl; }
	virtual void func4() { cout << "Derive::func4" << endl; }
	virtual void func5() { cout << "Derive::func5" << endl; }

	int d = 0;
};

void func() {}
int globalVar = 10;
int main()
{
	Base b;
	Derive d;
	const char* pChar = "hello";
	int c = 1;
	static int s = 20;
	int* p = new int;
	const int i = 10;

	printf("栈变量:%p\n", &c);
	printf("虚表指针:%p\n", (int*)&b);
	printf("对象成员:%p\n", ((int*)&b + 1));
	printf("堆变量:%p\n", p);
	printf("代码段常量:%p\n", pChar);
	printf("普通函数地址:%p\n", func);
	printf("成员函数地址:%p\n", &Base::func);
	printf("虚函数:%p\n", &Base::func1);
	printf("虚函数表:%p\n", *(int*)&b);
	printf("数据段:%p\n", &s);
	printf("数据段:%p\n", &globalVar);

	delete p;
	return 0;
}

1.2虚函数表(虚表)和 虚基表区别

1.虚函数存放在代码段当中,只是它的地址放到虚表当中,我们对象存放的是虚表指针,虚表里面放在虚函数的地址,虚表也是存放在代码段当中的,虚函数表里面只放虚函数,普通函数不会放入。
2.虚基表当中放着的就是8个字节,一个是多态的偏移量,一个是虚继承的基类成员在子类当中的偏移量。

🏆2.多态原理

多态是在运行时到指向的对象中的虚表中查找要调用的虚函数的地址,然后进行调用。

2.1为什么需要派生类函数为虚函数,并且必须要重写才能实现多态?

派生类会继承基类的虚函数表,如果派生类完成了重写,则会将重写的虚函数覆盖掉原有的函数。所以指针或引用指向哪一个对象,就调用对象中虚函数表中对应位置的虚函数,来实现多态。

2.2为什么必须要指针或者引用才能构成多态,不可以是父类对象?

子类对象给父类对象赋值时,**会因为对象切割,导致他的内存布局整个被修改,完全转换为基类对象的类型,虚函数表也与基类相同,**同时会调用父类的拷贝构造对父类的成员变量进行拷贝构造,但是虚表指针不会参与切片,这样父类对象无法找到子类的虚表,所以父类对象不能够调用子类的虚函数。但是子类对象给父类的指针或引用赋值时,内存布局是兼容的,他不会像赋值一样改变派生类对象的内存结构,所以派生类对象的虚函数表得到了保留,是让父类的指针指向父类的那一部分或引用父类的那一部分,这样父类还是可以拿到子类的虚表指针,通过虚表指针找到子类的虚表,从而可以调用虚表中的虚函数。

小结:

  1. 满足多态后,函数的调用不是编译时确认的,而是在运行时确认的。
  2. 多态满足的两个条件:一个是虚函数的覆盖,一个是对象的指针和引用调用

🏆3.动态绑定和静态管理

静态绑定(编译时)在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
动态绑定(运行时)是在程序运行期间,也称为动态多态,比如:派生类和虚函数组成的多态。

💎五、单继承和多继承关系的虚函数表(重点)

🏆1.单继承中的虚函数表

对于单继承:覆盖的函数被放到了虚表中原来父类虚函数的位置,没有被覆盖的函数依旧。

class Base
{
public:
	virtual void func1() { cout << "Base::func1" << endl; }
	virtual void func2() { cout << "Base::func2" << endl; }
	virtual void func3() { cout << "Base::func3" << endl; }
	void func() {}

	int b = 0;
};

class Derive :public Base
{
public:
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func2() { cout << "Derive::func2" << endl; }
	virtual void func4() { cout << "Derive::func4" << endl; }
	virtual void func5() { cout << "Derive::func5" << endl; }

	int d = 0;
};

在这里插入图片描述

派生类中只可以看见func1和func2,后面两个函数看不见,这是因为编译器把这两个新增的虚函数给隐藏了,为了我们能够更好的观察,我们可以通过写代码来看。

思路:

1.取出d地址,强转成int类型
2.解引用,这样就是d中头4个字节,即虚表指针的内容
3.将这个int指针强转为VFPTR*类型,因为虚表就是一个存放VFPTR类型指针的数组。
4.虚表指针传递给PrintVTable进行打印虚表。
5.需要说明的是这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,虚表最后面没有
放nullptr,导致越界,这是编译器的问题。我们只需要点目录栏的-生成-清理解决方案,再编译就好了。

typedef void(*VFPTR)();// 给函数指针typedef

void PrintVirtualTable(VFPTR vTable[])
{
	cout << "虚表地址:" << vTable << endl;
	for (int i = 0; vTable[i] != nullptr; i++)
	{
		printf("第%d个虚函数地址:%p\n", i, vTable[i]);
		VFPTR pf = vTable[i];
		pf();// 通过函数地址调用函数
	}
	cout << endl;
}

int main()
{
	Base b;
	Derive d;
	PrintVirtualTable((VFPTR*)*((int*)(&b)));
	PrintVirtualTable((VFPTR*)*((int*)(&d)));
	return 0;
}

image-20220531231214203

可以看出派生类对象中新增的虚函数会按照虚函数函数次序声明放在虚表的最后。

🏆2.多继承中的虚函数表

对于多继承:子类有多个虚表(即每个父类都有自己的虚表,看你继承几个父类 就有几张虚表),子类的成员函数被放到了第一个父类的表中。(所谓的第一个父类是按照声明顺序来判断的)

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()
{
	Derive d;
	//Base1中的虚函数
	VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);
	PrintVTable(vTableb1);
	//Base2中的虚函数
	VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));
	PrintVTable(vTableb2);
	return 0;
}

image-20220531231556687

多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中

💎六、问答

1.inline函数可以是虚函数吗 :可以,不过多态调用编译器就忽略inline属性,这个函数就不再是inline,因为虚函数要放到虚表中去

2.静态成员可以是虚函数吗 :不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表

3.构造函数可以是虚函数吗 :不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的 ,虚函数的意义是多态,多态调用要到虚函数中找,构造函数之前还没初始化。

4.析构函数可以是虚函数吗?什么场景下析构函数是虚函数?:可以,并且最好把基类的析构函数定义成虚函数

5.对象访问普通函数快还是虚函数更快?:首先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找

6.虚函数表是在什么阶段生成的,存在哪的:虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的


  • 10
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 10
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

penguin_bark

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值