C++多态详解

**1.**多态的概念

多态就是函数调用的多种形态,使用多态能够使得不同的对象去完成同一件事时,产生不同的动作和结果。
举个栗子:比如买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人
买票时是优先买票
再举个栗子: 最近为了争夺在线支付市场,支付宝年底经常会做诱人的扫红包支付给奖励金的 活动。那么大家想想为什么有人扫的红包又大又新鲜8块、10块...,而有人扫的红包都是1毛,5 毛....。其实这背后也是一个多态行为。支付宝首先会分析你的账户数据,比如你是新用户、比如你没有经常支付宝支付等等,那么你需要被鼓励使用支付宝,那么就你扫码金额 =random()%99;比如你经常使用支付宝支付或者支付宝账户中常年没钱,那么就不需要太鼓励你去使用支付宝,那么就你扫码金额 = random()%1;总结一下:同样是扫码动作,不同的用户扫得到的不一样的红包,这也是一种多态行为。ps:支付宝红包问题纯属瞎编,大家仅供娱乐。

2. 多态的定义及实现

2.1多态的构成条件

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

那么在继承中要构成多态还有两个条件:

1.父子类完成虚函数重写

2.父类的指针或者引用去调用虚函数

用下面的等价的 ||

  1. 必须通过基类的指针或者引用调用虚函数
  2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
cpp 复制代码
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关键字时,虽然也可以构成重写(因 为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用

2.2 虚函数

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

cpp 复制代码
class Person {
public:
 virtual void BuyTicket() { cout << "买票-全价" << endl;}
};

2.3虚函数的重写

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

指针版本

cpp 复制代码
void Func(Person* p)
{
	//不同的人做一件事结果不一样
	p->BuyTicket();
}

引用版本

cpp 复制代码
void Func(Person& p)
{
	//不同的人做一件事结果不一样
	p.BuyTicket();
}

当然这样是不行的,必须是指针和引用

cpp 复制代码
void Func(Person p)
{
	//不同的人做一件事结果不一样
	p.BuyTicket();
}

虚函数重写的两个例外:

1. 协变(基类与派生类虚函数返回值类型不同)

派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。

如果把继承去掉,那么便构不成斜变,返回值不同要求父子类的指针或引用

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

普通函数下,先子后父

cpp 复制代码
class Person {
public:
	~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:
	~Student() { cout << "~Student()" << endl; }
};

int main()
{
	Person(p);
    Studend(s);
	return 0;
}

如果使用指向谁,就无法调用子类的了

那我们这样岂不是会内存泄漏

我们加上虚函数就好了

只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函 数,才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数。

这样也没有内存泄漏了

cpp 复制代码
class Person {
public:
	virtual ~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:
	virtual ~Student()
	{
		cout << "delete[]" << _ptr << endl;
		delete[] _ptr;

		cout << "~Student()" << endl;
	}
private:
	int* _ptr = new int[10];
};

int main()
{
	Person* p1 = new Person;
	Person* p2 = new Student;
	delete p1;
	delete p2;
	return 0;
}

小贴士:建议析构函数加上virtual

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

派生类不加virtual,继承他的函数,重写他的实现。

来一个小题练练手吧:

cpp 复制代码
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;
}

再来一个吧

cpp 复制代码
class Base1 {  public:  int _b1; };
class Base2 {  public:  int _b2; };
class Derive : public Base1, public Base2 { public: int _d; };
int main(){
 Derive d;
 Base1* p1 = &d;
 Base2* p2 = &d;
 Derive* p3 = &d;
 return 0;
}

这道题主要考了切片和怎么放的

我们只需要修改一个public base1 ,public base2就可以分辨出来了

2.4 C++11 override 和 final

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

1. final:修饰虚函数,表示该虚函数不能再被重写

cpp 复制代码
class Car
{
public:
 virtual void Drive() final {}
};
class Benz :public Car
{
public:
 virtual void Drive() {cout << "Benz-舒适" << endl;}
};

C++98的方法:父类的构造函数私有

子类的构造无法生成和实现,导致子类对象无法实例化

cpp 复制代码
class Car final
{
public:

private:
	//C++98的方法:父类的构造函数私有
	//子类的构造无法生成和实现,,导致子类对象无法实例化
	Car() {}
};
class Benz :public Car
{
public:

};

int main()
{
	Benz b;
	return 0;
}

final 修饰的类叫做最终类不能被继承

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

cpp 复制代码
class Car {
public:
	virtual void Drive() {}
};
class Benz :public Car {
public:
	virtual void Drive() override { cout << "Benz-舒适" << endl; }
};

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

3. 抽象类

3.1 概念

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

某种程度上强制了派生类的重写

cpp 复制代码
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()
{
	/*Benz b1;
	b1.Drive();
	BMW b2;
	b2.Drive();*/


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

3.2 接口继承和实现继承

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

4.多态的原理

4.1虚函数表

我们先看一道面试题:

cpp 复制代码
class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
private:
	int _b = 1;
};

int main()
{
	cout << sizeof(Base) << endl;
	return 0;
}

通过观察测试我们发现b对象是8bytes ,为什么是8呢???按照正常来说不应该是4吗??除了_b成员,还多一个__vfptr放在对象的前面(注意有些 平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针 (v代 表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数 的地址要被放到虚函数表中,虚函数表 也简称虚表。那么派生类中这个表放了些什么呢?我们接着往下分析~~~~~

通过观察和测试,我们发现了以下几点问题:

  1. 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在部分的另一部分是自己的成员。
  2. 基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表 中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数 的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
  3. 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函 数,所以不会放进虚表。
  4. 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。
  5. 总结一下派生类的虚表生成:
  • a.先将基类中的虚表内容拷贝一份到派生类虚表中
  • b.如果派生 类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数
  • c.派生类自己 新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。

这里还有一个童鞋们很容易混淆的问题:虚函数存在哪的?虚表存在哪的? 答:虚函数存在 虚表,虚表存在对象中。注意上面的回答的错的。但是很多童鞋都是这样深以为然的。注意 虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是 他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。那么虚表存在哪的 呢?

4.2多态的原理

上面分析了这个半天了那么多态的原理到底是什么?还记得这里Func函数传Person调用的 Person::BuyTicket,传Student调用的是Student::BuyTicket

cpp 复制代码
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 Mike;
	Func(Mike);
	Student Johnson;
	Func(Johnson);
	return 0;
}

满足多态条件

那么这里调用生成的指令,就会指向对象的虚表中对应虚函数进行调用

运行时决定调用谁

指向父类调用父类的虚函数

执行子类调用子类的虚函数

打印虚函数表
cpp 复制代码
typedef void(*VFPTR)();

void PrintVFT(VFPTR* vft)
{
	for (size_t i = 0; i < 4; i++)
	{
		printf("%p\n", vft[i]);

		VFPTR pf = vft[i];
		(*pf)();
	}
}
class Base {
public:
	virtual void func1() { cout << "Base::func1" << endl; }
	virtual void func2() { cout << "Base::func2" << endl; }
private:
	int a;
};
class DeriveDerive :public Base {
public:
	virtual void func1() { cout << "Derive::func1" << endl; } //func1重写了
	virtual void func3() { cout << "Derive::func3" << endl; } //没有重写
	virtual void func4() { cout << "Derive::func4" << endl; } //没有重写
private:
	int b;
};
int main()
{
	Base b;
	DeriveDerive dd;

	VFPTR* ptr = (VFPTR*)(*((int*)&dd));
	PrintVFT(ptr);
	return 0;
}
相关推荐
逊嘘9 分钟前
【Java语言】抽象类与接口
java·开发语言·jvm
van叶~12 分钟前
算法妙妙屋-------1.递归的深邃回响:二叉树的奇妙剪枝
c++·算法
Half-up12 分钟前
C语言心型代码解析
c语言·开发语言
knighthood200122 分钟前
解决:ros进行gazebo仿真,rviz没有显示传感器数据
c++·ubuntu·ros
Source.Liu33 分钟前
【用Rust写CAD】第二章 第四节 函数
开发语言·rust
monkey_meng34 分钟前
【Rust中的迭代器】
开发语言·后端·rust
余衫马37 分钟前
Rust-Trait 特征编程
开发语言·后端·rust
monkey_meng40 分钟前
【Rust中多线程同步机制】
开发语言·redis·后端·rust
Jacob程序员42 分钟前
java导出word文件(手绘)
java·开发语言·word
小白学大数据1 小时前
正则表达式在Kotlin中的应用:提取图片链接
开发语言·python·selenium·正则表达式·kotlin