当前位置: 首页 > article >正文

C++ ——— 多态的概念及其原理和实现

目录

多态的概念

多态的定义及其实现

触发多态的条件

小结

面试题

抽象类

虚函数表

多态的原理


多态的概念

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

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


多态的定义及其实现

触发多态的条件

1. 虚函数重写
继承关系中父类和子类的两个虚函数,需要虚函数的函数名、参数、返回类型都相同,才构成虚函数重写

2. 必须使用父类的指针或者引用去调用虚函数才能实现多态

代码演示:

class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "普通人买票 - 全价" << endl;
	}
};

class Student : public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "学生买票 - 半价" << endl;
	}
};

Student 类继承了 Person 类,它们各自类中的虚函数构成了重写,那么它们各自实例化后都使用父类的指针或者引用去调用,看是否会出现多态

代码演示:

void Func(Person& rp)
{
	rp.BuyTicket();
}

int main()
{
	Person p;
	Student s;

	Func(p);
	Func(s);

	return 0;
}

代码验证:

当如果破坏多态的条件,不使用父类的引用或者指针调用虚函数的话,那么就只会是普通调用

代码演示:

void Func(Person rp)
{
	rp.BuyTicket();
}

int main()
{
	Person p;
	Student s;

	Func(p);
	Func(s);

	return 0;
}

传参接收的时候并不是引用或者指针接收,而只是父类的实例化对象接收,那么结果是否会有所区别

代码验证:

可以发现,只要破坏了多态的条件之一,都会形成不了多态

小结

普通调用:调用函数的类型是谁,那么就调用这个函数的对象类型

多态调用:调用指针或者引用指向的对象,指向父类调用父类的函数,指向子类调用子类的函数 

面试题

代码演示:

class A
{
public:
	virtual void func(int val = 1)
	{
		cout << "A->" << val << endl;
	}

	void test()
	{
		func();
	}
};

class B : public A
{
public:
	virtual void func(int val = 0)
	{
		cout << "B->" << val << endl;
	}
};

int main()
{
	B* p = new B;

	p->test();

	return 0;
}

B 这个类继承了 A 这个类的两个函数,并且 B 类和 A 类中的 func() 函数构成了虚函数重写

问:代码运行的结果是?

解析:B 类型的指针 p 调用了 A 继承给 B 的 test() 函数,那么就要进入 A 类中的 test() 函数中,并且此时 test() 函数中的 this 指针是 A* 的 this ,因为 B 类只是继承了 A 类的函数,并不是把 A 类的函数拷贝到了 B 类,所以在 test() 函数内部是 A* 的 this->func();那么这样调用 func() 时就满足了多态,并且最外面的 p 指针是 B 类型的,所以结果为 B->0

代码验证:

但是打印的结果并不是预想的结果,那是因为,虽然满足了多态的条件,但是多态执行的步骤是使用父类的接口,来实现子类的虚函数,所以 val 是 1,但是打印的是 B-> 


抽象类

在虚函数的后面写上 = 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;
	}
};

Car 这个类是不能实例化出对象的,只有实例化出 Car 这个类的指针或者引用

代码验证:

小结:

1. 抽象类不能实例化对象

2. 纯虚函数的抽象类是为了间接性的强制让派生类去重写虚函数,否则继承了抽象类的派生类也不能实例化对象,只有派生类重写了父类的虚函数后才能实例化


虚函数表

代码演示:

class A
{
public:
	virtual void func()
	{
		cout << "virtual void func()" << endl;
	}

protected:
	int _a;
};

int main()
{
	A a;
	cout << sizeof(a) << endl;

	return 0;
}

根据以往对类大小的计算,猜测 _a 占 4 个字节,那么函数是不占用内存的,所以大小是 4 字节

代码验证:

由此可得虚函数是占用内存的,那么为什么是占用 4 个字节呢,从内存的角度看待问题

代码内存:

可以看到,a 对象中有个 _vfptr 指针,所以占用了 4 个字节

那么 _vfptr 指针指向的空间就是虚函数表,用来存储虚函数的地址

无论类中有一个或者多个虚函数,它们的地址都是存储在 _vfptr 所指向的虚函数表中

代码演示:

class A
{
public:
	virtual void func1()
	{
		cout << "virtual void func1()" << endl;
	}

	virtual void func2()
	{
		cout << "virtual void func2()" << endl;
	}

protected:
	int _a;
};

代码验证:

所占空间大小:

由此可见多个虚函数并不会多占用内存,都只会存储到 _vfptr 指针所指向的虚函数表中


多态的原理

代码演示:

class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "普通人买票 - 全价" << endl;
	}

	virtual void test()
	{}

protected:
	int _p;
};

class Student : public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "学生买票 - 半价" << endl;
	}

private:
	int _s;
};

void func(Person* rp)
{
	rp->BuyTicket();
}

int main()
{
	Person p;
	Student s;

	func(&p);
	func(&s);

	return 0;
}

以上是一个多态的调用,为了方便区分,我在父类中多加了一个 test() 的虚函数,那么从父子类各自的内存看是如何完成多态的

Person 类的内存:

Student 类的内存:

Student 类把 Person 类中的虚函数表继承了下来,但是 Student 类和 Person 类中的 BuyTicket() 函数的地址发生了变化,但是 test() 函数的地址并没有发生变化

这是因为 Student 类重写了虚函数,重写虚函数是代码上的变化,而重写在原理层是覆盖,也就是说 Person 类中的 BuyTicket() 函数被 Student 类重写覆盖拷贝了一份,所以才会指向不同的地址,而 Person 类中的 test() 函数没有被 Student 类重写,所以继承下来后还是原地址

所以说,Student 类虽然继承了 Person 类的虚函数表,但是重写虚函数后的地址发生了改变,各自指向了各自实现的虚函数,所以才会实现多态

代码演示:

void func(Person* rp)
{
	rp->BuyTicket();
}

int main()
{
	Person p;
	Student s;

	func(&p);
	func(&s);

	return 0;
}

再来刨析这段代码,当 func() 函数传递的实参是 Person 类的对象地址 &p 时,就会去 Person 类中找到虚函数表中对应的函数,再运行函数

当如果传递的实参是 Student 类的对象地址 &s 时,就会产生切割切片,虽然此时的 rp 还是 Person 类的 rp ,但是 rp 中的虚函数表已经被切割切片成了 Student 类的了,所以才会实现 rp 指向父类调用父类的虚函数, rp 指向子类调用子类的虚函数,这样就实现了多态


http://www.kler.cn/a/536939.html

相关文章:

  • Python调取本地MongoDB招投标数据库,并结合Ollama部署的DeepSeek-R1-8B模型来制作招投标垂直领域模型
  • 为什么要设计DTO类/什么时候设置DTO类?
  • MySQL的底层原理与架构
  • Ollama python交互:chat+embedding实践
  • 电脑运行黑屏是什么原因?原因及解决方法
  • 结合深度学习、自然语言处理(NLP)与多准则决策的三阶段技术框架,旨在实现从消费者情感分析到个性化决策
  • BGP边界网关协议(Border Gateway Protocol)选路、属性(一)
  • 使用 java -jar 命令启动 Spring Boot 应用时,指定特定的配置文件的几种实现方式
  • QWidget中嵌入QQuickWidget,从qml端鼠标获取经纬度点(double类型),发到c++端。把c++端的对象暴露个qml端调用
  • 第九天 动手实践ArkTS,创建简单的UI组件
  • BFS算法——广度优先搜索,探索未知的旅程(下)
  • 陶氏环面包络减速机:为工业视觉检测注入“精准动力”!
  • 【机器学习】深入探索SVM概念及其核方法
  • 3NF讲解
  • Web3D基础: GLTF文件材质和纹理扫盲
  • matlab simulink 117-电路故障分析
  • Day48_20250130【回校继续打卡】_单调栈part1_739.每日温度|496.下一个更大元素I|503.下一个更大元素II
  • SQL高级技巧:高效获取两表交集数据的三种方法(JOIN、IN、EXISTS)
  • Spring Cloud 01 - 微服务概述
  • “无痕模式”VS指纹浏览器,哪个更安全?
  • 自定义飞书Webhook机器人api接口
  • 【vscode源码】如何编译运行vscode及过程中问题解决
  • 在 Java 中使用 JDBC 连接数据库时,DriverManager 的主要作用是什么?请简要描述其工作原理。
  • Linux在x86环境下制作ARM镜像包
  • git代理设置
  • 65.棋盘 C#例子 WPF例子