栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > C/C++/C#

C++:多态 详解

C/C++/C# 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

C++:多态 详解

目录

一.多态的概念

二.多态的定义及实现

1.重写/覆盖 的要求

2.多态两个要求:

3.多态的切片示意图

4.多态演示:

买票场景下的多态 完整代码

5.虚函数重写的例外:

协变(父类与子类虚函数返回值类型不同)

6.接口继承和实现继承

多态的坑题目(考接口继承)

7.析构函数名统一会被处理成destructor()

8.C++11 override 和 final

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

10.抽象类

三.多态的原理

1.虚函数表

2.虚表存储

(1)虚函数重写/覆盖 语法与原理层解释

(2)虚表存储解释

多态调用和普通调用底层解释

(3)父类赋值给子类对象,也可以切片。为什么实现不了多态?


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

二.多态的定义及实现

1.重写/覆盖 的要求 重写/覆盖: 子类的虚函数重写了基类的虚函数 要求:子类中有一个跟父类完全相同的虚函数

即:子类父类都有这个虚函数 + 子类的虚函数与父类虚函数的 函数名/参数/返回值 都相同 -> 重写/覆盖(注意:参数只看类型是否相同,不看缺省值)

2.多态两个要求:

 1、被调用的函数必须是虚函数,子类对父类的虚函数进行重写 (重写:三同(函数名/参数/返回值)+虚函数)
 2、父类指针或者引用去调用虚函数。

3.多态的切片示意图

(1)示例1:给一个student的子类对象(临时对象也行),然后把这个对象赋给一个父类指针,通过这个父类指针就可以访问student子类的虚拟函数

(2)示例2:假设B是子类,A是父类,new一个B类的临时对象,然后把这个临时对象赋给一个父类指针A* p2,通过这个父类指针p2就可以访问子类B的虚拟函数func

 

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*p1 = new B;
	//p1->test();	这个是多态调用,下有讲解 二->6
	p1->func();	//普通调用

	A*p2 = new B;
	p2->func();	//多态调用

	return 0;
}

4.多态演示:
class Person {
public:
	Person(const char* name)
		:_name(name)
	{}

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

protected:
	string _name;
	//int _id;
};

class Student : public Person {
public:
	Student(const char* name)
		:Person(name)
	{}

	// 虚函数 + 函数名/参数/返回值 -》 重写/覆盖
	virtual void BuyTicket() 
    { 
        cout << _name << " Student:买票-半价 50 ¥" << endl; 
    }
};

void Pay(Person& ptr)
{
	ptr.BuyTicket();
}

int main()
{
        string name;
		cin >> name;
		Student s(name.c_str());
		Pay(s);
}

买票场景下的多态 完整代码 普通人 买票时,是全价买票; 学生 买票时,是半价买票; 军人 买票时是优先买票。
class Person {
public:
	Person(const char* name)
		:_name(name)
	{}

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

protected:
	string _name;
	//int _id;
};

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; }
};

// 多态两个要求:
// 1、子类虚函数重写的父类虚函数 (重写:三同(函数名/参数/返回值)+虚函数)
// 2、父类指针或者引用去调用虚函数。

//void Pay(Person* ptr)
//{
//	ptr->BuyTicket();
//}

void Pay(Person& ptr)
{
	ptr.BuyTicket();
}

// 不能构成多态
//void Pay(Person ptr)
//{
//	ptr.BuyTicket();
//}

int main()
{
	int option = 0;
	cout << "=======================================" << endl;
	do 
	{
		cout << "请选择身份:";
		cout << "1、普通人 2、学生 3、军人" << endl;
		cin >> option;
		cout << "请输入名字:";
		string name;
		cin >> name;
		switch (option)
		{
		case 1:
		{
				  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);

	return 0;
}
解释 2、父类指针或者引用去调用虚函数,传值调用不构成多态。 用子类也不行,必须用父类,比如你用个student,那么你的Person或者Soldier就传不进形参
void Pay(Person* ptr)     //指针调用可以
{
	ptr->BuyTicket();
}

void Pay(Person& ptr)    //引用调用可以
{
	ptr.BuyTicket();
}

// 不能构成多态
//void Pay(Person ptr)    //传值调用不可以
//{
//	ptr.BuyTicket();
//}

5.虚函数重写的例外:

协变(父类与子类虚函数返回值类型不同) 子类重写父类虚函数时,与父类虚函数返回值类型不同 称为协变。

虚函数重写对返回值要求有一个例外:协变,协变是子类虚函数与父类虚函数返回值类型不同,但子类和父类的返回值类型也必须是父子关系指针和引用。

子类虚函数没有写virtual,f依旧是虚函数,因为子类先继承了父类函数接口声明(接口部分是virtual A* f()  ),重写是重写父类虚函数的实现部分( 重写函数实现部分是用子类虚函数的{ }里面的函数实现替代父类虚函数的{ }里面的函数实现 )        ps:我们自己写的时候子类虚函数也写上virtual

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,f依旧时虚函数,因为先继承了父类函数接口声明
	// 重写父类虚函数实现
	// ps:我们自己写的时候子类虚函数也写上virtual
	// B& f() { 
	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;
}

6.接口继承和实现继承 普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实 现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成 多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。 所以就有了 子类虚函数没有写virtual,依旧是虚函数;子类虚函数使用的是父类虚函数的缺省参数,只是重写了实现

多态的坑题目(考接口继承) 子类虚函数没有写virtual,func 依旧是虚函数,因为子类先继承了父类函数接口声明(接口部分是virtual A* f()  ),重写是重写父类虚函数的实现部分( 重写函数实现部分是用子类虚函数的{ }里面的函数实现替代父类虚函数的{ }里面的函数实现 )        ps:我们自己写的时候子类虚函数也写上virtual

p->test(),调用test中的this指针类型是A*,但指向的是对象B* p中的内容,类B中继承的test函数中又调用func函数,func函数没有写virtual 但依旧是虚函数,只要是虚函数重写就是接口继承,子类先继承了父类函数接口声明(父类接口部分是virtual void func(int va1=1) ),重写是重写父类虚函数的实现部分( 即使用子类的函数的实现部分{}内容 ),所以缺省函数用的是父类的1,实现用的子类的函数实现,打印结果是 B->1

7.析构函数名统一会被处理成destructor() 只有派生类 Student 的析构函数重写了 Person 的析构函数,下面的 delete 对象调用析构函 数,才能构成多态,才能保证 p1 和 p2 指向的对象正确的调用析构函数。 函数名处理成destructor() 才能满足多态: 如果父类的析构函数为虚函数,此时子类析构函数只要定义,无论是否加virtual关键字, 都与父类的析构函数构成重写,虽然父类与子类析构函数名字不同。虽然函数名不相同, 看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处 理,编译后析构函数的名称统一处理成destructor。
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; 
}

 2.注意:期望delete ptr调用析构函数是一个多态调用, 如果设计一个类,可能会作为基类,其次析构函数最好定义为虚函数
class Person {
public:
	virtual ~Person()
	{
		cout << "~Person()" << endl;
	}
};

class Student : public Person {
public:
	// Person析构函数加了virtual,关系就变了
	// 重定义(隐藏)关系 -> 重写(覆盖)关系
	virtual ~Student()    //这里virtual加不加都行
	{
		cout << "~Student()" << endl;
		delete[] _name;
		cout << "delete:" << (void*)_name << endl;
	}

private:
	char* _name = new char[10]{ 'j','a','c','k' };
};

int main()
{
	// 对于普通对象是没有影响的
	//Person p;
	//Student s;

	// 期望delete ptr调用析构函数是一个多态调用
	// 如果设计一个类,可能会作为基类,其次析构函数最好定义为虚函数
	Person* ptr = new Person;
	delete ptr; // ptr->destructor() + operator delete(ptr)

	ptr = new Student;
	delete ptr;  // ptr->destructor() + operator delete(ptr)

	return 0;
}

8.C++11 override 和 final (1)final :修饰虚函数,表示该虚函数不能再被重写;修饰类,该类不能被继承

 (2)override:override写在子类中,要求严格检查是否完成重写,如果没有完成重写就报错

 示例:如果父类没写virtual能检查出来并报错

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

10.抽象类 在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口 类),抽象类不能实例化出对象,但可以new别的对象来定义指针,例如Car* pBMW = new BMW; (1)子类继承后也不能实例化出对象,只有重写纯虚函数,子类才能实例化出对象。 (2)父类的纯虚函数强制了派生类必须重写,才能实例化出对象(跟override异曲同工,override是放在子类虛函数,检查重写。功能有一些重叠和相似​​​​​​​ ) 另外纯虚函数更体现出了接口继承。 ​​​​​​​ (3)纯虚函数也可以写实现{ },但没有意义,因为是接口继承,{ }中的实现会被重写;父类没有对象,所以无法调用纯虚函数

 抽象类  -- 在现实一般没有具体对应实体
 不能实例化出对象
 间接功能:要求子类需要重写,才能实例化出对象
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();
}

三.多态的原理

1.虚函数表

和菱形虚拟继承的虚基表不一样,那个存的是偏移量

// 这里常考一道笔试题:sizeof(Base)是多少?
class Base
{
public:
 virtual void Func1()
 {
 cout << "Func1()" << endl;
 }
private:
 int _b = 1;
};
通过观察测试我们发现b对象是8bytes,除了_b成员,还多一个__vfptr放在对象的前面(注意有些 平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代 表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数 的地址要被放到虚函数表中,虚函数表也简称虚表,。那么派生类中这个表放了些什么呢?我们 接着往下分析

2.虚表存储
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;
};

class Derive : public Base
{
public:
	virtual void Func1()
	{
		cout << "Derive::Func1()" << endl;
	}

	void Func3()
	{
		cout << "Derive::Func3()" << endl;
	}
private:
	int _d = 2;
};

int main()
{
	cout << sizeof(Base) << endl;
	Base b;

	cout << sizeof(Derive) << endl;
	Derive d;

	Base* p = &b;
	p->Func1();
	p->Func3();
	
	p = &d;
	p->Func1();
	p->Func3();

//	
}

(1)虚函数重写/覆盖 语法与原理层解释

--语法层的概念: 派生类对继承基类虚函数实现进行了重写
--原理层的概念: 子类的虚表,拷贝父类虚表进行了修改,覆盖重写那个虚函数

(2)虚表存储解释

多态调用和普通调用底层解释

 

重点总结:
多态调用:运行时决议-- 运行时确定调用函数的地址(不管对象类型,查对应的虚函数表,如果是父类的对象,就查看父类对象中存的虚表;如果是子类切片后的对象,就查看子类切片后对象中存的虚表)
普通调用:编译时决议-- 编译时确定调用函数的地址(只看对象类型去确定调用哪个对象中的函数)

(3)父类赋值给子类对象,也可以切片。为什么实现不了多态?

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;
};

class Derive : public Base
{
public:
	virtual void Func1()
	{
		cout << "Derive::Func1()" << endl;
	}

	void Func3()
	{
		cout << "Derive::Func3()" << endl;
	}
private:
	int _d = 2;
};

int main()
{
	cout << sizeof(Base) << endl;
	Base b;

	cout << sizeof(Derive) << endl;
	Derive d;


	// 父类赋值给子类对象,也可以切片。为什么实现不了多态?
	Base r1 = b;
	r1.Func1();
	r1.Func3();

	Base r2 = d;
	r2.Func1();
	r2.Func3();

	return 0;
}

我们发现r2没有拷贝子类d的虚表,则r2虚表中存的还是父类的虚表,调用时还是调用父类的func1,而不是子类切片后的func1

(4)静态和动态的多态(了解)

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/1033114.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号