C++多态

多态的概念

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

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

多态的定义及实现

多态的构成条件

多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。要实现多态必须满足这两个条件:

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

虚函数

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

class Person
{
public:
	//被virtual修饰的类成员函数就是虚函数
	virtual void BuyTicket()
	{
		cout << "买票-全价" << endl;
	}
};

**注意:1.只有类的非静态成员函数前可以加virtual变成虚函数,普通函数和静态成员函数不可以
2.虚函数所加关键字virtual和虚继承所加关键字virtual是同一个关键字,但是二者没有任何关系,虚函数加virtual是为了实现多态,而虚继承加virtual是为了解决菱形继承时数据冗余和二义性问题 **

虚函数重写

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

//父类
class Person
{
public:
	//父类的虚函数
	virtual void BuyTicket()
	{
		cout << "买票-全价" << endl;
	}
};
//子类
class Student : public Person
{
public:
	//子类重写了父类的虚函数
	virtual void BuyTicket()
	{
		cout << "买票-半价" << endl;
	}
};

派生类重写基类虚函数之后就构成多态,此时通过基类的指针或者引用就可以实现多态调用,此时不同类型的对象调用就能产生不同的结果,实现了函数调用的多态形态。
注意:普通调用: 看指针或者引用或者对象的类型
多态调用 看指针或者引用指向的对象类型。

例如下面:Person父类对象就调用父类的虚函数,Student子类就调用子类的虚函数实现了多态。

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

void Func(Person& p)//基类的引用调用虚函数
{
	p.BuyTicket();
}

int main()
{
	Person p;
	Student s;
	Func(p);//调同一函数,传什么类型对象调什么对象的虚函数,传父类就调父类
	Func(s);传子类就调子类的虚函数
	return 0;
}

结果:买票-全价
买票-半价
注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因
为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用,》》建议两个都加,但是父类不加一定构不成多态,子类加没有用。

虚函数重写的两个例外

例外一

协变(基类与派生类虚函数返回值类型不同),也构成多态,但是必须满足条件:派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变

如一下代码一个返回值为父类A的指针,一个返回子类B的指针,返回值有继承关系,构成虚函数重写。

//父类A
class A {};
//子类B
class B : public A{};

class Person {
public:
		//返回值为父类A的指针或引用
	virtual A* f() { 
		cout << "A::f()" << endl;
		return new A;
	}
};

class Student : public Person {
public:
	//返回值为子类的指针或引用
	virtual B* f() {
		cout << "B::f()" << endl;
		return new B;
	}
};

int main()
{
	基类Person的指针指向子类Student对象多态调用f()
	Person* p = new Student;
	p->f();//结果“B::f()”实现了多态

	return 0;
}
例外二

析构函数的重写(基类与派生类析构函数的名字不同)

以下代码delete 释放空间是普通调用,delete p1释放的是父类,delete p2释放的也是父类,那么p2指向的子类当中只有父类那部分成员被释放,子类成员没有被释放造成内存泄漏。要想delete p2 调用的是子类的析构函数,那么必须构成多态,父类对象调父类的析构,子类对象调子类的析构。

class Person {
public:
 virtual ~Person() {cout << "~Person()" << endl;}
};
class Student : public Person {
public:
 virtual ~Student() { cout << "~Student()" << endl; }
};
int main()
{
 Person* p1 = new Person;
 Person* p2 = new Student;
 delete p1;
 delete p2;
 return 0;
}

解决办法构成多态:只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函数,才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数。
只需要将基类Person的析构函数前加virtual就构成重写了!

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

C++11 override 和 final

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

要想让一个类不能被继承?
方法1:父类构造函数私有化,派生实例化不出对象

方法二:加final
—final修饰的类为最终类,不能被继承。
—final修饰虚函数,表示该虚函数不能再被重写

class Car
{
public:
 virtual void Drive() final {}//加上final派生类不能重写
};
class Benz :public Car
{
public:
//重写报错
 virtual void Drive() {cout << "Benz-舒适" << endl;}
};

override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错,重写了不报错。可以强制检查基类某个必须重写的虚函数有没有被重写成功。

class Car{
public:
 virtual void Drive(){}
};
class Benz :public Car {
public:
 virtual void Drive() override {cout << "Benz-舒适" << endl;}//不报错,重写成功
};

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

在这里插入图片描述
继承派生类和基类中只有重写和重定义,重载必须在同一作用域,函数名相同就构成隐藏,如果是虚函数,且符号返回值和参数列表相同,就构成重写。
注意:重写的两个函数必须是虚函数

抽象类

概念

在虚函数的后面写上 =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()
 {
 cout << "BMW-操控" << endl;
 }
};
void Test()
{
Car* pBenz = new Benz;
 pBenz->Drive();
 Car* pBMW = new BMW;
 pBMW->Drive();
}

接口继承和实现继承

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

多态的原理

虚函数表

// 这里常考一道笔试题:sizeof(Base)是多少?
class Base
{
public:
 virtual void Func1()
 {
 cout << "Func1()" << endl;
 }
private:
 int _b = 1;
};
int main()
{
	Base b;
	cout << sizeof(b) << endl; //8
	return 0;
}

结果是8,为什么呢?
b对象当中除了_b成员外,实际上还有一个_vfptr放在对象的前面(有些平台可能会放到对象的最后面,这个跟平台有关)。
对象中的这个指针vfptr叫做虚函数表指针,简称虚表指针,虚表指针指向一个虚函数表,简称虚表,每一个含有虚函数的类中都至少有一个虚表指针。虚表指针指向虚表,虚表里面有什么呢?

其实虚表就是一个函数指针数组,存的是每个虚函数的地址。

下面基类Base有虚函数Func1,Func2,普通成员函数Func3,派生类Derive重写了Func1。

#include <iostream>
using namespace std;
//父类
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 = 1;
	char _ch = 97;
};
//子类
class Derive : public Base
{
public:
	//重写虚函数Func1
	virtual void Func1()
	{
		cout << "Derive::Func1()" << endl;
	}
private:
	int _d = 2;
};
int main()
{
	Base b;
	Derive d;
	return 0;
}

通过调试看结果
在这里插入图片描述
所有虚函数表里面实际上存的是每个虚函数的地址,是一个指针数组,由于Func1被重写,所以子类对象dd存的Func1函数地址是自己重写了的函数地址,覆盖了原来父类Base的Func1的函数地址,所以语法上叫重写,原理上叫覆盖,而Func2没有被重写,存的依然是BB对象中Func2的函数地址。普通成员函数Func3地址没有放下来,一般情况下会在这个指针数组最后放一个nullptr。(vs是这样的,g++没放,平台原因)。

总结一下虚表,就是一个指针数组,存虚函数地址

派生类虚表生成步骤--------------

  1. 先将基类中的虚表内容拷贝一份到派生类的虚表。
  2. 如果派生类重写了基类中的某个虚函数,则用派生类自己的虚函数地址覆盖虚表中基类的虚函数地址。
  3. 派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。

虚表是什么阶段初始化的?虚函数存在哪里?虚表存在哪里?

虚表指针实际上是在构造函数初始化列表阶段进行初始化的,注意虚表当中存的是虚函数的地址不是虚函数,虚函数和普通函数一样,都是存在代码段的,只是他的地址又存到了虚表当中。另外,对象中存的不是虚表而是指向虚表的指针。
所以对象当中存虚表指针,初始化列表阶段初始化,虚表在代码段,里面存每个虚函数地址,虚函数也存在代码段

至于虚表是存在哪里的?,我们可以通过以下这段代码进行判断。

int j = 0;
int main()
{
	Base b;
	Base* p = &b;
	printf("vfptr:%p\n", *((int*)p)); //000FDCAC
	int i = 0;
	printf("栈上地址:%p\n", &i);       //005CFE24
	printf("数据段地址:%p\n", &j);     //0010038C

	int* k = new int;
	printf("堆上地址:%p\n", k);       //00A6CA00
	char* cp = "hello world";
	printf("代码段地址:%p\n", cp);    //000FDCB4
	return 0;
}

代码当中打印了对象b当中的虚表指针,也就是虚表的地址,可以发现虚表地址与代码段的地址非常接近,由此我们可以得出虚表实际上是存在代码段的。

所以虚表是在代码段的

多态的原理

为什么基类指针指向父类对象时就调用父类的BuyTicket,指向子类对象时就调用子类的BuyTicket呢?

#include <iostream>
using namespace std;
//父类
class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "买票-全价" << endl;
	}
	int _p = 1;
};
//子类
class Student : public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "买票-半价" << endl;
	}
	int _s = 2;
};
int main()
{
	Person Mike;
	Student Johnson;
	Johnson._p = 3; //以便观察是否完成切片
	Person* p1 = &Mike;
	Person* p2 = &Johnson;
	p1->BuyTicket(); //买票-全价
	p2->BuyTicket(); //买票-半价
	return 0;
}

通过调试可以发现,Mike对象和Johnson对象都有一个虚表指针,指向不同的虚表。
在这里插入图片描述
正是因为各自有各自的虚表所以,当基类指针指向不同的对象时就会到各自对象的虚表里面去找。这样就实现了不同对象调用不同的虚函数,完成了多态。

现在回想一下多态的两个条件:一必须构成虚函数重写,就像上面一样有各自的虚函数表,那么继承过来的虚函数如果重写了,子类虚表就存重写了的函数地址,没有重写,函数地址还是父类的虚函数地址,所以必须虚函数重写覆盖原来的虚函数地址,这样调用各自的才有不同效果。 二:为什么一定要用基类的指针或者引用去调用呢?

就是因为用父类的指针或者引用去调其实是一种切片行为,指向父类对象时,不切片,指向子类对象时,切片下来保留继承父类的那一部分成员和虚表。
这样就可以调用各自的虚表了。

在这里插入图片描述
那为什么用基类的对象调用就不可以呢?
**因为基类对象调用时会发生拷贝构造,构造出来的对象的虚表指针不会被拷贝过去,所以构造出来的父类的对象虚表指针还是指向父类的虚表,实现不了多态。
所以必须用基类的指针进行,切片,而不是拷贝构造。

在这里插入图片描述
总结一下
1.构成多态,基类指针指向什么对象就调用什么对象的虚函数。与对象有关。
2.不构成多态,什么类型的对象就调用什么对象的虚函数,与类型有关。

动态绑定和静态绑定

静态绑定: 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也成为静态多态,比如:函数重载。

动态绑定: 动态绑定又称为后期绑定(晚绑定),在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。

//父类
class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "买票-全价" << endl;
	}
};
//子类
class Student : public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "买票-半价" << endl;
	}
};
int main()
{
	Student Johnson;
	Person p = Johnson; //不构成多态
	p.BuyTicket();
	return 0;
}

不构成多态,函数调用是在编译期间完成的是静待绑定,构成多态函数调用是运行期间完成的。看汇编代码。
不构成多态调用,直接就是call函数地址是在编译期间完成的,是静态绑定。
在这里插入图片描述

int main()
{
	Student Johnson;
	Person& p = Johnson; //构成多态
	p.BuyTicket();
	return 0;
}

看一下多态调用情况-----
在这里插入图片描述
构成多态,汇编指令变多了,原因是在运行时去通过找虚表找到对应的虚函数调用。
总结:所以静态绑定是在编译时确定的,而动态绑定是在运行时确定的

单继承和多继承关系中的虚函数表

单继承中的虚函数表

通过这段代码可以观察单继承虚表模型

//基类
class Base
{
public:
	virtual void func1() { cout << "Base::func1()" << endl; }
	virtual void func2() { cout << "Base::func2()" << endl; }
private:
	int _a;
};
//派生类
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 _b;
};

在这里插入图片描述
单继承虚表模型中完成了一下动作:

  1. 继承基类的虚表内容到派生类的虚表。
  2. 对派生类重写了的虚函数地址进行覆盖,比如func1。
  3. 虚表当中新增派生类当中新的虚函数地址,比如func3和func4。
    一、使用内存监视窗口
    因为我们看不到虚表里面存的函数地址可以通过内存窗口观看。
    在这里插入图片描述
    二. 内存窗口看见的地址是否和我们说的子类的虚表和父类虚表继承过来的顺序一致,和子类自己的虚函数按照声明顺序放在后面,即和单继承模型图一致吗?可以
    通过代码打印出来。
typedef void(*VFPTR)(); //虚函数指针类型重命名
//打印虚表地址及其内容
void PrintVFT(VFPTR* ptr)
{
	printf("虚表地址:%p\n", ptr);
	for (int i = 0; ptr[i] != nullptr; i++)
	{
		printf("ptr[%d]:%p-->", i, ptr[i]); //打印虚表当中的虚函数地址
		ptr[i](); //使用虚函数地址调用虚函数
	}
	printf("\n");
}
int main()
{
	Base b;
	PrintVFT((VFPTR*)(*(int*)&b)); //打印基类对象b的虚表地址及其内容
	Derive d;
	PrintVFT((VFPTR*)(*(int*)&d)); //打印派生类对象d的虚表地址及其内容
	return 0;
}

在这里插入图片描述
三. 同一个类实例化出的所有对象都是共用同一张虚表的
在这里插入图片描述

多继承中的虚函数表

再看看多继承虚表模型,通过一下代码观察

//基类1
class Base1
{
public:
	virtual void func1() { cout << "Base1::func1()" << endl; }
	virtual void func2() { cout << "Base1::func2()" << endl; }
private:
	int _b1;
};
//基类2
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. 对派生类重写了的虚函数地址进行覆盖(派生类中的各个虚表中存有该被重写虚函数地址的都需要进行覆盖),比如func1。
3. 在派生类第一个继承基类部分的虚表当中新增派生类当中新的虚函数地址,比如func3。

一. 内存窗口查看完整的虚表
在这里插入图片描述

二. 通过代码打印虚表内容
**需要注意的是:**我们在派生类第一个虚表地址的基础上,向后移sizeof(Base1)个字节即可得到第二个虚表的地址。

typedef void(*VFPTR)(); //虚函数指针类型重命名
//打印虚表地址及其内容
void PrintVFT(VFPTR* ptr)
{
	printf("虚表地址:%p\n", ptr);
	for (int i = 0; ptr[i] != nullptr; i++)
	{
		printf("ptr[%d]:%p-->", i, ptr[i]); //打印虚表当中的虚函数地址
		ptr[i](); //使用虚函数地址调用虚函数
	}
	printf("\n");
}
int main()
{
	Base1 b1;
	Base2 b2;
	PrintVFT((VFPTR*)(*(int*)&b1)); //打印基类对象b1的虚表地址及其内容
	PrintVFT((VFPTR*)(*(int*)&b2)); //打印基类对象b2的虚表地址及其内容
	Derive d;
	PrintVFT((VFPTR*)(*(int*)&d)); //打印派生类对象d的第一个虚表地址及其内容
	PrintVFT((VFPTR*)(*(int*)((char*)&d + sizeof(Base1)))); //打印派生类对象d的第二个虚表地址及其内容
	return 0;
}

结果:
在这里插入图片描述

菱形继承、菱形虚拟继承

菱形虚拟继承模型通过以下派生类和基类观察

class A
{
public:
	virtual void funcA()
	{
		cout << "A::funcA()" << endl;
	}
private:
	int _a;
};
class B : virtual public A
{
public:
	virtual void funcA()
	{
		cout << "B::funcA()" << endl;
	}
	virtual void funcB()
	{
		cout << "B::funcB()" << endl;
	}
private:
	int _b;
};
class C : virtual public A
{
public:
	virtual void funcA()
	{
		cout << "C::funcA()" << endl;
	}
	virtual void funcC()
	{
		cout << "C::funcC()" << endl;
	}
private:
	int _c;
};
class D : public B, public C
{
public:
	virtual void funcA()
	{
		cout << "D::funcA()" << endl;
	}
	virtual void funcD()
	{
		cout << "D::funcD()" << endl;
	}
private:
	int _d;
};

在这里插入图片描述
继承关系如图,基类A当中有虚函数funcA,B有虚函数funcB,C有虚函数funcC,D有虚函数funcD,A,B,C三个类都重写了funcA。

A类对象的分布情况

A中包含一个成员变量_a, 一个虚表指针指向一个存放虚函数funcA的虚表。
在这里插入图片描述

B类对象当中的成员及其分布情况。
B类对象中存放了一个成员变量_b,一个虚表指针指向存放funcB的虚表,还有一个虚基表指针指向一个虚基表,虚基表当中存储的是两个偏移量,第一个是虚基表指针距离B虚表指针的偏移量,第二个是虚基表指针距离虚基类A的偏移量。 B类由于是虚拟继承的A类,所以B类对象当中将A类继承下来的成员放到了最后。
在这里插入图片描述

C类对象中的情况

C类对象当中的成员分布情况与B类对象当中的成员分布情况相同。C类也是虚拟继承的A类,所以C类对象当中将A类继承下来的成员放到了最后,除此之外,C类对象的成员还包括一个虚表指针、一个虚基表指针和成员变量_c,虚表指针指向的虚表当中存储的是C类虚函数funcC的地址。
虚基表当中存储的是两个偏移量,第一个是虚基表指针距离C虚表指针的偏移量,第二个是虚基表指针距离虚基类A的偏移量。

在这里插入图片描述

D类对象中的成员分布情况

D由于是菱形虚拟继承,把A继承下来的成员放到最后,还有从B,C继承下来的成员,还有D的成员,需要注意的是,D类对象当中的虚函数funcD的地址是存储到了B类的虚表当中

在这里插入图片描述
建议
实际中我们不建议设计出菱形继承及菱形虚拟继承,一方面太复杂容易出问题,另一方面使用这样的模型访问基类成员有一定的性能损耗。

继承和多态常见的面试题

  1. 以下程序输出结果是什么()
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;
   }

解析:由于基类B没有重定义test()成员函数,所以p->test()调用的是继承过来的test()即父类A的test(),但是A的成员函数test()隐藏形参this是A类型的,所以调用func()是A的指针类型即基类的指针调用func函数,同时func虚函数又满足重写,构成了多态两个条件,但是A*指向的对象是子类B类型,所以调用B的func结果是B->1,因为是多态调用,而且虚函数重写是实现重写,声明不重写,所以缺省值不是0,还是1,结果选B。如果是普通调用B的func函数,,就不是虚函数重写,是同名隐藏,就整个重定义了包括声明,所以输出B->0。

在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

维生素C++

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

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

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

打赏作者

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

抵扣说明:

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

余额充值