C++面向对象的多态特性:多态和虚函数

1.什么是多态

案例:父类animal,2个子类dogcat,实现run()方法。

class animal
{
public:
	void run(void)
	{
		cout<<"animal run()."<<endl;
	}

};

class dog:public animal
{
public:
	//重定义,也叫隐藏
	void run(void){
		cout<<"dog run()."<<endl;
	}
};

class cat:public animal
{
public:
	//重定义,也叫隐藏
	void run(void){
		cout<<"cat run()."<<endl;
	}
};

对于run()方法,当用父类对象指针指向子类对象时,调用的其实还是父类中的方法:

int main(int argc,char**argv)
{
	//定义子类对象a
	dog a;
	
	//父类对象指针p指向子类对象a
	animal* p = &a;//希望服从于a的类型
	
	//调用的其实还是父类中的run()
	p->run();
	
	return 0;
}

输出:

animal run().

多态(polymorphism)是面向对象的三大特征之一,动态运行时决策。

  • 从宏观讲(从外部看到的现象),多态就是要一套实现逻辑、但是多种具体适配的执行结果。猫就应该是猫的跑法,狗就应该是狗的跑法。
  • 从微观讲(从内部看实现原理),多态就是要一套代码,能够在运行时根据实际对象的不同来动态绑定或者说动态跳转执行相匹配的具体函数。

2.虚函数

在父类的函数声明前加virtual的即是虚函数,虚函数是C++实现多态特性的基础,从语法上讲多态特性的基类方法必须是虚函数。

class animal
{
public:
	virtual void run(void)
	{
		cout<<"animal run()."<<endl;
	}

};

class dog:public animal
{
public:
	//重定义,也叫隐藏
	void run(void){
		cout<<"dog run()."<<endl;
	}
};

class cat:public animal
{
public:
	//重定义,也叫隐藏
	void run(void){
		cout<<"cat run()."<<endl;
	}
};
int main(int argc,char**argv)
{
	dog a;
	animal* p = &a;//希望服从于b的类型
	p->run();
	
	return 0;
}

输出:

dog run().

3.多态中的override

基类中方法声明为virtual,派生类中重新实现同名方法以实现多态,这就叫override(中文为覆盖,或重写)。注意区分overrideredefining,微观上最大区别就是是否有virtual,宏观上最大区别就是是否表现为多态。

对比下三个概念:

  • overload重载:同一个类里面的多个方法,函数名相同但参数列表不同。
  • redifining重定义/隐藏:继承中子类再次实现父类中同名方法然后把父类方法隐藏掉。
  • override覆盖/重写:继承中子类去实现父类中同名virtual方法然后实现多态特性。

多态一定要通过面向对象和override来实现吗?宏观上的多态是一种编程效果,微观上的多态是一种C++支持的编程技术,微观是为了去实现宏观。不用C++virtualoverride也可以实现宏观上的多态,并且C中就经常这么干(例如通过枚举和if else来实现),只是C++源生支持多态,实现起来更容易,后续修改和维护更容易,架构复杂后优势更大。

4.纯虚函数与抽象类

4.1 什么是纯虚函数

纯虚函数就是基类中只有原型没有实体的一种虚函数,它的语法是:

class base
{
	virtual 函数原型 =0}

例如在基类animal中使用纯虚函数:

class animal
{
public:
	virtual void run(void) =0;
};

纯虚函数没有实体是因为语义上不需要。纯虚函数也不会占用内存,因为纯虚函数所在的类根本无法实例化对象。

4.2 抽象类(abstract type)

只要一个类内带有大于等于一个纯虚函数,这样的类就成为了抽象类。抽象类只能作为基类来派生新类,不可实例化对象。派生类必须实现基类的所有纯虚函数后才能用于实例化对象,否则派生类也成了一个抽象类。

抽象类的作用:将有关的数据和行为组织在一个继承层次结构中,保证派生类具有要求的行为。对于暂时无法实现的函数,可以声明为纯虚函数,留给派生类去实现。这种机制可以让语法和语义保持一致。抽象类的子类必须实现基类中的纯虚函数,这样子类才能创建对象,否则子类就还是个抽象类。

4.3 接口类(interface)

接口是一种特殊的类,用来定义一套访问接口,也就是定义一套规约。接口类中不应该定义任何成员变量 ,接口类中所有成员函数都是公有且都是纯虚函数。有些高级语言中直接提供关键字interface定义接口,接口其实就是个纯粹的抽象基类。

5.虚析构函数

5.1 什么是虚析构函数

析构函数前加virtual,则析构函数变为虚析构函数。

规则:如果基类有一个或多个虚函数时(注意不要求是纯虚函数),则其析构函数应该声明为virtual

5.2 为什么需要虚析构函数

父子类各自添加析构函数,用2种分配和回收对象的方式分别实验,观察析构函数被调用的规律。

没有使用虚析构函数时:

class animal
{
public:
	virtual void run(void)
	{
		cout<<"animal run()."<<endl;
	}
	~animal(){cout<<"~animal()"<<endl;}
};

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

int main(int argc,char**argv)
{
	//分配在动态内存上
	animal*p1 = new dog();
	delete p1

	cout<<"-----------"<<endl;
	//分配在栈上
	dog a;
	return 0;
}

输出:

~animal()
-----------
~dog()
~animal()

可以看到,分配在动态内存上时,语法和语义是不一致的。

加上虚析构函数:

class animal
{
public:
	virtual void run(void)
	{
		cout<<"animal run()."<<endl;
	}
	virtual ~animal(){cout<<"~animal()"<<endl;}
};

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

int main(int argc,char**argv)
{
	//分配在动态内存上
	animal*p1 = new dog();
	delete p1;

	cout<<"-----------"<<endl;
	//分配在栈上
	dog a;
	return 0;
}

输出:

~dog()
~animal()
-----------
~dog()
~animal()

结论:虚析构函数可以使得在各种情况下总能调用正确的(和对象真正匹配的)析构函数。

6.总结

其实虚函数的virtual的价值,就是让成员函数在运行时动态解析和绑定具体执行的函数,这是RTTI机制的一部分。析构函数也是成员函数,加virtual的效果和普通成员函数加virtual没什么本质差异。

virtual是有开销的,运行时动态绑定不如编译时静态绑定效率高资源消耗优,但是有了它可以实现多态(多态的本质就是编译时的静态绑定换成运行时的动态绑定)。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值