1.概念解释
在c++程序设计中,多态性是指具有不同功能的函数可以是同一个函数名。所以函数重载跟运算符重载都是函数的多态性的体现。
其实多态性在生活中也有体现,比如说校长发布说明天要开学了,对于这么同一个消息,学生要补作业,老师要备课,家长要早起做饭...。
从系统实现的叫角度来看,多态性分为静态多态性和动态多态性。静态多态性是在程序编译时就能决定调用哪一个的,有被称为编译时的多态性。静态多态性就是通过函数重载实现的。动态多态性是在程序运行过程中才动态地确定操作所针对的对象。又被称为运行时多态性。动态多态性是通过虚函数实现的。
2.对于静态多态性的回顾
来看一段代码:
#include<iostream>
using namespace std;
class point//这是点类
{
protected:
int _x;
int _y;
public:
point(int x=0 , int y=0 )
{
_x = x;
_y = y;
}
friend ostream& operator<<(ostream& output, const point& p)
{
output <<"圆心:" << p._x << ',' << p._y << endl;
return output;
}
};
class circle :public point//这是圆类
{
protected:
int _r;
public:
circle(int x=0,int y=0,int r=0):point(x,y)
{
_r = r;
}
float area()
{
return 3.14 * _r * _r;
}
friend ostream& operator<<(ostream& output,const circle& c)
{
output << "圆心:" <<c._x << ','<<c._y <<"半径" << c._r << endl;//这个是操作符重载函数,跟上面一个类的函数构成函数重载
return output;
}
};
class cylinder:public circle//这是圆柱类
{
protected:
int _h;
public:
cylinder(int x = 0, int y = 0, int r = 0, int h = 0) :circle(x, y, r)
{
_h = h;
}
float volume()
{
return 3.14 * _r * _r * _h;
}
float area()//注意这里是跟上面的一个函数完全一样的,参数,函数名返回类型都是一样的,这个地方不是函数重载,是基类和派生类之间的同名覆盖
{
return (3.14 * _r * _r * 2 + 3.14 * _r * 2 * _h);
}
friend ostream& operator<<(ostream& output, const cylinder& ch)
{
output << "圆心:" << ch._x << ',' << ch._y << ' ' << "半径:" << ch._r << ' ' << "高:" << ch._h << endl;
return output;
}
};
int main()
{
cylinder ch(2,2,1,3);
point& pref = ch;//注意这里的引用:定义了point类的引用变量pref,并用派生类的cylinder对象ch来初始化。
//派生类对象是可以代替基类对象为基类对象的引用初始化的,他就是ch基类部分的别名,与ch中的基类共享一段存储空间
circle& cref = ch;
cout << ch;
cout << pref;
cout << cref;
cout << ch.area() << endl;
cout << cref.area() << endl;
return 0;
}
3.虚函数
我们已经知道在同一个类中不能同时定义两个名字相同,参数个数相同和返回类型都相同的函数,否则就是重复定义。但是在类的继承层次结构中就可以出现完全相同的函数。编译系统会按照同名覆盖的原则决定调用的对象,就像上面的例子,调用cylinder类中的area函数:ch.area();调用基类circle中的area函数:ch.circle::area();用这种方法区分两个同名函数,但是这样很不方便。
有一个想法是通过一个基类类型的指针,改变指针的指向来访问不同类的同名函数。(请看下面代码)
#include<iostream>
#include<string>
using namespace std;
class student
{
protected:
int _num;
string _name;
float _score;
public:
student(int num, string name, float score)
{
_num = num;
_name = name;
_score = score;
}
void display()
{
cout << _num << ' ' << _name << ' ' << _score << endl;
}
};
class graduate:public student
{
protected:
int _pay;
public:
graduate(int num,string name,float score,int pay):student(num,name,score)
{
_pay = pay;
}
void display()
{
cout << _num << ' ' << _name << ' ' << _score << ' '<<_pay<<endl;
}
};
int main()
{
student stu(1,"zhangsan",100.0);
graduate gra(1, "zhangsan", 100.0, 5000);
student* p1 = &stu;
p1->display();
p1 = &gra;
p1->display();
return 0;
}
运行结果如下:
其实p1指针在改变的时候是改变了其指向的内存空间的,但是调用的还是基类的display函数。但是如果我们将student类中的display函数前面加上virtual关键字,使其变成一个虚函数就能解决这个问题:
#include<iostream>
#include<string>
using namespace std;
class student
{
protected:
int _num;
string _name;
float _score;
public:
student(int num, string name, float score)
{
_num = num;
_name = name;
_score = score;
}
virtual void display()
{
cout << _num << ' ' << _name << ' ' << _score << endl;
}
};
class graduate:public student
{
protected:
int _pay;
public:
graduate(int num,string name,float score,int pay):student(num,name,score)
{
_pay = pay;
}
void display()
{
cout << _num << ' ' << _name << ' ' << _score << ' '<<_pay<<endl;
}
};
int main()
{
student stu(1,"zhangsan",100.0);
graduate gra(1, "zhangsan", 100.0, 5000);
student* p1 = &stu;
p1->display();
p1 = &gra;
p1->display();
return 0;
}
这就是虚函数!
说明:本来基类指针是用来指向基类对象的,如果用它来指向派生类对象,则进行指针了类型转换,将派生类类型的指针转换成基类的指针,所以原来的基类指针指向的是派生类对象的基类部分。(可见派生类中的成员跟基类成员是没有任何关系的,是不同地址的东西,可以推测存储时基类成员跟派生类成员是分开的,仅仅是我的推测,有佬直到答案可以发在评论区,让我学习一下)。
可以看到虚函数允许同名函数但是不会同名覆盖。
4.虚函数的使用方法
1.在基类用virtual声明成员函数为虚函数,这样就可以在派生类中重新定义此函数,赋予心得功能。(注意声明加了virtual后,在类外定义这个函数的时候不用再加virtual)
2.在派生类中重新定义此函数时要求两个函数函数名,返回值,参数与基类完全一样,并根据派生类的要求重新定义函数。
3.c++规定当一个成员函数被声明为虚函数后,其派生类中的同名函数都自动变成虚函数。(注意:一定要在基类中声明为虚函数)。
在派生类中声明同名函数时也可以加virtual。也可以不加,但习惯上是都加上。如果在派生类中没有对基类函数的虚函数重新定义,则派生类则简单继承其直接基类的虚函数。
4.定义一个指向基类对象的指针变量,并使它指向同一类族中的某一个对象。(不能定义一个派生类的指针,然后去指向基类,想想为什么可以定义基类指向派生类你就明白了)
5.通过该指针变量丢调用此虚函数,此时调用的就是指针所指向的对象的同名函数。
需要说明:有时再基类中定义的非虚函数会在派生类中重新定义,如果用基类指针调用该成员函数,则调用基类部分的成员函数。
如果用派生类指针调用该成员函数,则回调用派生类部分的成员函数。这非常的理所当然。这完全不是虚函数,仅仅是不同类型的的指针调用不同的类的同名函数,只是同名覆盖罢了。
5.静态关联与动态关联
前面所说的函数重载跟通过对象名调用的虚函数,再编译的时候就能确定调用的是哪一个虚函数是属于哪一个类,其过程称为静态关联,又叫早期关联,函数重载属于静态的关联。
但是在使用指针的时:先定义了一个指向基类的指针变量,并使它指向相应的类对象,然后通过这个基类指针去调用虚函数,编译阶段编译器是无法确定调用的是哪一个对象的虚函数的。因为编译只做静态的语法检查,光从语句的形式是无法确定调用对象的。
这样的情况,编译系统把他放到运行阶段处理,在运行阶段确定关联关系。在运行阶段,基类指针先指向了某一个类对象,然后通过该指针调用该对象的函数。由于实在运行阶段将虚函数跟类对象绑定到一起的,因此这个过程又叫动态关联。这种多态性叫做动态多态性,即运行阶段的多态性。
6.虚函数的注意事项
1.只能用virtual来声明类中的成员函数为虚函数,不能将类外的普通函数声明为虚函数。因为虚函数的作用是允许在派生类中对基类的虚函数重新定义,很显然这个过程是发生在类的继承层次中的。
2.当一个成员函数被定义为虚函数时在同一个类中不能再出现与该函数完全形同的函数
3.在程序中最好将析构函数声明为虚函数,以防只析构了基类。
4.很多时候我们再基类声明的一个虚函数对于基类是没有作用的,仅仅是一个声明,具体的用处在派生类中。
7.纯虚函数
就如上面所说基类中的虚函数在如果在基类中仅仅是一个声明,在基类中并不使用此函数,为了简化,我们可以不写出这种没有意义的函数体,只给出函数原型,并在后面加上"=0",如:virtual float area()=0;纯虚函数实在声明虚函数时被初始化成0的函数。
注意:纯虚函数没有函数体,最后面的"=0"并不表示函数的返回值,她只起到形式上的作用,告诉编译系统这是纯虚函数,这是一个声明语句后面应该有分号。纯虚函数只有函数的名字而没有函数的的功能,不能被调用。
8.抽象类(简单说一下)
如果声明了一个类,一般可以用它来定义对象,但是在c++程序设计时,有的类被定义出来的目的就不是类创建对象的,而是仅仅作为基类,来实现定义派生类的,这样的类就是抽象类,也叫抽象基类。