C++之多态性与虚函数(一)

1 多态性

1.1 多态的概念

多态性:向不同的对象发送同一个消息,不同的对象在接收时会产生不同的行为(方法)。所谓消息,就是调用函数,不同的行为就是指不同的实现,即执行不同的函数。函数的重载、运算符重载都是多态现象。
C++中的多态:具有不同功能的函数可以用同一个函数名,这样就可以实现用一个函数名调用不同内容的函数。

1.2 多态的分类

多态性可以分为:静态多样性、动态多样性。
静态多态性又称编译时的多态性,通过函数重载实现。在程序运行前就已经决定了执行的函数和方法。
动态多样性又称运行时的多态性,通过虚函数实现,在程序运行过程中才动态地确定操作所针对的对象。

2 利用虚函数实现动态多态性

2.1 虚函数的作用

所谓虚函数,就是在基类声明函数是虚拟的,并不是实际存在的函数,然后在派生类中才正式定义此函数。
虚函数的作用是允许在派生类中重新定义与基类同名的函数,并且可以通过基类指针或引用来访问基类和派生类中的同名函数
通过虚函数与指向基类对象的指针变量的配合使用,就能实现动态的多态性。如果想调用同一类族中不同类的同名函数,只有先用基类指针指向该类对象即可。如果指针先后指向同一类族中的不同类的对象,就能不断地调用这些对象中的同名函数。
函数重载处理的是同一层次上的同名函数问题,而虚函数处理的是不同派生层次上的同名函数问题。
下面给出两个例子,请注意观察未使用虚函数和使用虚函数得到的结果。example1.cpp是未使用虚函数的例子,example2.cpp是使用虚函数的例子。

example1.cpp

#include<iostream>
#include<string>

using namespace std;

//基类与派生类中有同名函数

class Student {       //声明基类Student
public:                     //基类公用成员
	Student(int, string, float); //声明构造函数
    void display(); //声明输出函数
	

protected:       //基类保护成员,派生类可以访问
	int num;
	string name;
	float score;
};

//Student类成员函数的实现
Student::Student(int n, string nam, float s) { //定义构造函数
	num = n;
	name = nam;
	score = s;
}

void Student::display() { //定义输出函数
	cout << endl << " num:" << num << endl;
	cout << " name:" << name << endl;
	cout << " score:" << score << endl;
}

class Graduate :public Student {          //以public方式声明派生类Graduate
public:
	Graduate(int, string, float, float);  //声明构造函数
	void display();  //声明输出函数,与基类的输出函数同名
private:
	float wage; //津贴
};

//Graduate类成员函数的实现
Graduate::Graduate(int n, string nam, float s, float w) :Student(n, nam, s), wage(w) {} //定义构造函数


void Graduate::display() { //定义输出函数
	
	cout  << " \n\nnum:" << num << "\n name:" << name << " \nscore:" 
		<< score <<  "\n wage:" << wage << endl;
}

//主函数
int main() {
	Student stud1(1001, "Li", 87.5);  //定义Student类对象stud1
	Graduate grad1(2001, "Wang", 98.5, 1000); //定义Graduate类对象grad1
	Student *pt = &stud1;  //定义指向Student类对象的指针并指向stud1
	pt->display(); //调用stud1.display函数
	pt = &grad1;  //指针指向grad1
	pt->display();  //希望输出Graduate类对象grad1中的数据
	
	system("pause");
	return 0;
}

输出结果如图所示,学生stud1的全部数据都被输出,grad1的基类数据成员被输出,grad1的wage成员没有输出。说明调用了stud1的display函数。
在这里插入图片描述
现在思考一下为什么没有输出grad1的wage成员。
通过指向基类对象的指针,只能访问派生类中的基类成员,而不能访问派生类增加的成员。 pt是指向Student类对象的指针,因此即使指向grad1,实际上指向的也是grad1从基类继承的部分。即pt->display()调用的不是派生类Graduate对象所增加的display函数,而是基类的display函数。

如果想通过指针输出研究生grad1的wage成员,可以再定义一个指向派生类对象的指针变量ptr,使它指向grad1,然后用ptr->display()调用派生类对象的display函数。但这样比较复杂,而虚函数可以方便地解决这个问题。具体办法如下:

将基类Student中的display函数声明为虚函数,其他部分不做改动。
virtual void display();

在基类中的display被声明为虚函数,在声明派生类时被重载,这时派生类的同名函数display就取代了其基类中的虚函数。因此在使基类指针指向派生类对象后,调用display函数就调用了派生类的display函数。

example2.cpp

#include<iostream>
#include<string>

using namespace std;

//基类与派生类中有同名函数

class Student {       //声明基类Student
public:                     //基类公用成员
	Student(int, string, float); //声明构造函数
    virtual void display(); //声明输出函数为虚函数
	

protected:       //基类保护成员,派生类可以访问
	int num;
	string name;
	float score;
};

//Student类成员函数的实现
Student::Student(int n, string nam, float s) { //定义构造函数
	num = n;
	name = nam;
	score = s;
}

void Student::display() { //定义输出函数
	cout << endl << " num:" << num << endl;
	cout << " name:" << name << endl;
	cout << " score:" << score << endl;
}

class Graduate :public Student {          //以public方式声明派生类Graduate
public:
	Graduate(int, string, float, float);  //声明构造函数
	void display();  //声明输出函数,与基类的输出函数同名
private:
	float wage; //津贴
};

//Graduate类成员函数的实现
Graduate::Graduate(int n, string nam, float s, float w) :Student(n, nam, s), wage(w) {} //定义构造函数


void Graduate::display() { //定义输出函数
	
	cout  << " \n\nnum:" << num << "\n name:" << name << " \nscore:" 
		<< score <<  "\n wage:" << wage << endl;
}

//主函数
int main() {
	Student stud1(1001, "Li", 87.5);  //定义Student类对象stud1
	Graduate grad1(2001, "Wang", 98.5, 1000); //定义Graduate类对象grad1
	Student *pt = &stud1;  //定义指向Student类对象的指针并指向stud1
	pt->display(); //调用stud1.display函数
	pt = &grad1;  //指针指向grad1
	pt->display();  //调用grad1.display函数
	
	system("pause");
	return 0;
}

输出结果如下图所示。学生stud1的全部数据都被输出,grad1的全部数据也被输出。说明调用了grad1的display函数。
在这里插入图片描述

2.2 静态关联和动态关联

确定调用的具体对象的过程称为关联。在这里是指把一个函数名与一个类对象捆绑在一起,建立关联。函数重载和通过对象名调用的虚函数,在编译时即可确定其调用的虚函数属于哪一个类,其过程称为静态关联,也称早期关联。
在运行阶段确定关联关系称为动态关联,通过基类指针和虚函数结合实现多态就是动态关联的一个例子。先定义一个指向基类的指针变量,并使它指向相应的类对象,然后通过这个基类指针去调用虚函数。运行阶段基类指针指向了确定的对象,调用该对象中的函数,调用哪一个对象的函数确定无疑,这就是动态关联。动态关联也称为滞后关联

2.3 虚函数的注意事项

  1. 只能用virtual声明类的成员函数,使其作为虚函数。不能声明类外的普通函数为虚函数。
  2. 一个成员函数被声明为虚函数后,在同一类族中的类就不能再定义一个非virtual的但与该虚函数具有相同的参数(包括个数和类型)和函数返回值类型的同名函数。
  3. 当一个成员函数被声明为虚函数后,其派生类中的同名函数都自动成为虚函数。
  4. 在派生类中重新定义此函数,函数名、函数类型、函数参数个数和类型必须与基类的虚函数相同,根据派生类的需要重新定义函数体。
  5. 使用虚函数,系统要有一定的空间开销。当一个类带有虚函数时,编译系统会为该类构造一个虚函数表,是一个指针数组,存放每个虚函数的入口地址。

2.4 虚析构函数

当派生类的对象从内存在撤销时一般先调用派生类的析构函数,然后再调用基类的析构函数。但是,如果用new运算符建立了临时对象,若基类中有析构函数,并且定义了一个指向该基类的指针变量。在程序用带指针参数的delete运算符撤销对象时,系统只会执行基类的析构函数,而不执行派生类的析构函数。
可以把基类的析构函数声明为虚函数,这样所有派生类的析构函数自动成为虚函数,即使派生类的析构函数与基类的析构函数名字不相同。如果程序中显式地用了delete运算符准备删除一个对象,而delete运算符的操作对象用了指向派生类对象的基类指针,则系统会采用动态关联,调用相应类的析构函数,以保证在撤销动态分配空间时得到正确的处理。
构造函数不能声明为虚函数。这是因为在执行构造函数时类对象还未完成建立过程,当然谈不上把函数与类对象的绑定。

参考文献

《C++程序设计(第三版)》谭浩强编著

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值