C++多态的理解与应用

 一.多态

1.多态的概念

多态:在不同继承关系的类对象,调用同一函数,产生了不同的行为;

2.多态的构成条件:

1.必须通过基类(父类)的指针或者引用去调用虚函数;
2.被调用的函数必须是虚函数,且派生类必须对基类的虚函数完成重写;

3.虚函数与虚函数的重写

虚函数:被virtual修饰的类成员函数
 

class A
{
 public:
     virtual void fun(){cout<<"A"<<endl;}        
};

虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数,注意与隐藏(重定义)区别;
注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性);

class A
{
 public:
     virtual void fun(){cout<<"A"<<endl;}        
};
class B:public A
{
 public:
     virtual void fun(){cout<<"B"<<endl;} 
      //void fun(){cout<<"B"<<endl;}  //同样构成虚函数  
};

虚函数重写的例外:析构函数
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。

4. override 和 final

final:修饰虚函数,表示该虚函数不能再被重写,重写编译器会报错
 

class A
{
 public:
     virtual void fun()final
     {
        cout<<"A"<<endl;
     }        
};

override:检查派生类虚函数是否重写了基类某个虚函数,如果没有重写则报错;

class A
{
 public:
     virtual void fun(){cout<<"A"<<endl;}        
};
class B:public A
{
 public:
     virtual void fun()override
    {
      cout<<"B"<<endl;
     } 
      
};

5.重载、覆盖(重写)、隐藏(重定义)的对比

6.抽象类

包含纯虚函数的 类叫做抽象类(也叫接口类),抽象类不能实例化对象;派生类继承后如果不重写纯虚函数,也无法实例化对象,因此可以说纯虚函数规范了派生类必须重写,另外纯虚函数更体现了接口继承;
什么是纯虚函数?,在虚函数后面写上0 即可,代码如下:

class A
{
 public:
   virtual void fun()=0;//纯虚函数
};
class B:public A
{
 public: 
    virtual void fun()
    {
      cout<<"B"<<endl;  
    }   
};

什么是实现继承,什么是接口继承?

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

7.多态的原理

引入一个问题,在下面的代码中,sizeof(a)多大?,

#include<iostream>
using namespace std;
class A
{
public:
virtual void Func1()
{
 cout << "Func1()" << endl;
}
private:
int _a = 1;
};
int main()
{
    A a;
	cout << sizeof(a);
}

运行代码我们可以得出结论,在32位系统中其大小是8,在64位系统中大小为16;为什么?
实际上,在对象a中,不仅存储了_a成员,还存储了_vfptr,这是一个指针,我们叫他虚函数表指针(v代表virtual,f代表funtion),而在32位系统中,指针的大小为4bytes,在64位系统中,指针的大小为8bytes。


一个含有虚函数的类中至少有一个虚函数表指针,而虚函数指针指向一个虚函数表也简称虚表,在表中,存放着该类的虚函数的地址那么在继承中,基类与派生类的虚函数表会如何变化?观察以下代码

#include<iostream>
using namespace std;
class Base
{
public:
	virtual void Func1()
	{
		cout << "Base::Func1()" << endl;
	}
	virtual void Func2()
	{
		cout << "Base::Func2()" << endl;
	}
	void Func3()
	{
		cout << "Base::Func3()" << endl;
	}
private:
	int _b = 1;
};
class Derive : public Base
{
public:
	virtual void Func1()
	{
			cout << "Derive::Func1()" << endl;
	}
	virtual void Func4()
	{
		cout << "Derive::Func4()" << endl;
	}
	
private:
	int _d = 2;
};
int main()
{
	Base b;
	Derive d;
	return 0;
}

运行之后:

对象b和d都拥有自己的虚函数表(虚表),B继承了fun1,fun2,fun3,其中fun3不是虚函数因此他的地址不会被放在虚表中,同时b重写了fun1,因此重写的fun1会覆盖掉继承而来的fun1;同时我们也会发现没有看见b自己的虚函数fun4,实际上是被被编译器隐藏的,他的地址也存放在b的虚表中;

总结一下派生类的虚表生成:
a.先将基类中的虚表内容拷贝一份到派生类虚表中
b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数
c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。

多继承中的虚函数表

观察以下代码:
 

#include<iostream>
using namespace std;
class A {
public:
	virtual void func1() { cout << "A::func1" << endl; }
	virtual void func2() { cout << "A::func2" << endl; }
private:
	int a;
};
class B {
public:
	virtual void func1() { cout << "B::func1" << endl; }
	virtual void func2() { cout << "B::func2" << endl; }
private:
	
	int b;
};
class C : public A, public B {
public:
	virtual void func1() { cout << "C::func1" << endl; }
	virtual void func3() { cout << "C::func3" << endl; }
private:
	int c;
};
int main()
{
	C c;
	return 0;
}

        

C继承了A和B,因此有两个虚表,C重写了fun1函数,因此两个表都完成了重写;
C自己的虚函数fun3的地址存放在哪里?
先继承了A,因此存放在继承自A的虚表中,若是先继承B,则存放在继承自A的虚表中;

注意:
1.虚函数表(虚表)中存储的是虚函数的地址,不是虚函数本身,虚函数与普通函数一样,都是存储在代码段中的,对象存储的也不是虚表,而是虚表指针;
2.一个类的 不同对象共享该类的虚表;

8.多态常见问题

1.什么是多态?
静态多态:函数重载;
动态多态:虚函数完成了重写,父类的指针或者引用调用虚函数,指向谁,调用谁的虚函数,形成多态;在不同继承关系的类对象,调用同一函数,产生了不同的行为;


2.什么是重载、重写(覆盖)、重定义(隐藏)?

1. 重载(Overloading):在同一个作用域内,可以定义多个同名函数,但参数列表不同(个数、类型、顺序等),从而实现函数重载。函数重载可以根据不同的参数类型和个数来区分不同的函数,使得程序可以根据调用的参数类型来选择合适的函数进行调用。

2. 重写(覆盖,Override):子类继承了父类的方法,并对该方法进行重新定义,即在子类中实现一份与父类方法具有相同名称、参数列表和返回类型(三同)的方法,这个过程被称为方法的重写或覆盖。在运行时,当通过基类的指针或引用调用该方法时,实际执行的是子类中重写的方法,而不是基类中的方法。

3. 重定义(隐藏,Redeclaration):当在派生类中定义了与基类中同名(函数名称相同即可)的方法或属性时,无论该方法或属性是否具有相同的参数列表,都会导致基类中的方法或属性被隐藏。这个过程被称为重定义或隐藏。在运行时,通过基类指针或引用调用被重定义的方法时,只会调用基类中的方法,而不会调用派生类中的方法。

总结:
- 重载是在同一个作用域内,根据不同的参数类型和个数来定义多个同名函数。
- 重写(覆盖)是子类对父类中的方法进行重新定义,使得在运行时调用子类中的方法。
- 重定义(隐藏)是在派生类中定义和父类同名的方法或属性,导致基类中的方法或属性被隐藏。


3.多态的实现原理?
多态的实现原理主要依靠虚函数(virtual function)和指针或引用的动态绑定。
1.虚函数:在基类中通过将成员函数声明为虚函数,即在函数声明前加上关键字 “virtual”,表示这个函数可以被派生类覆盖(重写)。虚函数在基类中使用 virtual 关键字声明,在派生类中可以根据需要进行重写。
2.动态绑定:当使用基类指针或引用指向派生类对象时,通过调用虚函数,程序会在运行时根据实际对象的类型来决定调用哪个类的函数,这种将函数调用与对象实际类型绑定起来的过程称为动态绑定。


4.inline函数可以是虚函数吗?
可以,普通调用,inline起作用;多态调用,linline不起作用;


5.静态成员可以是虚函数吗?
不能,编译报错,因为静态成员函数没有this指针,可以指定类域调用,但是无法构成多态,没有意义;


6.构造函数可以是虚函数吗?
不能,编译报错,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。
而虚函数的多态调用,要到虚表中找,但是此时虚表指针还没有初始化;


7.析构函数可以是虚函数吗?什么场景下析构函数是虚函数?
可以,并且最好把基类的析构函数定义成虚函数。
父类指针=new 子类对象
delete 父类指针
这种场景下只有析构函数构成重写,才能正确调用子类的析构函数,否则会导致内存泄漏;


8.对象访问普通函数快还是虚函数更快?
普通调用则一样快,多态调用,虚函数则慢一些,因为要到虚表中去查找虚函数的地址才能调用到虚函数;


9.虚函数表是在什么阶段生成的,存在哪的?
虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的。


10.什么是抽象类?抽象类的作用?
拥有纯虚函数的类就是抽象类;
作用:强制派生类重写了虚函数,另外抽象类体现出了接口继承关系。


 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值