【C++】多态

多态

1、多态的概念

多态的概念:通俗来说,就是多种形态,即去完成某个行为时,不同的对象去完成时会产生不同的形态

举个例子:当普通人买票时,是全价买票;学生买票时,是优惠买票(5折或75折);军人买票时是优先买票。再举个例子,同样是动物叫的⼀个行为(函数),传猫对象过去,就是"喵",传狗对象过去,就是"汪汪"。

2、多态的定义及实现

2.1 多态的构成条件

多态是一个继承关系的类对象,去调用同一函数,产生了不同的行为

比如Student继承了Person。Person对象买票全价,Student对象买票半价

实现多态有两个必须重要条件:

  • 必须通过基类的指针或者引用调用虚函数
  • 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
cpp 复制代码
class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
};

class Student : public Person {
public:
	virtual void BuyTicket() { cout << "买票-打折" << endl; }
};

void Func(Person& people)
{
	// 这里可以看到虽然都是Person指针People在调用BuyTicket
	// 但是跟people没关系,而是由people指向的对象决定的。
	people.BuyTicket();
}

int main()
{
	Person Mike;
	Student Johnson;

	Func(Mike);
	Func(Johnson);

	return 0;
}

输出结果:

买票-全价

买票-打折

2.2 虚函数

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

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

注意:只能是类的成员函数才能变成虚函数,全局函数和静态函数都不行

2.3虚函数的重写

虚函数的重写/覆盖:派生类中有⼀个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称派生类的虚函数重写了基类的虚函数。

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

class Student : public Person {
public:
	virtual void BuyTicket() { cout << "买票-打折" << endl; }
};

// 基类的virtual不可以去掉,派生类的virtual可以去
void Func(Person& people)
{
	people.BuyTicket();
}

int main()
{
	Person Mike;
	Student Johnson;
	
	Func(Mike);
	Func(Johnson);

	return 0;
}

重写基类虚函数是,派生类的虚函数在不加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();  //重写用的是virtual void func(int val = 1) + { std::cout << "B->" << val << std::endl; }

	return 0;
}
  1. 执行p->test();先在B类中找test(),B类没有重写test,向上找父类A中的test函数,最终执行A类的test函数
  2. test内部代码:func(),编译器自动把无参调用补全为this→func()
  3. 执行this→func()时:由于p是B的指针所指的对象,所以this是B,由于构成多态时重写虚函数的实现,所以最后结果是virtual void func(int val = 1) + { std::cout << "B->" << val << std::endl; }
  4. 输出B→1

注意

  • 继承并不是把基类的成员拷贝一份到派生类中
  • 编译时,派生类对象调用成员函数是先在派生类中找,找不到再到基类中找
  • 重写的本质:构成多态时,是重写虚函数的实现。函数的实现是指函数体,不包括接口。所以虚函数A和函数B构成重写后函数接口是基类A的接口(virtual void func(int val = 1)),函数体是派生类B({ std::cout << "B->" << val << std::endl; }),这也是为什么派生类函数前不需要加virtual关键字

补充:如果是p->func();,则输出B→0

2.4 虚函数重写的两个意外

2.4.1 协变

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

cpp 复制代码
class A {};
class B : public A {};  //B是A的派生类

class Person {
public:
	virtual A* BuyTicket()
	{
		cout << "买票-全价" << endl;
		return nullptr;
	}
};

class Student : public Person {
public:
	virtual B* BuyTicket()
	{
		cout << "买票-打折" << endl;
		return nullptr;
	}
};

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

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

注意:协变的前提是B公有继承A,这样编译器才能认定这是合法重写

如果返回两个毫无关系的类指针就不算协变,例如:

cpp 复制代码
// 父类返回A*,子类返回和A无关的C*,报错,不算重写
virtual C* BuyTicket(){}

2.4.2 析构函数的重写

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

class B : public A {
public:
	~B()
	{
		cout << "~B()->delete:" << _p << endl;
		delete _p;
	}
protected:
	int* _p = new int[10];
};

int main()
{
	/*A a1;
	B b1;*/

	A* p1 = new A;
	A* p2 = new B;

	// 多态调用,指向谁,调用谁的析构函数
  //因为名字的原因,所以析构函数名字改成destructor
	// p1->destructor() + operator delete(p1)
	delete p1;
	delete p2;

	return 0;
}

上面的代码可以看到,如果~A不加virtual,那么delete p2时只调用A的析构函数,因为两个函数的函数名不同,不能构成覆盖

但如果基类的析构函数为虚函数,加上virtual关键字,此时派生类析构函数只要定义 ,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同

因为这里编译器对析构函数的名称做了特殊处理,析构函数的名称统一处理成destructor

2.5 override和final

2.5.1 override

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

cpp 复制代码
class Car
{
public:
	virtual void Drive()
	{}
};

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

2.5.2 final

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

cpp 复制代码
class Car
{
public:
	virtual void Drive() final //不能被重写,子类重写会报错
	{}
};
class Benz :public Car
{
public:
	virtual void Drive() 
	{ 
		cout << "Benz-舒适" << endl; 
	}
};

2.6 重载/重写/隐藏的对比

3、纯虚函数和抽象函数

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

cpp 复制代码
class Car
{
public:
	// 纯虚函数
	// 1、抽象类不能实例化对象
	// 2、强制子类重写
	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;
	}
};

int main()
{
	//Car car;  //错误,抽象类不能实例化对象

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

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

	return 0;
}

输出结果:

Benz-舒适

BMW-操控

4、多态的原理

4.1 虚函数表

cpp 复制代码
class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
	
int main()
{
	Base b;
	cout << sizeof(b) << endl;

	return 0;
}

sizeof(b)的值是多少?

输出结果:12

所以除了_b成员(4字节),_ch成员(1字节),根据内存对齐,还多了一个_vfptr是4字节,_vfptr本质是一个指针,叫做虚函数表指针。一个含有虚函数的类中都至少有一个虚函数表指针,虚函数的地址要被放到虚函数白表中,虚函数也简称虚表

简单来说,虚表是存放当前类所有的虚函数地址的数组

4.2 派生类对象中的虚函数表

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

class Student : public Person {
public:
	virtual void BuyTicket() { cout << "买票-打折" << endl; }
	virtual void Func1() {}  //重写父类Fun1
	virtual void Func3() {}  //子类新增虚函数,追加到虚表末尾
private:
	string _id;
};

class Soldier : public Person {
public:
	virtual void BuyTicket() { cout << "买票-优先" << endl; }
	//没有重写Func1,也没有新增虚函数,仅覆盖BuyTicket
	//Func1没有重写,保留父类的函数地址
private:
	string _codename;
};

void Func(Person* ptr)
{
	// 这里可以看到虽然都是Person指针Ptr在调用BuyTicket
	// 但是跟ptr没关系,而是由ptr指向的对象决定的。
	ptr->BuyTicket();
}

int main()
{
	// 其次多态不仅仅发生在派生类对象之间,多个派生类继承基类,重写虚函数后
	// 多态也会发生在多个派生类之间。
	Person ps;
	Student st;
	Soldier sr;

	Func(&ps);
	Func(&st);
	Func(&sr);

	Person p1;
	Person p2;

	return 0;
}

Person类的虚表

Person_vtable0 = &Person::BuyTicket

Person_vtable1 = &Person::Func1

Student类虚表

Student_vtable0 = &Student::BuyTicket 重写覆盖父类同位置函数

Student_vtable1 = &Student::Func1 重写覆盖父类同位置函数

Student_vtable2 = &Student::Func3 子类新增虚函数,追加到表末尾

Soldier类虚表

Soldier_vtable0 = &Soldier::BuyTicket 重写覆盖父类BuyTicket

Soldier_vtable1 = &Person::Func1 未重写Func1,沿用父类函数地址

派生类虚表的生成

  1. 先将基类中的虚表内容拷贝一份到派生类虚表中
  2. 如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数
  3. 派生类自己新增的虚函数按其在派生类中的声明次序增加到派生类虚表的最后
  4. 同类型的对象共用同⼀张虚表,不同类型的对象各自有独立的虚表,所以基类和派生类有各自独立的虚表

4.3 多态的原理

满足多态条件后,底层运行时,基类指针指向哪个对象就去这个对象的虚函数表中找到对应的虚函数地址进行调用,当指针指向基类,编译器就会到基类的虚表中找虚函数,指向派生类是就会到派生类的虚表中找虚函数

满足多态,BuyTicket是虚函数,运行时到指向对象虚函数表中找到对应函数调用

不满足多态,BuyTicket不是虚函数,编译链接时就确定函数的地址

注意:若虚函数重写了,则基类和派生类的函数地址不相同

若虚函数未重写,则基类和派生类的函数地址内容都相同

4.4 动态绑定与静态绑定

  • 对不满足多态条件(指针或者引用+调用虚函数)的函数调用是在编译时绑定,也就是编译时确定调用函数的地址,叫做静态绑定
  • 满足多态条件的函数调用是在运行时绑定,也就是在运行时到指向对象的虚函数表中找到调用函数的地址,也就做动态绑定

4.5 虚表的存储位置

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

class Student : public Person {
public:
	virtual void BuyTicket() { cout << "买票-打折" << endl; }
	virtual void Func1() {}
	virtual void Func3() {}
private:
	string _id;
};

class Soldier : public Person {
public:
	virtual void BuyTicket() { cout << "买票-优先" << endl; }
private:
	string _codename;
};

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

int main()
{
	int i = 0;
	static int j = 1;
	int* p1 = new int;
	const char* p2 = "xxxxxxxx";
	printf("栈:%p\n", &i);
	printf("静态区:%p\n", &j);
	printf("堆:%p\n", p1);
	printf("常量区:%p\n", p2);

	Person p;
	Person* ptr = &p;
	Student s;
	printf("虚表:%p\n", *((int*)ptr));
	printf("虚表:%p\n", *((int*)&s));

	printf("虚函数地址:%p\n", &Person::Func1);
	printf("普通函数地址:%p\n", &Person::Func2);

	return 0;
}

输出结果:

可以看出,虚表的地址与常量区最为接近,所以猜测虚表就是在常量区

如果文章中有错误或不足,欢迎大家指正交流。

相关推荐
rebibabo1 小时前
Java高并发底层原理(七)volatile 到底解决了什么问题
java·学习笔记·volatile·可见性·有序性·指令重排
名字还没想好☜1 小时前
Go 并发实战:用 channel 实现 worker pool
java·数据库·后端·golang·go
人道领域2 小时前
【LeetCode刷题日记】贪心算法理论与实战:455.分发饼干最优解
java·开发语言·数据结构·算法·leetcode·贪心算法
_abab2 小时前
Java面试宝典:从基础到架构2
java·面试·架构
会周易的程序员2 小时前
microLog 的 log_reader 架构解析:嵌入式日志检索引擎的设计之道
c++·物联网·架构·日志·iot·aiot
万亿少女的梦1682 小时前
基于Spring Boot与Vue的繁星技术论坛系统设计与实现
java·spring boot·mysql·vue·系统设计
阿pin2 小时前
Android随笔-神经系统Handler
android·java·开发语言·handler
汉克老师2 小时前
GESP2026年6月认证C++七级( 第一部分选择题(8-15))精讲
c++·dfs·bfs·哈希·二分算法·gesp7级·网格dp