C++多态

多态的概念

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

例如: 当我们使用支付宝扫码领取红包时,不同的用户扫到的是不一样的红包.

多态的定义及实现

多态的构成条件

多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为.
在继承中要构成多态还有两个条件:
1: 必须通过基类的指针或者引用调用虚函数.
2: 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写.

在这里插入图片描述

虚函数

即被virtual修饰的类成员函数称为虚函数.

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

虚函数的重写

虚函数的重写(覆盖): 派生类中有一个跟基类完全相同的虚函数(即使派生类虚函数与基类虚函数的返回值类型,函数名字,参数列表完全相同),称子类虚函数重写了基类的虚函数.

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

虚函数重写的两个例外

1: 协变(基类与派生类函数返回值类型不同)

派生类重写基类虚函数时,与基类虚函数返回值类型不同,即基类函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变.

using namespace std;
class A
{};
class B : public A
{};
class Person
{
public:
	virtual A* f()   //返回的是基类的指针
	{
		cout << "A* " << endl;
		return new A;
	}
};
class Student : public Person
{
public:
	virtual B* f()   //返回的是派生类的指针.
	{
		cout << "B* " << endl;
		return new B;
	}
};
void  main()
{
	Person p;
	Student st;
	
	//父类指针
	Person* ptr1 = &p;
    
	//指向子类中的父类那一部分,但也指向子类对象.
	Person* ptr2 = &st;

	//ptr1指向父类,调用父类中的虚函数.
	ptr1->f();
	
	//ptr2指向子类,调用子类中的虚函数.
	ptr2->f();

}

2: 析构函数的重写(基类与派生类析构函数的名字不同)

1:如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写. 2:
2 虽然基类与派生类析构函数名不同,看起来违反了重写的规则,但是编译器对析构函数的名称做了特殊处理,统一处理称destructor.

class Person
{
public:
	virtual ~Person()
	{
		cout << "~Person()" << endl;
	}
};
class Student : public Person
{
public:
	virtual ~Student()
	{
		cout << "~Student()" << endl;
	}
};
int main()
{

    //析构父类
	Person* ptr1 = new Person;
	delete ptr1; //ptr1->destructor();

     //析构子类
	Person* ptr2 = new Student;
	delete ptr2; //ptr2->destructor();


	return 0;
}

注意:
1:如果不构成多态,那么按照符合多态的按照指针指向的对象调用,不符合多态的按照指针类型调用原则,此时原本析构子类却变成了析构析构父类(因为ptr2的指针类型为Person*),此时因为子类没有析构有可能会造成内存泄露.
2:.因此,我们可以将析构函数定义为虚函数,构成多态,这样才能让我们传入什么类的地址就能从该类的虚函数表中,找到对应的虚函数调用.

final和override

我们知道,C++编译器对函数重写的要求很严格,但是,在某些情况下由于疏忽,可能会造成函数名字字母顺序写反等情况,但是这种情况在编译期间是不会报错的.因此:C++11提供了override和final两个关键字,帮助用户检测是否重写.

1: final修饰虚函数,表示该虚函数不能被重写.

class Car
{
public:
	virtual void Drive() final
	{

	}
};
class Benz : public Car
{
public:
	virtual void Drive() 
	{
		cout << "Benz-舒适" << endl;
	}
};

2: override检查派生类函数是否重写了基类某个虚函数,如果没有重写就会编译报错.

class Car
{
public:
	virtual void Drive() 
	{

	}
};
class Benz : public Car
{
public:
	virtual void Drive() override
	{
		cout << "Benz-舒适" << endl;
	}
};

重载,覆盖,隐藏的对比

在这里插入图片描述

抽象类

1:在虚函数的后面写上=0,则这个函数为纯虚函数.其中,包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象.

2: 派生类继承后夜泊包含了纯虚函数,也不能实例化对象,只有重写纯虚函数,派生类才能够实例化出对象.

class Car
{
public:
	virtual void drive() = 0;
};
class Benz : public Car
{
public:
	virtual void drive() 
	{
		cout << "Benz-舒适" << endl;
	}
};
int main()
{
	Car* pBenz = new Benz; //子类中函数虚函数已经重写了,
	                       //可以实例化子类.
	pBenz->drive();

	Car* pCar = new Car;  //无法实例化出对象
	pCar->drive();
}

接口继承和实现继承

普通函数的继承是实现继承,派生类继承了基类函数,可以使用函数.继承的是函数的实现,虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的就是为了重写,所以这就是派生类虚函数重写时可以不加上virtual关键字,因为将基类虚函数的接口继承了.

多态的原理

虚函数表

以下是一道常考的笔试题: Base 类实例化对象的大小为多少?

class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
private:
	int _b = 1;
};
int main()
{
	Base b;
	cout << sizeof(b) << endl; //8
	return 0;
}

通过观察测试,我们发现b对象是8bytes.
除了_b成员,还多一个_vfptr指针反在内置成员的前面(顺序与平台有关),其中,对象中的这个指针我们叫做虚函数表指针,每一个含有虚函数的类中都至少有一个虚函数指针,因为虚函数的地址要放到虚函数表中.
在这里插入图片描述
那么虚表中存放的到底是什么呢?
以下Base基类中还有三个成员函数,其中Func1,Func2为虚函数,Func3为普通函数,其中派生类Derive对基类Base中的Func1进行重写.

class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
	virtual void Func2()
	{
		cout << "Func2()" << endl;
	}
	 void Func3()
	{
		cout << "Func3()" << endl;
	}
private:
	int _b = 1;
};
class Derive : public Base
{
public:
	virtual void Func1()
	{
		cout << "Derive::Func1()" << endl;
	}
private:
	int _d = 2;
};
int main()
{
	Base b;
	
	Derive d;

	return 0;
}

通过观察和调试:
1: 派生类对象d中也有一个虚表指针,其中d对象由两部分组成,一部分是基类继承下来的成员,虚表指针. 另一部分则是自己的成员.
2: 基类b对象和派生类d对象虚表是不一样的,其中Func1完成了重写,所以d的虚表中重写的是Derive::Func1(void),所以虚函数重写也叫作覆盖.
3: 此外,因为Func2()也是虚函数,所以放进了虚表,Func3()也被继承下来,但因为不是虚函数,所以并没有放入打虚表中.
4: 虚函数表本质上是一个存放虚函数指针的指针数组,一般情况下,会在这个数组最后存放nullptr.

在这里插入图片描述
派生类虚函数表的总结:
1: 先将基类中的虚表内容拷贝一份到派生类中.

2: 如果派生重写了基类中的某个虚函数,则用派生类自己的虚函数覆盖虚表中的虚函数.

3: 派生类自己新增加的虚函数表按照它在派生类中的声明次序增加到派生类虚函数表的最后.

虚函数存放在哪? 虚表存放在那? 虚表是在什么阶段生成的?

虚函数存放在代码段中,虚表中存放的是虚函数指针,虚表指针是在构造函数(初始化列表)阶段填入到对象中,虚表则是在编译期间就已经生成.

int main()
{
	Base b;
	Base* p = &b;
	printf("虚函数表指针:%p\n", *((int*)p)); //000FDCAC
	
	int i = 0;
	printf("栈:%p\n", &i);      
	
	static int j = 0;
	printf("static变量: %p\n", &j); 

	int* k = new int;
	printf("堆:%p\n", k);       
    const char* cp = "hello world";
	printf("代码段:%p\n", cp);    
	return 0;
}

通过比较观察,可以发现虚表地址与数据段,代码段最为接近,
又根据在地址空间中,地址由低到高变化,且虚表只具备读属性,所以虚表实际上存放于代码段常量区.
在这里插入图片描述

多态的原理

当满足多态条件后,基类的指针或者引用调用虚函数时,不是编译期间确定的,而是在运行时到指向的对象中的虚表去寻找对应的虚函数,指针指向基类的对象,调用的就是基类的虚函数,指针指向派生类,就是调用派生类的虚函数.
在这里插入图片描述

例如:

#include <iostream>
using namespace std;
//父类
class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "买票-全价" << endl;
	}
	int _p = 1;
};
//子类
class Student : public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "买票-半价" << endl;
	}
	int _s = 2;
};
int main()
{
	Person P;
	Student S;
	Person* p1 = &P;
	Person* p2 = &S;
	p1->BuyTicket(); //买票-全价
	p2->BuyTicket(); //买票-半价
	return 0;
}

通过调试观察我们发现:
1: p1指向P对象时,p1->BuyTicket()在P基类对象中的虚函数表找到对应的虚函数指针,进而找到虚函数Person::BuyTicket().
2: p2指向S对象时,p2->BuyTicket()在S派生类对象中的虚函数表找到对应的虚函数指针,进而找到虚函Student::BuyTicket()

在这里插入图片描述
总结:
1: 构成多态的话,指针指向谁就会调用谁的虚函数.
2: 不构成多态,是什么类类型就调用什么类的虚函数.

单继承和多继承关系中虚函数表

单继承中的虚函数表

例如:

//基类
class Base
{
public:
	virtual void func1() { cout << "Base::func1()" << endl; }
	virtual void func2() { cout << "Base::func2()" << endl; }
private:
	int _a;
};
//派生类
class Derive : public Base
{
public:
	virtual void func1() { cout << "Derive::func1()" << endl; }
	virtual void func3() { cout << "Derive::func3()" << endl; }
	virtual void func4() { cout << "Derive::func4()" << endl; }
private:
	int _b;
};
int main()
{
	Base b;
	Derive d;
}

以下为基类对象与派生类对象模型:
在这里插入图片描述
派生类虚表生成过程:
1:先将基类中的虚表内容拷贝一份到派生类中.
2:将派生类重写的虚函数进行覆盖,例如:func1由基类对象中变成了派生类对象中的.
3:将新增的虚函数地址按照顺序放入派生类的虚函数表中.例如:func3(),func4().

但是当我们在监视窗口中观察派生类对象中的虚函数表内容,却发现虚函数func3,func4却看不见,原因是编译器的监视窗口故意隐藏了这两个函数.那怎么查看d的虚表呢

我们可以在内存中输入对应的虚函数表指针就可以看到具体的虚函数表内容了.
其中蓝色框中的两个虚函数就是编译器隐藏的func3()和func4().
在这里插入图片描述

多继承中的虚函数表

以下,当派生类Derive同时继承了基类Base1和Base2时,分别查看基类和派生类的模型.

//基类1
class Base1
{
public:
	virtual void func1() { cout << "Base1::func1()" << endl; }
	virtual void func2() { cout << "Base1::func2()" << endl; }
private:
	int _b1;
};
//基类2
class Base2
{
public:
	virtual void func1() { cout << "Base2::func1()" << endl; }
	virtual void func2() { cout << "Base2::func2()" << endl; }
private:
	int _b2;
};
//派生类
class Derive : public Base1, public Base2
{
public:
	virtual void func1() { cout << "Derive::func1()" << endl; }
	virtual void func3() { cout << "Derive::func3()" << endl; }
private:
	int _d1;
};

Base1和Base2基类虚表模型如下:
在这里插入图片描述
派生类虚表模型如下:
在这里插入图片描述
派生类虚表生成顺序如下:
1: 分别继承Base1和Base2的虚表内容,分别放进两张虚表中.

2: 分别对派生类重写的虚函数进行覆盖.比如func1在两张虚表中都进行了覆盖重写.

3: 在派生类第一张继承的虚表中按照声明顺序加上新增的虚函数,func3().

查看验证上述虚表模型,我们可以在调试中查看内存窗口
在这里插入图片描述

继承和多态常见的面试问题

问题1:

以下程序输出的结果是什么?

 class A
   {
   public:
       virtual void func(int val = 1){ std::cout<<"A->"<< val <<std::endl;}
       virtual void test(){ func();}
   };
   
   class B : public A
   {
   public:
       void func(int val=0){ std::cout<<"B->"<< val <<std::endl; }
   };
   
   int main(int argc ,char* argv[])
   {
       B*p = new B;
       p->test();
       return 0;
   }

p为派生类类型指针,指向的为派生类对象.当p调用test()函数时,实际为普通调用,可是,在test()函数中,实际上是this->func(),此时的this为基类指针,又因为func构成重写,所以这次调用为多态调用,多态调用为指针指向谁就调用谁的虚函数.所以编译器就会调用派生类中的虚函数,理论上程序输出val=0,可是,又因为虚函数重写为接口继承,它继承了A对象中的func接口,重写的是func实现,所以此时val=1.

问题2

如果现在将p的指针类型改为A*,那么如何理解?

class A
   {
   public:
       virtual void func(int val = 1){ std::cout<<"A->"<< val <<std::endl;}
       virtual void test(){ func();}
   };
   
   class B : public A
   {
   public:
       void func(int val=0){ std::cout<<"B->"<< val <<std::endl; }
   };
   
   int main(int argc ,char* argv[])
   {
       A*p = new B;
       p->test();
       return 0;
   }

将派生类地址 B赋给类型为A的指针p,此时发生了切片行为,即p指向的是派生类对象中基类的那一部分,本质上指的还是派生类对象,传递给this指针,即将p拷贝一份给this,this的类型依旧为A,指向的是派生类对象,此时就构成了多态调用,调用的还是派生类中的虚函数.

inline函数可以是虚函数吗?

可以,我们知道内联函数生效的话,会在调用的地方直接展开,也就是说内联函数是没有地址的,但是当内联函数定义成虚函数的话,此时内联属性就被编译器忽略了,这个函数本质上为虚函数,它的地址会被放进虚表中,在运行时候调用.

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

不能,static没有this指针,直接可以用类域指定的方式调用,例如:A::Func().
虚函数是为了实现多态,多态是在运行时去虚表中寻对应的虚函数,但是static成员函数都是在编译时决议,即使它通过父类引用调用,但是它也不会去虚表中寻找调用.所以他是虚函数完全没有价值.

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

不可以,虚表是在初始化列表阶段初始化的,虚函数是为了实现多态,运行时去虚表找对应的虚函数进行调用,而对象中虚表指针都是构造函数初始化列表阶段初始化的,然而构造函数调用时,虚表指针根本就没有初始化,也就是说,此时我们就根本找不到虚表,更别提实现多态了,所以构造函数为虚函数没有意义.

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

可以,并且建议基类的析构函数定义成虚函数,这样构成多态时,就可以指向父类对象指针调用父类析构函数,指向子类对象调用子类析构函数.

构造函数和operator=可以是虚函数吗?

1:拷贝构造不可以,因为拷贝构造本质上也是构造函数,原因和构造函数一样.
2:赋值运算符重载语法上可以,但是赋值运算符本质上是完成同类型对象的拷贝,而父类赋值给子类没有意义,子类赋值给父类可通过切片.

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

1:如果虚函数不构成多态,虚函数和普通函数一样调用,一样快.

2:如果虚函数构成多态,普通函数快,因为普通函数在编译阶段已经确定地址,而虚函数调用则需要在运行时去虚表中找到对应的虚函数地址.

虚函数表是在什么阶段生成的?

1:虚函数表编译阶段就生成好了,存在代码段(常量区)中.
2:而虚函数表指针是在初始化列表阶段初始化的,存在于对象当中.

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

暂停更新

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值