C++多态


多态

多态:可以理解为一种事务有多种形态,不同的对象可以通过多态的方式去实现不同的事情

多态的前提是先继承,然后才能实现多态。

多态实现的条件:

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

重写

重写:虚函数+三同

同函数名、同参数类型、同返回类型

重写的内容为派生类对应虚函数{}中的内容。

class Person
{
public:
	virtual void Print()
	{
		cout << "Person" << endl;
	}
};
class Student : public Person  //多态的前提是继承
{
public:
	virtual void Print()  //virtual修饰,表示该函数为虚函数
	{
		cout << "Student" << endl;
	}
};

int main()
{
	Person p;
	p.Print();
	Student s;
	s.Print();
	return 0;
}

此时的virtual实现的虚函数和虚拟继承中的virtual没什么联系,只是都使用该关键字。

在这里插入图片描述

虚函数

虚函数是被virtual修饰的类成员函数,静态成员函数不能被修饰为虚函数。

class A
{
public:
	virtual void Print()  //虚函数,虚函数是可以支持类中声明,类外定义的。
	{
		cout<<"A"<<endl;
	}
};

虚函数的重写

基类和父类的虚函数,其完全相同,同函数名,同参数、同返回值类型,这样的父子类的虚函数构成重写。

父类的virtual必须有,子类重写的虚函数的virtual可以不带,因为我们继承父类中的该函数,仍然保留虚函数的属性,但是我们最好是带上吧。

虚函数重写的两个特例:

  1. 协变

派生类重写基类虚函数时,返回值类型可以不同,基类的返回值类型为基类对象的指针或者引用,派生类的返回值类型为派生类对象的指针或者引用

class A
{};
class B : public A
{};
class Person
{
public:
	virtual Person* Print()  	//返回值类型也可以为A*
	{
		cout << "Person" << endl;
		return 0;
	}
};
class Student : public Person
{
public:
	virtual Student* Print()	//返回值类型也可以为B*
	{
		cout << "Student" << endl;
		return 0;
	}
};
void Func(Person& p)
{
	p.Print();
}
int main()
{
	Person p;
	p.Print();
	return 0;
}

只要基类对应基类,派生类对应派生类即可,两个返回值类型要有继承关系,即构成协变

​ 2.析构函数的重写

当我们手动new一个对象时候,如Person* ptr=new Student; 此时遇到切片,This指针为Person对象,我们使用delete释放空间的时候,只会调用~Person()不会调用Student的析构函数,从而导致内存泄漏。

所以我们对于这种情况,将析构函数重写(虚函数),此时编辑器对于函数名做特殊化处理,设置为destructor,这样就符合重写的三同要求。

在这里插入图片描述

协变实际用到不多,基本上没有,析构函数的重写主要是为了兼容前面所学的new和delete内容,然后才会让编辑器去特殊处理函数名为destructor,从而实现重写。

C++11 override 和 final

  • override:用于检查重写是否正确(格式符合要求)
  • final:修饰虚函数,表示该虚函数不能被重写,也可以final类,使得该类不能被继承

在这里插入图片描述
在这里插入图片描述

重载、重写、重定义

重载:在同一个作用域,函数名相同,参数不同(返回值类型无所谓)

重写(覆盖):基类和派生类中,对于基类的虚函数在派生类中重写,函数名、参数、返回值类型都相同。

重定义(隐藏):在子类中不构成重写的,与父类和子类的同名函数都成为重定义

抽象类

抽象类就是将类中的虚函数加上=0,只是声明,不会定义,作为接口供子类重写实现函数,这样的函数称为纯虚函数。包含纯虚函数的类叫做抽象类(接口类),抽象类不能实例化出对象,子类继承后也不能实例化对象,只能重写纯虚函数,派生类才能实例化出对象。纯虚函数在子类中必须重写,体现接口继承。

class Person
{
public:
	virtual void Print() = 0;
	virtual ~Person() { cout << "~Person()" << endl; }
};
class Student : public Person
{
public:
	virtual void Print() override  //override来判断是否重写正确
	{
		cout << "Student" << endl;
	}
	 ~Student() { cout << "~Student()" << endl; }
};
void Func(Person& p)
{
	p.Print();
}
int main()
{
	//Person p;  //不允许创建抽象类对象
	//Person* ptr = new Person;//不允许指向抽象类本身
	Person* p = new Student;  //抽象类指针只能指向子类
	p->Print();
	Student s;
	s.Print();
	return 0;
}

总结:

  • C++抽象类是为子类抽象一个基类,抽象类的主要作用是为子类提供相应的属性和方法,其他如果需要在子类中修改方法,需要将其声明为纯虚函数。
  • 抽象类特点为不能实例化对象(但是可以有自己的指针和引用,即可以实现多态),要有纯虚函数供子类重写。
  • 含有一个及以上纯虚函数的类称为抽象类。
  • 抽象类的指针只能动态指向子类对象,即Person* ptr = new Student;
  • 如果子类中没有实现纯虚函数,而只是继承基类的纯虚函数,则这个子类仍然还是一个抽象类。
  • 如果子类中给出了基类纯虚函数的实现,则该子类就不再是抽象类,可以建立对象。

接口继承和实现继承

实现继承:普通函数的继承。子类继承普通函数,目的是直接调用,是继承函数的实现

接口继承:虚函数的继承。虚函数存在的目的是为了子类重写,继承的是接口,实现需要子类重写,达成多态,继承的是接口。

虚函数的目的就是为了实现多态和接口继承,如果不实现多态,那就不要定义为虚函数。

多态的原理

多态之所以能实现,是因为虚函数和重写,那么虚函数在内存中是如何存储的呢?

虚函数表

虚函数表:存储类中虚函数地址,有虚函数的类至少有一个虚函数表,在vs下虚函数表指针_vfptr放在对象的头部(不同平台不同),_vfptr存放地址,指向虚函数表(虚表)

在这里插入图片描述

在这里插入图片描述

虚表中只会存放虚函数,普通函数不会放在虚表中,虚函数表的本质是一个存储虚函数指针的指针数组,一般情况下数组最后放置nullptr

子类虚表生成顺序:

  1. 拷贝父类虚表到子类虚表中
  2. 将子类重写的虚函数覆盖父类的虚函数
  3. 子类本身的虚函数按照生命顺序依次加入

虚函数存在哪里呢?虚表在哪里呢?

虚函数和普通函数一样存放在代码段,只是将虚函数地址存放在虚表中,对象中存储的不是虚表,是虚表指针,虚表在vs下是存储在代码段的

在这里插入图片描述

多态实现的过程

通过汇编查看多态实现的过程

在这里插入图片描述

满足多态的函数调用,不是在编译时确定的,是在运行起来之后到对象中去寻找的,不满足多态的函数调用是编译时确定好的。

动态绑定和静态绑定

静态绑定又称为前期绑定(早绑定),在程序编译期间就确定了程序的行为,称为静态多态,比如函数重载,编译期间就会直到要执行哪一个函数。

动态绑定又称为后期绑定(晚绑定),在程序编辑期间不知道调用哪一个函数,在运行后根据具体拿到的类型确定程序的具体行为,调用具体的函数,称为动态多态

一句话,静态绑定编译器在编译阶段就知道要调用哪一个,动态绑定需要在运行时才知道具体调用哪一个函数。

单继承和多继承的虚函数表

单继承子类只有一个虚表,多继承有多个虚表,虚表的顺序和继承顺序有关。

我们前文讲解的都是单继承的虚表,直到子类的虚函数是按照声明顺序放在虚表最后的,那么多继承的时候子类的虚函数放在哪一个表中呢?

class Person1
{
public:
	virtual void Func1()
	{
		cout << "Person1::Func1" << endl;
	}
	virtual void Func2()
	{
		cout << "Person1::Func2" << endl;
	}
	int _a;
};
class Person2
{
public:
	virtual void Func1()
	{
		cout << "Person2::Func1" << endl;
	}
	virtual void Func3()
	{
		cout << "Person2::Func3" << endl;
	}

	int _b;
};

class Student : public Person1, public Person2
{
public:
	virtual void Func1()
	{
		cout << "Student::Func1" << endl;
	}
	virtual void Func2()
	{
		cout << "Student::Func2" << endl;
	}
	virtual void Stu()
	{
		cout << "STU" << endl;
	}
};
//_vfptr是函数指针数组的形式
typedef void(*VFPTR) ();  
void PrintVTable(VFPTR vTable[])
{
	// 依次取虚表中的虚函数指针打印并调用。调用就可以看出存的是哪个函数
	for (int i = 0; vTable[i] != nullptr; ++i)
	{
		printf("[%d]->%p", i, vTable[i]);
		VFPTR f = vTable[i];
		f();//调用
	}
	cout << endl;
}

int main()
{
	//得到虚表地址
	Student s;
	//得到Person1虚表的地址

	PrintVTable((VFPTR*)(*(int*)&s));

	//得到Person2虚表的地址
	//切片
	Person2* ptr = &s;
	PrintVTable((VFPTR*)*(int*)ptr);
	return 0;
}

多继承,假设两个基类,切片之后是如何调用子类重写的函数Func1,调用过程是什么?

在这里插入图片描述

class Person1
{
public:
	virtual void Func1()
	{
		cout << "Person1::Func1" << endl;
	}
	virtual void Func2()
	{
		cout << "Person1::Func2" << endl;
	}
	int _a;
};
class Person2
{
public:
	virtual void Func1()
	{
		cout << "Person2::Func1" << endl;
	}
	virtual void Func3()
	{
		cout << "Person2::Func3" << endl;
	}

	int _b;
};

class Student : public Person1, public Person2
{
public:
	virtual void Func1()
	{
		cout << "Student::Func1" << endl;
	}
	virtual void Func2()
	{
		cout << "Student::Func2" << endl;
	}
	virtual void Stu()
	{
		cout << "STU" << endl;
	}
};
int main()
{
	Student s;
	s.Func1();
	Person1* p1 = &s;
	p1->Func1();
	Person2* p2 = &s;
	p2->Func1();
	return 0;
}

对于切片后的p1、p2,还是s,子类重写函数Func1,并分别调用的时候,都是使用This指针调用函数,所以由于p1==s!=p2,所以p1和s都是直接找到Func1然后调用,p2需要先通过ecx调整This指针的地址,找到p1位置,然后再去调用。

为什么两个虚表中的Func1函数是一个函数,但是地址不同?

这是因为,第二个虚表中的调度Func1需要对于This指针进行调整然后再去调度,所以拐了个弯,去寻找偏移量,然后修改This,最后才去调用,另外两个虚表中的Func1地址,只是指向Func1的指针。

在这里插入图片描述

菱形继承和菱形虚拟继承

菱形继承和菱形虚拟继承+虚函数重写,底层很复杂,我们要避免写出菱形继承

分析:类有四种,A、B1、B2、C

  • 当菱形继承时

在这里插入图片描述

  • 当菱形虚拟继承,但是B1和B2中只是重写A中的虚函数,自身无虚函数

在这里插入图片描述

  • 当菱形虚拟继承+B1和B2都有各自的虚函数

在这里插入图片描述

菱形继承和菱形虚拟继承重写虚函数,此时的底层已经很复杂了,所以我们在以后使用多继承的时候,避免形成菱形继承

总结

多态前提是继承

多态:重写+基类指针或引用

重写:虚函数+三同

为什么只有基类指针和引用能实现多态,基类对象却不行,这是因为切片,基类得到的是派生类中的基类的成员,但无法将父类对象的虚函数进行改变,所以无法实现多态。

多态的两种特殊情况:协变、析构函数(用于new和delete)

常见多态面试题

  • 什么是多态?

多态朴素理解为,不同的对象来在同一件事情(例如,买票)得到不同的结果。多态分为静态多态和动态多态,静态多态指函数重载,动态多态是指继承中虚函数重写+基类指针或引用调用重写的虚函数,多态的目的是为了更方便和灵活多种形态的调用。

  • 什么是重载、重写、重定义?

重载:在同一作用域中,两个相同函数名,不同参数的函数,构成重载

重写:在不同作用域,即父类和子类中,使用虚函数,对于三同(同函数名、同参、同返回类型)函数的实现,构成重写。

重定义:在父类和基类中,非重写的函数,同名构成重定义。

  • 多态的实现原理

多态是依据函数名修饰规则(同名函数,构成重写)和虚函数表(存放虚函数),简单来讲:多态=虚函数重写+基类指针或引用调度。

  • inline函数可以是虚函数吗?

可以,inline修饰的函数是可以加上virtual修饰符成为虚函数的,但是同时,编辑器也会否认inline内联,因为虚函数要放在虚函数表中,内联调用是以直接展开的形式。

  • 静态成员可以是虚函数吗?

不可以,静态成员没有This指针,无法访问虚函数表,静态成员的访问形式为类型::成员,且静态成员只有一份,静态成员无法实现多态,也就没有意义,用virtual修饰,在编译阶段,就会报错。

  • 构造函数可以是虚函数吗?

不可以,虚函数表指针是在构造函数初始化列表阶段才会初始化的。

  • 析构函数可以是虚函数吗?什么场景下的析构函数为虚函数?

可以,析构函数实现重写,是针对于new和delete这样的函数,在基类实现多态的时候,析构成员能调度子类对象的析构函数,然后调用父类的析构函数。

Person& p=new Student("张三");
delete p;//此时需要Person类中析构函数为虚函数,重写的方式,调用Student类的析构函数,然后在调用Person类的析构函数,放置内存泄漏
  • 对象访问普通函数快还是虚函数快?

如果是普通对象调用这两个函数,是一样快,如果是基类指针或引用对象,普通函数更快。构成多态,虚函数需要通过虚函数表查找,来找到真正要调用的函数,普通函数直接调用即可,所以普通函数的调度更快一点,但是现在电脑的CPU运算很快,基本上体会不到时间差距。

  • 虚函数表是什么阶段生成的,存在哪里?

虚函数表是在编译阶段产生的,存放在代码段(常量区)。

  • C++菱形继承的问题?虚继承的原理?

菱形继承问题:数据冗余和二义性,虚继承的原理:通过虚基表实现。

  • 什么是抽象类?抽象类的作用?

抽象类:拥有纯虚函数的类,抽象类是将纯虚函数做为接口使用的,将整个类作为接口。抽象类纯虚函数,相当于强制子类重写该函数,不然无法实例化对象,拥有纯虚函数(继承,没重写)的类都无法实例化对象

  • 37
    点赞
  • 37
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 39
    评论
评论 39
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

小王学代码

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

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

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

打赏作者

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

抵扣说明:

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

余额充值