浅析c++的多态性质(从继承到多态)

面向对象三大特性:封装,继承,多态。可见继承和多态性的重要性,继承就不多说了,但在c++中是如何实现多态的呢?先来简单介绍下多态:

       多态性可以简单地概括为“一个接口,多种方法”,程序在运行时才决定调用的函数,它是面向对象编程领域的核心概念,一种不严谨的说法是:继承是子类使用父类的方法,而多态是父类使用子类的方法。多态(polymorphism),字面意思多种形状,那么多态的作用是什么呢?封装可以使得代码模块化,继承可以扩展已存在的代码,他们的目的都是为了代码重用。而多态的目的则是为了接口重用。也就是说,不论传递过来的究竟是那个类的对象,函数都能够通过同一个接口调用到适应各自对象的实现方法。一般我们使用多态是为了避免在父类里大量重载引起代码臃肿且难于维护。


为了更深入理解多态,我们先理解一下与多态相关的以下几种特性:使用虚函数重写(override),在c语言中用的很多的重载(overload),以及不使用虚函数直接在子类中重定义(redifining)函数的各自的特性和功能。

1 重写(覆盖)override
  override是重写(覆盖)了一个方法,以实现不同的功能。一般用于子类在继承父类时,重写(覆盖)父类中的方法。函数特征相同,但是具体实现不同。
重写需要注意:
  • 被重写的函数不能是static的,必须是virtual的
  • 重写函数必须有相同的类型,名称和参数列表
  • 重写函数的访问修饰符可以不同。尽管virtual是private的,派生类中重写改写为public、protect也是可以的
2 重载overload
  overload是重载,一般是在一个类实现若干重载的方法,这些方法的名称相同而参数形式不同。但是不能靠返回类型来判断。
重载需要注意:
  • 位于同一个类中
  • 函数的名字必须相同
  • 形参列表不同
  • 若一个重载版本的函数面前有virtual修饰,则表示他是虚函数,但他也是属于重载的一个版本
  • 不同的构造函数(无参构造、有参构造、拷贝构造)是重载的应用
3 重定义redefining
  派生类对基类的成员函数重新定义,即派生类定义了某个函数,该函数的名字与基类中函数名字一样。
  重定义也叫做隐藏,子类重定义父类中有相同名称的非虚函数(参数可以不同)。如果一个类,存在和父类相同的函数,那么这个类将会覆盖其父类的方法,除非你在调用的时候,强制转换为父类类型,否则试图对子类和父类做类似重载的调用时不能成功的。
重定义需要注意:
  • 不在同一个作用域(分别位于基类、派生类)
  • 函数的名字必须相同
  • 对函数的返回值、形参列表无要求
  • 若派生类定义该函数与基类的成员函数完全一样(返回值、形参列表均相同),且基类的该函数为virtual,则属于派生类重写基类的虚函数
  • 若重新定义了基类中的一个重载函数,则在派生类中,基类中该名字函数(即其他所有重载版本)都会被自动隐藏,包括同名的虚函数

这三种特性均体现了c++中的多态特性,多态分为两类:静态多态性和动态多态性,函数重载和运算符重载实现的多态性属于静态多态性,在程序编译时系统就能决定调用哪个函数,因此静态多态性又称为编译时的多态性。静态多态性是通过函数的重载实现的。动态多态性是在程序运行过程中才动态地确定操作所针对的对象。它又称运行时的多态性。动态多态性是通过虚函数实现的。

下面看一段代码:

class Father
{
public:	
	Father(int parameter){cout << "Fathrer construct,parameter is "<<parameter<<endl;}
	~Father(){cout << "Fathrer destruct"<<endl;}
	void fun_notVirtual(){cout << "call father notVirtual function " <<endl;}
	void fun_notVirtual(int parameter){cout << "call father notVirtual overload function " <<endl;}
	virtual void fun_Virtual(){cout << "call father Virtual function " <<endl;}
};

class Son:public Father
{
public:
	Son():Father(1){cout << "Son construct"<<endl;}
	~Son(){cout << "Son destruct"<<endl;}
	void fun_notVirtual(){cout<<"call son notVirtual function";}
	void fun_Virtual(){cout << "call son Virtual function"<<endl;}
};


int main() 
{
	
	{
		Father* father;
		Son son;
		father = &son;
		father->fun_Virtual();
		father->fun_notVirtual();
		father->fun_notVirtual(1);
	}
	
	
	return 0;
} 

运行结果:


下面开始分析这段代码,从这段代码可以清晰的看的出整个子类的构造,析构,父类中fun_notVirtual()的重载(静态多态),子类中通过virtual函数实现的重写(动态多态),

静态多态的体现:

                father->fun_notVirtual();
		father->fun_notVirtual(1);

动态多态的体现:

                father->fun_Virtual();
		father->fun_notVirtual();

下面来对比下静态多态和动态多态:

静态多态与动态多态的实质区别就是函数地址是早绑定还是晚绑定。如果函数的调用,在编译器编译期间就可以确定函数的调用地址,并生产代码,是静态的,就是说地址是早绑定的。而如果函数调用的地址不能在编译器期间确定,需要在运行时才确定,这就属于晚绑定。
  那么动态多态的作用是什么呢,封装可以使得代码模块化,继承可以扩展已存在的代码,他们的目的都是为了代码重用。而多态的目的则是为了接口重用。也就是说,不论传递过来的究竟是那个类的对象,函数都能够通过同一个接口调用到适应各自对象的实现方法。
  最常见的用法就是声明基类的指针,利用该指针指向任意一个子类对象,调用相应的虚函数,可以根据指向的子类的不同而实现不同的方法。如果没有使用虚函数的话,即没有利用C++的动态多态性,则利用基类指针调用相应的函数的时候,将总被限制在基类函数本身,而无法调用到子类中被重写过的函数。因为没有动态多态性,函数调用的地址将是一定的,而固定的地址将始终调用到同一个函数,这就无法实现一个接口,多种方法的目的了。
这里再说一下为什么虚构函数要是虚函数:虚函数的目的就是通知系统在函数调用时能够自动识别对应的类对象类型,从而能够根据指针所指类型调用对应的类对象,实现函数调用时的多态性。对于析构函数而言,同样适用于上述规则。如果析构函数不是虚函数,那么在调用该函数时(对象被删除时)则只会调用当前对象对应的类的析构函数,这对于直接定义的对象是没有什么影响的,但是对于使用基类指向派生类的指针而言,因为基类指针实际上是基类类型,所以析构时自然只会调用基类的析构函数,这就可能产生内存泄漏(因为派生类的析构函数不被调用)。所以如果确定程序中有基类指针指向派生类的问题,则必须将基类的析构函数指定为虚函数,如此才能确保NEW出来的对象被正确的DELETE。
同理, 构造函数不可以是虚函数的,这个很显然,毕竟虚函数都对应一个虚函数表,虚函数表是存在对象内存空间的,如果构造函数是虚的,就需要一个虚函数表来调用,但是类还没实例化没有内存空间就没有虚函数表,这根本就是个死循环。
       再说一下虚函数表的存放位置:

                1.虚函数表是全局共享的元素,即全局仅有一个.

                2.虚函数表类似一个数组,类对象中存储vptr指针,指向虚函数表.即虚函数表不是函数,不是程序代码,不肯能存储在代码段.

                3.虚函数表存储虚函数的地址,即虚函数表的元素是指向类成员函数的指针,而类中虚函数的个数在编译时期可以确定,即虚函数表的大小可以确定,即大小是在编译时期确定的,不必动态分配内存空间存储虚函数表,所以不再堆中.


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值