虚函数,多态,纯虚函数、抽象类

​虚函数详细介绍

1、什么是多态

面向对象三大特性(多态,封装,继承)之一,简单的说是一种接口,多种实现

不同的对象可以执行相同的动作,但要通过它们自己的实现代码来执行。

例:猪、狗、猫都属于动物类,我们将动物类作为父类,猪“哼哼叫”,狗“汪汪叫”,猫“喵喵叫”,猪、狗、猫三者都能叫,但三者都要通过重写父类的方法才能实现。

2、为什么要使用多态?使用多态有什么好处?

我们知道,封装可以隐藏实现细节,使得代码模块化;继承可以扩展已存在的代码模块(类);它们的目的都是为了——代码重用。

代码重用,也称代码复用, 就是说你把一个功能写成一个模块, 以便再次需要相同功能的时候,可以直接使用,而不用重新开发。
举例: 假如你的网站需要验证码, 你就可以把验证码这个功能单独提取出来以便复用。
通常代码复用是通过类与对象来实现的, 这也是面向对象编程与面向过程编程最主要的区别之一。

作为面向对象的三大特性之一,多态也有代码重用的功能,还有解决项目中紧耦合的问题,提高程序的可扩展性·。

举例:添加一个子类,继承父类,重写父类的方法。至此,该程序的扩展性得到了提升,而又不需要查看源代码是如何实现的就可以扩展新功能。

三、多态有哪些形式?

​​
​​​​​​多态有哪些形式?在C++中,多态有两种

  • 一种是函数重载,函数重载发生在编译的时候,它的函数参数是不一样的。
  • 一种是虚函数,而虚函数是发生在运行的时候,它的函数原型是一样的,依靠的是指针的指向。

函数重载:
静态多态是编译器在编译期间完成的,编译器会根据实参类型来选择调用合适的函数,如果有合适的函数可以调用就调,没有的话就会发出警告或者报错

四、虚函数

总结:是通过继承重写基类的虚函数实现的多态,在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。

虚函数实现多态满足条件

  • 有继承关系
  • 子类重写父类中的虚函数

多态使用条件

  • 父类指针或引用指向子类对象
  • 重写:函数返回值类型 函数名 参数列表 完全一致称为重写

例子:

class Animal
{
public:
	//Speak函数就是虚函数
	//函数前面加上virtual关键字,变成虚函数,那么编译器在编译的时候就不能确定函数调用了。
	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();
}
//
//多态满足条件: 
//1、有继承关系
//2、子类重写父类中的虚函数
//多态使用:
//父类指针或引用指向子类对象

void test01()
{
	Cat cat;
	DoSpeak(cat);


	Dog dog;
	DoSpeak(dog);
}


int main() {

	test01();

	system("pause");

	return 0;
}

c++中虚函数表
虚函数,多态,纯虚函数、抽象类_第1张图片

虚函数存在哪的?虚表存在哪的?

答:虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。
空类的大小是一个字节(占位符),函数,静态变量都在编译期就形成了,不用类去分配空间,虚函数表是在类之外的,一个类的size不包括虚函数表的大小。而虚函数指针则包含在类中,sizeof一个类则会包含一个虚函数表的指针,定义了虚函数之后,类的大小是 4个字节的东西,除了整形int,就是指针了,没错这里装的就是函数指针

  • 作用主要是实现了多态的机制。首先先解释一下多态的概念,多态是c++的特点之一,关于多态,简而言之就是 用父类的指针指向其子类的实例然后通过父类的指针调用实际子类的成员函数,这种方法呢,可以让父类的指针具有多种形态,也就是说不需要改动很多的代码就可以让父类这一种指针,干一些很多子类指针的事情,这里是从虚函数的实现机制层面进行研究

  • 虚函数表保存的是虚函数的指针,所以大小是虚函数个数*4个字节

  • 在使用了虚函数的父类成员函数,虽然写的还是父类函数指针,实际上却是指向子类函数的指针,而指向哪个虚函数表就由,创建的对象来决定。

虚函数在运行时,是如何调用的,详细步骤说下?程序如何判断一个函数是虚函数

  • C++编译阶段,没办法知道一个基类的指针或引用所指对象的类型,所以没办法通过这个指针判断调用的虚函数到底是谁的,所以只能通过查找虚函数表来找到函数的入口地址。
    一个类,如果有虚函数,那么编译器在编译这个类的时候就会为它添加一个虚函数表,以及指向这个虚函数表的指针。继承这个基类的之类,也会新建一个虚函数表,如果没有重载,那么这个新的虚函数表中的函数指针就被拷贝为父类该函数的地址,否则为新的函数地址。编译器会将这些函数指针在虚函数表中按照基类中该函数出现的次序排列,子类中的虚函数表也将以这种方式排列。
    每个有虚函数的类都有一个虚函数表指针pv,当通过指针或引用调用一个虚函数时,先通过pv找到虚函数表,然后根据这个虚函数在虚函数表中的偏移量来找到正确的函数地址,然后再CALL之。
  • 假设存在一个类A,我们对其内部的实现一无所知,如何判断该类是否有虚函数呢?
    如果一个类存在虚函数的话,在给该类实例对象分配内存的时候,会存在一个虚函数表的指针。
    主要是派生类可以写可以不写virtual关键字,然后编译器通过与基类的虚函数的对比进行判断(名称,参数,返回值类型)是否是虚函数。(判断是否重写)
    虚函数,多态,纯虚函数、抽象类_第2张图片

为什么虚函数是动态绑定

C++中实现动态绑定需要有两个基本条件

a.指向基类的指针和引用

b.子类对基类的虚函数进行重定义。(函数定义原型完全一样,除一个特例,)
动态绑定的实质是:对虚表的查询。虚表中保存每个函数的实际地址。

C++ 父类构造/析构函数中调用虚函数,会如何?

虽然可以对虚函数进行实调用,但程序员编写虚函数的本意应该是实现动态联编。在构造函数中调用虚函数,函数的入口地址是在编译时静态确定的,并未实现虚调用。但是为什么在构造函数中调用虚函数,实际上没有发生动态联编呢?
构造函数是用来初始化一个对象的,在初始化一个对象时,先调用该对象基类类的构造函数,然后再调用自己类的构造函数。

第一个原因,在概念上,构造函数的工作是为对象进行初始化。在构造函数完成之前,被构造的对象被认为“未完全生成”。当创建某个派生类的对象时,如果在它的基类的构造函数中调用虚函数,那么此时派生类的构造函数并未执行,所调用的函数可能操作还没有被初始化的成员,将导致灾难的发生。

第二个原因,即使想在构造函数中实现动态联编,在实现上也会遇到困难。这涉及到对象虚指针(vptr)的建立问题。在Visual C++中,包含虚函数的类对象的虚指针被安排在对象的起始地址处,并且虚函数表(vtable)的地址是由构造函数写入虚指针的。所以,一个类的构造函数在执行时,并不能保证该函数所能访问到的虚指针就是当前被构造对象最后所拥有的虚指针,因为后面派生类的构造函数会对当前被构造对象的虚指针进行重写,因此无法完成动态联编。

同样的,在析构函数中调用虚函数,函数的入口地址也是在编译时静态决定的。也就是说,实现的是实调用而非虚调用。

从概念上说,析构函数是用来销毁一个对象的,在销毁一个对象时,先调用该对象所属类的析构函数,然后再调用其基类的析构函数,所以,在调用基类的析构函数时,派生类对象的“善后”工作已经完成了,这个时候再调用在派生类中定义的函数版本已经没有意义了。

哪些函数不能是虚函数?把你知道的都说一说

  1. 构造函数,构造函数初始化对象,派生类必须知道基类函数干了什么,才能进行构造;当有虚函数 时,每一个类有一个虚表,每一个对象有一个虚表指针,虚表指针在构造函数中初始化;

  2. 内联函数,内联函数表示在编译阶段进行函数体的替换操作,而虚函数意味着在运行期间进行类型确定,所以内联函数不能是虚函数;

  3. 静态函数,静态函数不属于对象属于类,静态成员函数没有this指针,因此静态函数设置为虚函数没有任何意义。

  4. 友元函数,友元函数不属于类的成员函数,不能被继承。对于没有继承特性的函数没有虚函数的说法。

  5. 普通函数,普通函数不属于类的成员函数,不具有继承特性,因此普通函数没有虚函数。

二、纯虚函数、抽象类

总结:
在成员函数(必须为虚函数)的形参列表后面写上=0,则成员函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。

纯虚函数是一定要被继承的,纯虚函数允许我们在基类中定义一个没有实现的函数,然后强制子类去实现该函数,你只能在实现了所有这些纯虚函数之后才能够实例化。目的是强制子类为特定的函数提供自己的定义

  • 成员函数的形参后面写上=0,则成员函数为纯虚函数
    在这里插入图片描述

  • 1、纯虚函数对类最大的束缚就是:类中一旦出现纯虚函数,就不可实例化了;

  • 2、纯虚函数还是可以有函数体,只不过最终还是要被派生类重写,在派生类的函数中可以调用基类中有函数体的纯虚函数;

  • 3、为使派生类能完全释放资源,基类析构函数必须声明为虚函数,否则,在用基类指针new一个派生类对象后,delete该指针就只能回收与基类相关的资源,造成内存泄漏;

  • 4、纯虚析构函数必须要有函数体,根据C++析构函数的调用规则,派生类会调用基类析构函数,如果基类析构函数没有函数体会造成函数调用失败而报错,这是纯虚析构函数与普通纯虚函数不同之处。

class Base
{
public:
	virtual void print() = 0;//纯虚函数,可以有函数体,可实例化的派生类必须重写它
	virtual void play() {//虚函数,有函数体,若派生类没有重写它,就原样继承下来
		cout << "Base play!" << endl;
	}
	virtual ~Base() = 0;//纯虚析构函数,必须要有函数体
	//virtual ~Base(){}	//很多情况下,基类的虚函数都会有函数体,将析构函数声明为纯虚函数是一种抽象化基类、不许其实例化的一种方法。
};
void Base::print() {
    //被声明为纯虚函数,仍可以有函数体
    //但派生类若想实例化,必须重写纯虚函数,派生类中可以调用基类有函数体的纯虚函数(Base::print())
	cout << "Base print!" << endl;
}
Base::~Base(){
    //被声明为纯虚析构函数,根据C++类的规则,因为其派生类析构函数会调用基类的析构函数,所以必须有函数体,否则不知道你执行了啥
}

抽象类
(1)声明纯虚函数可使当前类变成抽象类,禁止该类被实例化,一个基类如果包含一个或一个以上纯虚函数,就是抽象基类。抽象基类不能也没必要定义对象。
(2)在类的层次结构中,顶层或最上面几层可以是抽象基类。抽象基类体现了本类族中各类的共性,把各类中共有的成员函数集中在抽象基类中声明。
(3)抽象基类是本类族的共公共接口,即就是从同一基类中派生出的多个类有同一接口。
(4) 一个纯虚函数就可以使类成为抽象基类,但是抽象基类中除了包含纯虚函数外,还可以包含其它的成员函数(虚函数或普通函数)和成员变量。
(5) 只有类中的虚函数才能被声明为纯虚函数,普通成员函数和顶层函数均不能声明为纯虚函数
虚函数,多态,纯虚函数、抽象类_第3张图片

你可能感兴趣的:(c++基础,c++,开发语言,后端)