C++:类与对象——详解多态原理、虚函数和抽象类

1. 多态基本内容

C++ 中的多态是面向对象编程的一个重要特性,指的是同一个函数或对象在不同的情况下可以表现出不同的行为。多态通常通过继承和虚函数来实现。它分为两种类型:编译时多态(静态多态)运行时多态(动态多态)

多态分为两类

  • 静态多态函数重载运算符重载属于静态多态,复用函数名
  • 动态多态派生类虚函数实现运行时多态

静态多态和动态多态的区别

  • 静态多态的函数地址早绑定 - 编译阶段确定函数地址
  • 动态多态的函数地址晚绑定 - 运行阶段确定函数地址

动态多态满足条件

  1. 继承关系
  2. 子类要重写父类

动态多态使用:父类的指针或者引用指向子类对象

2. 多态原理

在C++中,多态的底层原理主要依赖于虚函数表(Virtual Table, vtable)虚函数指针(Virtual Table Pointer, vptr)。当类中包含虚函数时,编译器会为类生成虚函数表,并为每个对象分配一个指向该虚函数表的指针。这种机制在运行时通过虚函数表来动态决定调用哪个函数,从而实现多态。

虚函数表(vtable)

  • vtable是编译器为每个包含虚函数的类生成的一个隐藏的数据结构。它实际上是一个指针数组,存储了类中所有虚函数的地址。
  • 每个含有虚函数的类都有自己的vtable。如果某个类继承了基类并重写了基类中的虚函数,那么派生类的vtable中将存储派生类的虚函数实现的地址,而非基类的实现

虚函数指针(vptr)

  • 每个包含虚函数的对象都拥有一个隐藏的指针(vptr),这个指针指向该对象所属类的vtable。
  • 当对象被创建时,vptr被初始化,指向该对象所属类的vtable。

运行时多态的实现过程

当我们通过基类指针或引用调用虚函数时,编译器会在运行时通过vptr找到该对象的vtable,然后从vtable中找到对应虚函数的地址并进行调用。这就是运行时动态绑定的过程。

  1. 对象创建时:
    • 对象中包含一个vptr,它被指向所属类的vtable。
  2. 调用虚函数时:
    • 程序会通过vptr查找vtable。
    • 根据vtable找到实际要调用的虚函数地址。
    • 调用实际函数,实现多态。

动态绑定的开销:使用虚函数和多态机制引入了额外的运行时开销

  • 内存开销:每个对象需要存储一个指向vtable的vptr。
  • 性能开销:每次调用虚函数都需要通过vptr进行间接查找,而非直接调用函数。

总结:多态的底层原理依赖于虚函数表(vtable)和虚函数指针(vptr)。在编译时,编译器为每个包含虚函数的类生成vtable,并在对象中添加vptr。在运行时,通过vptr指向的vtable实现动态绑定,从而实现多态。这种机制虽然灵活,但也带来了一定的性能开销。

3. 代码示例多态原理

#include<iostream>
using namespace std;
class Animal {
public:
	virtual void speak() {
		cout << "动物在说话" << endl;
	}
};
class Cat : public Animal {
public:
	void speak() {
		cout << "猫在说话" << endl;
	}
};
class Dog : public Animal {
public:
	void speak() {
		cout << "狗在说话" << endl;
	}
};
void doSpeak(Animal& animal) {
	animal.speak();
}

int main()
{
	Cat cat;
	doSpeak(cat);
	Dog dog;
	doSpeak(dog);
	system("pause");
	return 0;
}

这段代码通过虚函数实现了C++中的运行时多态。下面详细叙述其实现过程:

3.1 类结构与继承

  • 代码中有一个基类 Animal,它包含一个虚函数 speak(),表示动物发出声音的行为。
  • CatDog 是从 Animal 类继承的派生类,它们分别重写了基类的 speak() 函数,提供了自己的实现。

3.2 虚函数的作用

  • 在基类 Animal 中,speak() 被声明为虚函数virtual 关键字),这意味着派生类可以重写该函数,而当通过基类指针或引用调用该函数时,会根据对象的实际类型调用相应的重写函数。这就是运行时多态

3.3 多态的实现过程

3.3.1 对象创建时

  • Cat 对象和 Dog 对象在 main() 函数中被创建时:
    • 编译器会为 CatDog 类分别生成虚函数表(vtable),表中记录它们重写的 speak() 函数的地址。
    • 每个 CatDog 对象会有一个虚函数指针(vptr),指向其对应类的虚函数表。

3.3.2 调用 doSpeak() 时的多态行为

  • main() 函数中,通过 doSpeak(Animal& animal),使用基类 Animal 的引用调用了不同对象的 speak() 函数:
    • doSpeak(cat) 被调用时:
      • 传入的 catCat 类型的对象,但它通过 Animal& animal 基类引用传递。
      • 由于 speak() 是虚函数,编译器会根据传入对象的实际类型(Cat)查找 Cat 类的虚函数表,找到 Cat::speak() 函数的地址,并调用 Cat 类的 speak() 函数。
      • 因此,输出为 “猫在说话”
    • doSpeak(dog) 被调用时:
      • 传入的 dogDog 类型的对象,但它同样通过 Animal& animal 基类引用传递。
      • 在运行时,编译器根据 dog 的实际类型(Dog),查找 Dog 类的虚函数表,找到 Dog::speak() 的地址,调用 Dog 类的 speak() 函数。
      • 因此,输出为 “狗在说话”

3.3.3 总结

  • 通过基类的引用 Animal& 来调用 speak(),编译器在运行时根据实际对象类型(CatDog)来决定调用哪个重写的函数。
  • 这种动态决策的过程就是运行时多态,它是通过虚函数表(vtable)和虚函数指针(vptr)机制来实现的。

3.4 输出结果

代码的结果:

猫在说话
狗在说话

4. 多态特点

优点

  1. 代码组织结构清晰
  2. 可读性强
  3. 利于前期和后期的扩展以及维护

示例:用多态实现计算器

#include<iostream>
#include<string>
using namespace std;

class Calculator {   //计算器类
public:
	virtual int getResult() {  //得到计算结果,用virtual修饰
		return 0;
	}
	int m_A;
	int m_B;
};

class AddCalculator : public Calculator {  //加法类继承计算器类并重写getResult方法
public:
	int getResult() {
		return m_A + m_B;
	}
};

class SubCalculator : public Calculator {   减法类继承计算器类并重写getResult方法
public:
	int getResult() {
		return m_A - m_B;
	}
};

int main() {
	Calculator* c = new AddCalculator;   //实例化加法类,用父类指针指向
	c->m_A = 10;  //赋初值
	c->m_B = 10;  //赋初值
	cout<<"加法运算结果为:"<<c->getResult()<<endl;
	delete c;  //释放堆内存空间
	c = new SubCalculator;   //实例化减法类,用父类指针指向
	c->m_A = 10;
	c->m_B = 10;
	cout << "减法运算结果为:" << c->getResult() << endl;
    delete c;  //释放堆内存空间
}

5. 纯虚函数和抽象类

在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容

因此可以将虚函数改为纯虚函数

纯虚函数语法:virtual 返回值类型 函数名 (参数列表) = 0;

当类中有了纯虚函数,这个类也称为抽象类

抽象类特点

  • 无法实例化对象
  • 子类必须重写抽象类中的纯虚函数,否则也属于抽象类

6. 虚析构和纯虚析构

多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码。具体而言就是在使用多态时,如果通过基类指针引用来指向派生类对象,调用 delete 操作符时,只会调用基类的析构函数,不会调用派生类的析构函数。这样一来,如果派生类中有动态分配的资源,这些资源将无法正确释放,导致内存泄漏

解决方法:将父类中的析构函数改为虚析构或者纯虚析构

虚析构和纯虚析构共性

  • 可以解决父类指针释放子类对象
  • 都需要有具体的函数实现

区别

  • 如果是纯虚析构,该类属于抽象类,无法实例化对象

虚析构语法virtual ~类名(){}

纯虚析构语法virtual ~类名() = 0; 还有:类名::~类名(){}

总结

  • 如果类中存在虚函数,并且你打算通过基类指针来操作派生类对象,必须将基类的析构函数声明为虚函数,以确保删除基类指针时,能够调用派生类的析构函数。
  • 否则,派生类的析构函数不会被调用,可能会导致堆区资源无法正确释放,产生内存泄漏
  • 虚析构或纯虚析构就是用来解决通过父类指针释放子类对象
  • 如果子类中没有堆区数据,可以不写虚析构或纯虚析构
  • 拥有纯虚析构函数的类也属于抽象类
  • 21
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值