首页 > 编程语言 >C++:类与对象——详解多态原理、虚函数和抽象类

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

时间:2024-09-11 18:20:08浏览次数:3  
标签:调用 vtable 函数 多态 C++ 基类 抽象类 speak

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; 还有:类名::~类名(){}

总结

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

标签:调用,vtable,函数,多态,C++,基类,抽象类,speak
From: https://blog.csdn.net/qq_45855825/article/details/142147089

相关文章

  • 南沙C++信奥老师解一本通题: 1315:【例4.5】集合的划分
    ​ 【题目描述】【输入】给出n和k。【输出】n个元素a1,a2,……,an放入k个无标号盒子中去的划分数S(n,k)。【输入样例】106 【输出样例】22827 #include<iostream>usingnamespacestd;longlongSplit(intn,intplate)//等同于n个不同的数......
  • C++模拟实现stack和queue(容器适配器)
    适配器是一种设计模式(设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结),该种模式是将一个类的接口转换成客户希望的另外一个接口。简单理解,将模板参数给成容器,就是容器适配器,写成参数的容器的各种接口,均满足需要。#include<list>#includ......
  • 走进C++——初识与探索
    一.C++发展历史  C++的起源可以追溯到1979年,当时BjarneStroustrup(本贾尼·斯特劳斯特卢普)在⻉尔实验室从事计算机科学和软件⼯程的研究⼯作。⾯对项⽬中复杂的软件开发任务,特别是模拟和操作系统的开发⼯作,他感受到了现有语⾔(如C语⾔)在表达能⼒、可维护性和可扩展性......
  • C++ 类继承
    一、继承1.继承的概念和意义继承机制是面向对象程序设计使代码可以复用的最重要的手段,它允许我们在保持原有类特性的基础上进行扩展,产生新的类,称子类。例如下面的Student类和Teacher类,它们都有人名,性别,电话,年龄,住址等信息。不同的是Student类有学号之分,而老师有职称之......
  • 中国电科网安校园招聘:C++工程师
      本文介绍2024届秋招中,中国电子科技网络信息安全有限公司的C/C++开发工程师岗位一面的面试基本情况、提问问题等。  2024年10月投递了中国电子科技网络信息安全有限公司的C/C++开发工程师岗位,并不清楚所在的部门。目前完成了一面,在这里记录一下一面经历。  这一次面试面......
  • C++最强功能之一指针
    最是人间留不住,朱颜辞镜花辞树。                            ——《蝶恋花·阅尽天涯离别苦》【清】王国维今天我们来说一说这个C++区别其他语言最明显的功能,指针。C++的指针可以说是功能强大,很多游戏的外挂中核......
  • C++ 不要将有符号整数和无符号整数相加
    一有符号整数和无符号整数相加时,把负数转换成无符号数类似于直接给无符号数赋一个负值,结果等于这个负数加上无符号数的模。unsignedintn=300;intm=-500;cout<<m+m<<'\n';cout<<n+m<<'\n';输出:-1000//正确4294967096//错误结果做个类型......
  • 南沙C++信奥老师解一本通题:1203:扩号匹配问题
    ​【题目描述】在某个字符串(长度不超过100)中有左括号、右括号和大小写字母;规定(与常见的算数式子一样)任何一个左括号都从内到外与在它右边且距离最近的右括号匹配。写一个程序,找到无法匹配的左括号和右括号,输出原来字符串,并在下一行标出不能匹配的括号。不能匹配的左括号用"$"标......
  • C++题目收集2
    这是本专栏的的第二篇收录集,我们一起来看一看那些有意思的题目,拓宽自己的思路。本期的题目有一些难,所以数目少一点。题目一:约瑟夫环#include<iostream>#include<vector>intjosephus(intn,intm){std::vector<int>people(n);for(inti=0;i<n;++i)......
  • 《C++ Primer Plus》学习day3
    C++11新增的内容:char16_t和char32_tchar16_t:无符号,16位,使用前缀u表示char_16字符常量和字符串常量;char32_t:无符号,32位,使用前缀U表示char32_t常量浮点类型C++有三种浮点类型:float、double、longdouble头文件cfloat中对对浮点数进行了限制:比如最低有效位......