IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> C++ 多态 | 虚函数、抽象类、虚函数表 -> 正文阅读

[C++知识库]C++ 多态 | 虚函数、抽象类、虚函数表


多态

一种事物,多种形态。换言之,对于同一个行为,不同的对象去完成就会产生不同的结果。

多态的构成条件

多态是继承体系中的一个行为,如果要在继承体系中构成多态,需要满足两个条件:

  1. 必须通过基类的指针or引用调用虚函数

  2. 被调用的函数必须是虚函数,并且派生类必须要对继承的基类的虚函数进行重写


虚函数

虚函数就是被 virtual 修饰的类成员函数 (这里的 virtual 和虚继承的 virtual 虽然是同一个关键字,但是作用不一样)。


重写

一般情况下,当派生类中有一个和基类完全相同的虚函数(函数名、返回值、参数完全相同),则说明子类的虚函数重写了基类的虚函数。

class Human
{
public:
	virtual void print()
	{
		cout << "i am a human" << endl;
	}
};

class Student : public Human
{
public:
	virtual void print()
	{
		cout << "i am a student" << endl;
	}
};

void ShowIdentity(Human &human) // 形参是基类引用,构成多态
{
	human.print(); // 被调用的函数是虚函数
}


int main()
{
	Human h;
	Student s;

	ShowIdentity(h); 
	ShowIdentity(s);
}

在这里插入图片描述
通常如果不满足函数名、返回值、参数完全相同,则不构成重写,即无法实现多态。但也有例外:


重定义(参数不同)

参数不同则会变成重定义

class Base{
public:
    virtual void Show(int n = 10)const{ // 提供缺省参数值
        std::cout << "Base:" << n << std::endl;
    }
};
 
class Base1 : public Base{
public:
    virtual void Show(int n = 20)const{ // 重新定义继承而来的缺省参数值
        std::cout << "Base1:" << n << std::endl;
    }
};
 
int main(){
    Base* p1 = new Base1;        
    p1->Show();           
 
    return 0;
}

在这里插入图片描述

如果子类重写了缺省值,此时的子类的缺省值是无效的,使用的还是父类的缺省值。

因为多态是动态绑定,而缺省值是静态绑定。

  • 对于 p1,他的静态类型即指针的类型——Base,所以这里的缺省值是 Base 的缺省值。
  • 而动态类型也就是指向的对象是 Base1,所以这里调用的虚函数则是 Base1 中的虚函数。
  • 调用了 Base1 中的虚函数,Base 中的缺省值,因此输出 Base1:10

或者可以更简单的一句话描述,虚函数的重写只重写函数实现,不重写缺省值。

动态绑定和静态绑定

  • 对象的静态类型:对象在声明时采用的类型。是在编译期确定的。(比如上面的 p1Base 是静态类型,指向的对象的类型 Base1 是动态类型)
  • 对象的动态类型:目前所指对象的类型。是在运行期决定的。

对象的动态类型可以更改,但是静态类型无法更改。

  • 静态绑定:绑定的是对象的静态类型,发生在编译期。
  • 动态绑定:绑定的是对象的动态类型,发生在运行期。

协变(返回值不同)

当基类和派生类的返回值类型不同时,如果基类对象返回基类的 引用or指针,派生类对象返回的是派生类的 引用or指针能实现多态。这样实现多态的方式叫协变

class Human
{
public:
	virtual Human& print()
	{
		cout << "i am a human" << endl;
		
		return *this;
	}
};

class Student : public Human
{
public:
	virtual Student& print()
	{
		cout << "i am a student" << endl;

		return *this;
	}
};

在这里插入图片描述
但如果返回类型不是对应类的 指针or引用,则不足以构成协变:
在这里插入图片描述


析构函数重写(函数名不同)

在特定条件下,函数名不同也能实现多态,最好的例子是析构函数,编译器为了让析构函数实现多态,会将它的名字处理成destructor ,也就是说,实际上析构函数的函数名也是“相同的”,其多态实现遵循重写的规定。

class Human
{
public:
	~Human()
	{
		cout << "~Human()" << endl;
	}
};

class Student : public Human
{
public:
	~Student()
	{
		cout << "~Student()" << endl;
	}
};

在这里插入图片描述
可以看到,如果不构成多态,那么指针是什么类型的,就会调用什么类型的析构函数。那么,如果派生类的析构函数中有资源释放的操作,而这里却没有释放掉那些资源,就会导致内存泄漏的问题。

所以为了防止这种情况,必须要将析构函数定义为虚函数。这也就是编译器将析构函数重命名为 destructor 的原因:
在这里插入图片描述


final和override

finaloverrideC++11 中提供给用户用来检测是否进行重写的两个关键字。

final

使用 final 修饰的基类虚函数不能被重写。

如果虚函数不想被派生类重写,就可以用 final 来修饰这个虚函数:
在这里插入图片描述

override

override 关键字是用来检测派生类虚函数是否构成重写的关键字。

在我们写代码的时候难免会出现些小错误,如 基类虚函数没有 virtual 或者 派生类虚函数名拼错 等问题,这些问题不会被编译器检查出来,发生错误时也很难一下子锁定,所以 C++ 增添了 override 这一层保险,当修饰的虚函数不构成重写时就会编译错误:

在这里插入图片描述


重载、重写、重定义

重载:

  1. 在同一作用域
  2. 函数名相同,参数的类型、顺序、数量不同。
  3. 重载不一定要求返回值相同:参数相同、返回值不同不构成重载;参数、返回值都不同则构成重载。(会发现返回值不同是否构成重载还是看参数相同与否……)

重写(覆盖):

  1. 作用域不同,一个在基类一个在派生类。
  2. 函数名,参数,返回值必须相同(协变和析构函数除外)
  3. 基类和派生类必须都是虚函数(派生类可以不加 virtual,基类的虚函数属性可以继承,但是最好要加上 virtual

考虑这样一个问题,下面有几个虚函数:
在这里插入图片描述

正确答案是 3 个,A 中的 fun1,B 中的 fun1fun2。原因就如第三点所说,基类的虚函数属性可以继承 ,但是如果有 C类 继承了 B类 ,且也有一个 没有virtual关键字的 void fun1(); 函数 ,该函数并不是虚函数,因为 B类fun1 并没有显式声明 virtual 属性。

而形如 fun2 这样的,子类是虚函数而父类没有 virtual 属性的,父类的 fun2 不是虚函数,虚函数不具备对称性。

重定义(隐藏):

  1. 作用域不同,一个在基类一个在派生类
  2. 函数名相同
  3. 派生类和基类同名函数如果不构成重写那就是重定义

抽象类

如果在虚函数的后面加上 =0,并且不进行实现,这样的虚函数就叫做纯虚函数。而包含纯虚函数的类,也叫做抽象类或者接口类。抽象类不能实例化出对象,因为他所具有的信息不足以描述一个对象,派生类继承后也只有在重写纯虚函数后才能实例化出对象。

抽象类就像是一个蓝图,为派生类描述好一个大概的架构,派生类必须实现完这些架构,至于要在这些架构上面做些什么,增加什么,就属于派生类自己的问题。

class Human
{
public:
	virtual void print() = 0;
};

class Student : public Human
{
public:
	virtual void print()
	{
		cout << "i am a student" << endl;
	}
};

class Teacher : public Human
{
public:
	virtual void print()
	{
		cout << "i am a teacher" << endl;
	}
};

void ShowIdentity(Human& human)
{
	human.print();
}

在这里插入图片描述

  • 普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。
  • 虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,所以如果不实现多态,不要把函数定义成虚函数。

多态的原理

虚函数

class Human
{
public:
	virtual void print()
	{
		cout << "i am a human" << endl;
	}

	virtual void test1()
	{
		cout << "1test1" << endl;
	}
	void test2()
	{
		cout << "1test1" << endl;
	}

	int _age;
};

class Student : public Human
{
public:
	virtual void print() 
	{
		cout << "i am a student" << endl;
	}

	void test2()
	{
		cout << "2test2" << endl;
	}

	int _stuNum;
};

在这里插入图片描述

我们创建一个 Human 类对象 h,观察它的大小,按理来说应该输出 4,因为它只有一个 int类型 的数据成员,但是结果却是 8

在这里插入图片描述
可以看到奇怪的是除了 _age 之外,还有个 void**(void*类型的指针,注意不是数组) 类型的 _vfptr ,这个 _vfptr 也被称为虚函数表指针,其指向了一个函数指针数组,这个函数指针数组也就是虚函数表,其中的每一个成员指针指向的都是虚函数,而不是虚函数的 test2 则没有被放入表中。

此时再创建一个 Student 类的对象 s,进一步观察:

在这里插入图片描述
我们可以看到,如果派生类实现了某个虚函数的重写,那么在派生类的虚函数表中,重写的虚函数就会覆盖掉原有的函数,如Student::print。而没有完成重写的 test1 则依旧保留着从基类继承下来的虚函数 Human::test1

总结

  • 派生类会继承基类的虚函数表,如果派生类完成了重写,则会将重写的虚函数覆盖掉原有的函数。
  • 指针或引用指向哪一个对象,就调用对象中虚函数表中对应位置的虚函数,来实现多态。

继续分析构成多态的另一个条件,为什么必须要指针或者引用才能构成多态?
在这里插入图片描述

  • 如果将派生类对象赋值给基类对象,会因为对象切割,导致他的内存布局整个被修改,完全转换为基类对象的类型,虚函数表也与基类相同,所以不能实现多态。
  • 如果用基类指针或者引用指向派生类对象,他们的内存布局是兼容的,不会像赋值一样改变派生类对象的内存结构,所以派生类对象的虚函数表得到了保留,所以他可以通过访问派生类对象的虚函数表来实现多态。

总结一下派生类虚函数表的生成过程:

  1. 首先派生类会将基类的虚函数表拷贝过来
  2. 如果派生类完成了对虚函数的重写,则用重写后的虚函数覆盖掉虚函数表中继承下来的基类虚函数
  3. 如果派生类自己又新增了虚函数,则添加在虚函数表的最后面

常见问题解析

内联函数可以是虚函数吗?

不可以,内联函数没有地址,无法放进虚函数表中。

静态成员函数可以是虚函数吗?

不可以,静态成员函数没有 this指针,无法访问虚函数表。

构造函数可以是虚函数吗?

不可以,虚函数表指针也是对象的成员之一,是在构造函数初始化列表初始化时才生成的

析构函数可以是虚函数吗?

可以,上面有写,最好把基类析构函数声明为虚函数,防止使用基类指针或者引用指向派生类对象时,派生类的析构函数没有调用,可能导致内存泄漏。

对象访问虚函数快还是普通函数快?

  • 如果不构成多态,虚函数和普通函数的访问是一样快的,都是直接在编译时符号表中找到函数的地址后调用。
  • 如果构成多态,调用虚函数就得在运行期虚函数表中查找,就会导致速度变慢,所以普通函数更快一些。

虚函数表

从上面的观察可以看出来,虚函数存于虚函数表中,那么虚函数表又存储在哪里呢?

虚函数表在编译阶段生成,存储于代码段。

详情可以看这篇博客。

注意:

  • 同一个类的不同实例(对象)共用同一份虚函数表。
  • 子类 特有的虚函数 会加在父类 虚函数表 中的 父类虚函数的后面
  • 如果子类继承多个父类、且这些父类都有虚函数表,子类特有的虚函数 加在 第一个父类的虚函数表 中。
  • 如果子类继承多个父类、但只有部分父类有虚函数表,子类特有的虚函数 加在 第一个有虚函数表的父类虚函数表 中。
  • 如果子类继承多个父类、且这些父类都没有虚函数表,子类会自己创建一个虚函数表来存储特有的虚函数。
  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2021-09-13 09:05:31  更:2021-09-13 09:07:22 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年12日历 -2024/12/28 13:26:19-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码
数据统计