c++面向对象程序设计基础教程————多态性和虚函数

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

继承性反映的是类与类之间的层次关系,多态性则是考虑这种层次关系以及类自身特定成员函数之间的关系来解决行为的再抽象问题。多态性有两种表现形式一种是不同的对象在收到相同的消息时,产生不同的动作,主要通过虚函数来实现;另一种是同一对象收到相同的消息却产生不同的函数调用,主要通过函数重载来实现。本章将讨论多态性的主要内容:虚函数和动态联编;

一、静态联编与动态联编

多态性就是同一符号或名字在不同情况下具有不同解释的现象,既是指同一个函数的多种形态。c++可以支持两种多态性,编译时的多态性和运行时的多态性。
对一个函数的调用,要在编译时或在运行时确定将其
连接上相应的函数体的代码,这一过程称为函数联编(简称联编)。c++中两种联编形式:静态联编和动态联编。

静态联编

  静态联编是指在程序编译连接阶段进行的联编。编译器根据源代码调用固定的函数标识符,然后由连接器接管这些标识符,并用物理地址代替他们。这种联编又被称为早期联编,应为这种联编工作是在程序运行之前完成的。
  静态联编所支持的多态性称为编译时的多态性,当调用重载函数时,编译器可以根据调用时所使用的实参在编译时就确定下来应调用哪个函数。下面来看在层次关系中一个静态联编的例子。
#include<iostream>
const double PI = 3.14;
using namespace std;
class Figure         //定义基类;
{
public:
	Figure() {};
	double area() const { return 0.0; }
};
class Circle :public Figure           //定义派生类,公有继承;
{
public:
	Circle(double myr) { R = myr; }
	double area() const { return PI * R * R; }
protected:
	double R;
};
class Rectangle :public Figure   //定义派生类,公有继承;
{
public:
	Rectangle(double myl, double myw) { L = myl; W = myw; }
	double area() const { return L * W; }
private:
	double L, W;
};
int main()
{
	Figure fig;                //基类Figure对象;
	double area;
	area = fig.area();
	cout << "Area of figure is" <<area<< endl;
	Circle c(3.0);            //派生类Circle对象;
	area = c.area();
	cout << "Area of Circle is" << area << endl;
	Rectangle rec(4.0, 5.0);
	area = rec.area();
	cout << "Area of Rectangle is" << area << endl;
	return 0 ;
}

上面的程序我们可以看出,在静态联编中,其实就是对重载函数的使用。定义不同的对象,通过对象来引用不同的函数从而实现我们要实现的功能。
静态联编的最大优点就是速度快,运行时的开销仅仅是传递参数,执行函数调用,清除等。不过,程序员必预测在每一种情况下所有的函数调用时,将要使用那些对象。这样,不仅有局限性,有时也是不可能实现的。
下面的程序就会说明这种情况:

#include<iostream>
using namespace std;
const double PI = 3.14;
class Figure
{
public:
	Figure() {};
	double area() const { return 0.0; }
};
class Circle :public Figure             //定义派生类,公有继承;
{
public:
	Circle(double myr) { R = myr; }
	double area()const { return PI*R*R; }
protected:
	double R;
};
class Rectangle :public Figure               //定义派生类,公有继承;
{                               
	public:
		Rectangle(double myl, double myw) { L = myl, W = myw; }
		double area()const { return L * W; }
private:
	double L, W;
};
void func(Figure &p)         //形参为基类的引用;
{
	cout << p.area() << endl;
}
int main()
{
	Figure fig;
	cout << "Area of figure is";
	func(fig);
	Circle c(3.0);   //Circle派生类对象;
	cout << "Area of Circle is";
	func(c);
	Rectangle rec(4.0, 5.0);
	cout << "Area of Rectangle is";
	func(rec);
	return 0;
}

在这里插入图片描述
上面的程序没有报错,但是结果正确,那是为什么?
在编译时,编译器将函数中的形参p所执行的area()操作联编到Figure类的area()上,这样访问的知识从基类继承来的成员。
那有没有什么方法来改变这种局限性喃?

动态联编

动态联编是指在程序运行时进行的联编。只有向具有多态性的函数传递一个实际对象的时候,该函数才能与多种可能的函数中的一种来联系起来。这种联编又被称为晚期联编。
动态联编所支持的多态性被称为运行时多态性。在程序代码中要知名某个成员函数具有多态性需要进行动态联编,且需要关键字virtual来标记。这种用virtual关键字标记的函数称为虚函数.

动态联编的优点动态联编的缺点
增强了编程灵活性,问题抽象性和程序易维护性函数调用顺序慢

虚函数

虚函数的作用

虚函数是一个成员函数,该成员函在基类内部声明并且被派生类重新定义。为了创建虚函数,应在基类中该函数生命的前面加上关键字virtual。
virtual<返回值类型><函数名>(<形式参数>)
{
《函数体》
}
如果某类中一个成员函数被说明为虚函数,这便意味着该成员函数在派生类中可能存在不同的实现方式。当继承包含虚函数的类时,派生类将重新定义该虚函数衣服和自身的需要。从本质上讲,虚函数实现了“相同界面,多种实现”的理念。而这种理念时运行时的多态性的基础,既是动态联编的基础。
动态联编需要满足的条件:
(1).类之间满足类型兼容规则;
(2).要声明虚函数;
(3).要由成员函数来调用或者是通过指针,引用来访问虚函数。

#include<iostream>
using namespace std;
const double PI = 3.14;
class Figure
{
public:
	Figure() {};
	virtual double area()const { return 0.0; }        //定义为虚函数;
};
class Circle :public Figure                          //定义派生类,公有继承;
{                               
public:
	Circle(double myr) { R = myr; }
	virtual double area()const { return PI * R * R; }
protected:
	double R;
};
class Rectangle :public Figure           //定义派生类,公有继承方式;
{
public:
	Rectangle(double myl, double myw) { L = myl, W = myw; }
	virtual double area()const { return L * W; }
private:
	double L , W;
};
void fun(Figure& p)                 //形参为积基类的引用;
{
	cout << p.area() << endl;
}
int main()
{
	Figure fig;
	cout << "Figure of area is";
	fun(fig);
	Circle c(3.0);
	cout << "Area of Circle is";
	fun(c);
	Rectangle rec(4.0, 5.0);
	cout << "Area of Retangle is";
	fun(rec);
	return 0;
}

在这里插入图片描述
这个时候我们发现,答案正确了。看到这里,有没有一种熟悉的感觉,和我们前面学的虚基是不是相似。所以这里我们以可以理解为动态联编之所以能够不断地联编,就是因为成员函数产生了副本。

虚函数与一般重载函数的区别

乍一看,上面的程序,虚函数类似于重载函数。但它不属于重载函数,虚函数与一般的重载函数的区别:

虚函数重载函数
虚函数不仅要求函数名相同,而且要求函数的签名,返回类型也相同。也就是说函数原型必须完全相同,而且虚函数的特性必须是体现在基类和派生类的类层次结构中重载函数只要求函数有相同的函数名,并且重载函数 是在相同作用域定义的名字相同的不同函数
虚函数只能是非静态成员函数重载函数可以是成员函数或友元函数
构造函数不能定义为虚函数,析构函数能定义为虚函数构造函数可以重载,析构函数不可以
虚函数是根据对象来调用的重载函数调用是以传递参数 序列的差别 来调用
虚函数是在运行时联编重载函数是在编译时联编

继承虚属性

基类中说明的虚函数具有自动向下传给他的派生类的性质。不管经历多少派生类层,所有界面相同的函数都熬吃虚特性。因为派生类也是基类。
在派生类中重新定义虚函数时,要求与基类中说明的虚函数原型完全相同,这时对派生类的虚函数中virtual说明可以省略,但是为了提高程序的可读性。为了区分重载函数,而把一个派生类中重定义基类的虚函数称为覆盖。
示例:

#include<iostream>
using namespace std;
class Base
{
public:
	virtual int func(int x)          //虚函数;
	{
		cout << "This is Base class";
		return x;
	}
};
class Subclass :public Base          //派生类,公有继承;
{
public:
 int func(int x)                     //没有使用virtual关键字,但是依旧为虚函数;
	{
	 cout << "This is Subclass class";
	 return x;
	}
};
void fun(Base& x)
{
	cout << "x=" << x.func(5) << endl;
}
int main()
{
	Base bc;
	fun(bc);
	Subclass be;
	fun(be);
	return 0;
}

在这里插入图片描述
下面我们来分析一下虚函数的错误使用

#include<iostream>
using namespace std;
class Base
{
public:
	virtual int func(int x)          //虚函数;
	{
		cout << "This is Base class";
		return x;
	}
};
class Subclass :public Base          //派生类,公有继承;
{
public:
 virtual float func(int x)                     //函数返回类型不同
	{
	 cout << "This is Subclass class";
	 return x;
	}
};
void fun(Base& x)
{
	cout << "x=" << x.func(5) << endl;
}
int main()
{
	Base bc;
	fun(bc);
	Subclass be;
	fun(be);
	return 0;
}

在这里插入图片描述
这里派生类中的虚函数,仅仅只是使用了不同的返回类型,但是就报错了,这是用为在c++中,只靠返回类型不同的信息,进行函数匹配是模糊的。

#include<iostream>
using namespace std;
class Base
{
public:
	virtual int func(int x)          //虚函数;
	{
		cout << "This is Base class";
		return x;
	}
};
class Subclass :public Base          //派生类,公有继承;
{
public:
 virtual int func(float x)                     //函数形参不同
	{
	 cout << "This is Subclass class";
	 return x;
	}
};
void fun(Base& x)
{
	cout << "x=" << x.func(5) << endl;
}
int main()
{
	Base bc;
	fun(bc);
	Subclass be;
	fun(be);
	return 0;
}

如果派生类与基类的虚函数仅函数名相同,其他不同,则c++认为是重定义函数,是隐藏的,丢失了虚特性。
一个类中的虚函数说明只对派生类中重定义的函数有影响,对他的基类并没有影响。

示例虚函数对他的基类的函数没有影响
#include<iostream>
using namespace std;
class Base
{
public:
	int func(int x)             //不是虚函数;
	{
		cout << "This is Base class";
		return x;
	}
};
class Subclass :public Base
{
public:
	virtual int func(int x)              //虚函数;
	{
		cout << "This is Subclass class";
		return x;
	}
};
class Subclass2 :public Subclass
{
public:
	int func(int x)                //自动成为虚函数;
	{
		cout << "This is Baseclass2 class";
		return x;
	}
};
int main()
{
	Subclass2 sc2;
	Base& bc = sc2;
	cout << "x=" << bc.func(5) << endl;
	Subclass& sc = sc2;
	cout << "x=" << sc.func(5) << endl;
	return 0;
}

在这里插入图片描述
从结果看出,func()的操作贝莱你白难道Subclasses类中,显然进行的是动态联编。

成员函数中调用虚函数

一个基类或派生类的成员函数中可以直接调用该类等级中的虚函数;

#include<iostream>
using namespace std;
class Base
{
public:
	virtual void func1()         //虚函数
	{
		cout << "Tis is Base class";
	}
	void func2() { func1(); }     //调用了虚函数;
};
//一般类中的成员都是通过对象来调用,但是这里用虚函数就可以,除了虚函数,还有就是使用静态static关键字,表明他已经被被分配内存;
class Subclass :public Base
{
public:
	virtual void func1()
	{
		cout << "This is Subclass func1()";
	}
};
int main()
{
	Subclass sc;
	sc.func2();
	return 0;
}


上面的例子中,我们可以得出,在满足公有继承的情况下,成员函数中调用虚函数使用的是动态联编;

构造函数和析构函数中调用虚函数

在构造函数和析构函数中调用虚函数时,采用静态联编,即它们所调用的虚函数是自己的类定义的函数。如果在自己的类中没有实现这个函数,则调用的时基类中虚函数,但绝不是任何在派生类中重新定义的虚函数。这是因为在建立派生类的对象时,它所包含的基类成员在派生类中定义的成员建立之前被建立。在对象撤销时,该对象所包含的在派生类中定义的成员要先与基类成员的撤销。
示例代码:

#include<iostream>
using namespace std;
class Base
{
public:
	Base() { func1(); }       //构造函数调用虚函数,静态联编;
	virtual void func1()         //虚函数;
	{
		cout << "This is Base func1()" << endl;
	}
	virtual void func2()
	{
		cout << "This is Base func2()" << endl;
	}
	~Base() { func2(); }     //析构函数调用虚函数,静态联编;
};
class Subclass :public Base
{
public:
	virtual void fnc1()
	{
		cout << "This is Subclass func1()" << endl;
	}
	virtual void func2()
	{
		cout << "This is Subclass func2()" << endl;
	}
};
int main()
{
	Subclass sc;
	cout << "Exit main" << endl;
	return 0;
}

在这里插入图片描述
上面的程序中,我们定义了一个派生类对象,派生类中没有构造函数,所以调用了基类中的构造函数,基类中的构造函数调用了它自身定义的虚函数,这里是静态联编。

纯虚函数和抽象类

当虚函数没有被派生类重新定义时,将使用基类中的虚函数。然而基类常用来表示一些抽象的概念,基类中没有有意义的虚函数定义。另外,在某些情况下,需要一个虚函数被所有派生类覆盖。为了处理上述两种情况,可以将虚函数定义为纯虚函数,相应的类就变成了抽象类。

纯虚函数

如果不能在基类中给出有意义的虚函数的实现,但又必须让基类为派生类提供一个公共界面函数。这时可以将它说明为纯虚函数,留给派生类去实现,说明纯虚函数的格式:
virtual 返回类型 函数名(参数)=0;
纯虚函数的定义是在虚函数定义的基础上再让函数等于0,这只是表示纯虚函数的形式,并不是说他的返回值为0;
在这里插入图片描述

#include<iostream>
const double PI = 3.14;
using namespace std;
class Figure
{
public:
	Figure() {};
	virtual double area()const = 0;    //定义为纯虚函数;如果不是纯虚函数,就要返回一个double值;
};
class Circle :public Figure
{
public:
	Circle(double myr) { R = myr; }
	virtual double area()const { return PI * R * R; }         //定义为虚函数;
protected:
	double R;
};
class Rectangle :public Figure
{
public:
	Rectangle(double myl, double myw) { L = myl; W = myw; }
	virtual double area()const { return L * W; }   //定义为虚函数;
private:
	double L ,W;

};
void fun(Figure& p)
{
	cout << p.area() << endl;
}
int main()
{
	Circle c(3.0);
	cout << "Area of cicle is";
	fun(c);
	Rectangle rec(4.0, 5.0);
	cout << "Area of rectangle is";
	fun(rec);
	return 0;
}

上面的代码中,Figure中的虚函数仅仅起到为派生类提供接口的作用。

抽象类

一个类可以说明多个纯虚函数,对于包含有纯虚函数的类被成为抽象类。一个抽象类只能作为基类来派生新类,不能说明抽象类的对象。应为抽象类中有一个或者多个函数没有被定义,也不能用作参数类型,函数返回类型或显示类型类型。但剋以说明指向抽象类对象的指针(和引用),以支持运行时的多态性;

例如:
Figure fig; //错误;
Figure func1();//错误;
int func2(Figure);  //错误;
void func&(Figure&p);//正确;

虚析构函数

一个类中经所有的成员函数总是有益的,它除了会增加一些资源开销,没有其他坏处。但设置虚函数有许多限制,一般只有非静态的成员函数才可以说明为虚函数。除此之外,析构函数也可以是虚函数,并且最好在动态联编的时候说明为虚函数;

虚析构函数的定义和使用

c++目前不支持构造虚构造函数。由于析构函数不能有参数,所以一个类只能有一个虚析构函数;

格式:
virtual ~<类名>()
入宫一个类的析构函数是虚函数,那么,由它派生而来的所有子类的析构函数也是虚函数。如果析构函数是虚函数,则这个调用是动态联编;
在这里插入图片描述

#include<iostream>
using namespace std;
class Base
{
public:
	Base(){}
	virtual ~Base() { cout << "Base destructor is called" << endl; }
};
class Subclass :public Base
{
public:
	Subclass(){}
	~Subclass();
};
Subclass::~Subclass()
{
	cout << "Subclass destructor is called" << endl;
}
int main()
{
	Base* b = new Subclass;
	delete b;
	return 0;
}

上面的程序中,我们构造了虚析构函数,那么我们构造虚析构函数的意义又在哪里?

虚析构函数的必要性

如果我们在类中定义了一个虚函数,我们在定义了一个对象后,如果要删除这个对象(delete),那么就会自动调用析构函数,也就是说,为了满足能够删除所有的成员函数,也就是构造虚析构函数;

#include<iostream>
using namespace std;
class Base
{
public:
	Base(){}
	~Base() { cout << "Base destructor is called" << endl; }
};
class Subclass :public Base
{
public:
	Subclass();
	~Subclass();
private:
	int* ptr;
};
Subclass::Subclass()
{
	ptr = new int;
}
Subclass::~Subclass()
{
	cout << "Subclass destructor is called" << endl;
	delete ptr;
}
int main()
{
	Base* b = new Subclass;
	delete b;
	return 0;
}

在这里插入图片描述
上面的程序我们会发现,用delete删除对象的时候,只调用了基类的析构函数,尾调用派生类的析构函数,,这造成ptr的内存未得到释放,现在我们将~Base()改为虚析构函数;看它的运行:
在这里插入图片描述
为什么把析构函数改为虚函数就会成功?这是因为将析构函数改为虚函数后,在撤销对象的时候,会先执行派生类的析构函数,由于这里采用了动态联编,所以才会得到这个结果;

应用实例

编写一个小型公司的工资管理程序,该公司主要有4类人员:经理。兼职技术人员,销售人员,销售经理。经理固定月薪8000,尖子技术人员100元每小时,销售员为当月销售额的4%,销售经理保底工资为5000,加上部门销售额的5%;
代码实现:

#include<iostream>
#include<string>
using namespace std;
//基类Employee的定义
class Employee
{
protected:
	int no;  //编号;     
	string name; //姓名
	float salary;  //月星总额
	static int totalno;  //编号最大值
public:
	Employee()
	{
		no = totalno++;        //输入员工号加1;
		cout << "职工姓名:";
		cin >> name;
		salary = 0.0;   //总额赋值为0;
	}
	virtual void pay() = 0;   //计算月薪函数;
	virtual void display() = 0;  //输出员工信息函数;
};
//兼职技术人员类Technician的定义
class Technician :public Employee
{
private:
	float hourlyrate;    //每小时酬金;
	int workhours;   //当月工作时数;
public:
	Technician()   //构造函数;
	{
		hourlyrate = 100;         //每小时酬薪100;
		cout << name << "本月工作时间:";
		cin >> workhours;
	}
	void pay()
	{
		salary = hourlyrate * workhours;    //计算月薪,按小时计算;
	}
	void display()
	{
		cout << "兼职技术员:" << name << ",编号:";
		cout << no << ",本月工资:" << salary << endl;
	}
};
//销售人员类Salesman的定义
class Salesman :virtual public Employee         //派生类,销售员;
{
protected:
	float commrate;     //按销售额提取酬金的百分比;
	float sales;         //当月销售额;
public:
	Salesman()        //构造函数;
	{
		commrate = 0.04f;     //销售提成比例为4%;
		cout << name << "本月销售额:";
		cin >> sales;
	}
	void pay()           //计算销售员月薪函数;
	{
		salary = sales * commrate;     //月薪=本月销售额*销售提成比例;
	}
	void display()  //显示销售员信息函数;
	{
		cout << "销售员:" << name << ",编号:";
		cout << no << ",本月工资:" << salary << endl;
	}
};
class Manager :virtual public Employee
{
protected:
	float monthlypay;      //固定月薪;
public:
	Manager()
	{
		monthlypay = 8000;
	}
	void pay()           //计算经理月薪总数;
	{
		salary = monthlypay;          //月薪总额;
	}
	void display()
	{
		cout << "经理:" << name << "编号";
		cout << no << ",本月工资:" << salary << endl;
	}
};
//销售经理类Salesmanager的定义:
class Salesmanager :public Manager, public Salesman
{
public:
	Salesmanager()
	{
		monthlypay = 5000;
		commrate = 0.005f;
		cout << name << "所有部门月销售量:";
		cin >> sales;
	}
	void pay()
	{
		salary = monthlypay + commrate * sales;
	}
	void display()
	{
		cout << "销售经理:" << name << ",编号:" << no << ",本月工资:" << salary << endl;
	}
};
int Employee::totalno = 10000;
//主函数;
int main()
{
	Manager m1;
	Technician t1;
	Salesman s1;
	Salesmanager sm1;
	Employee* em[4] = { &m1,&t1,&s1,&sm1 };
	cout << "上述人员的基本信息:" << endl;
	for (int i = 0; i < 4; i++)
	{
		em[i]->pay();
		em[i]->display();
	}
	return 0;
}

在这里插入图片描述
上面的程序就是一个简单的应用,公司的工资管理系统。c++的灵魂就是动态联编,所以掌握好动态联编是学好c++的基础。
在这里插入图片描述

  • 30
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 61
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

心随而动

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

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

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

打赏作者

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

抵扣说明:

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

余额充值