C++多态实现
什么是多态?
多态是同一个行为具有多个不同表现形式或者形态的能力,简单的概括为“一个接口,多种方法”,是面向对象领域的核心概念
c++支持两种多态性:编译时多态,运行时多态
- 编译时多态(静态多态):通过重载函数实现
- 运行时多态(动态多态):通过虚函数实现
c++运行时多态是通过虚函数来实现的,虚函数允许子类重新定义函数,而子类重新定义父类的做法称为覆盖/重写(Override),或者称为重写
重写/覆盖
- 重写(Override):是父类与子类之间的多态性,是子类对父类函数的重新实现。函数名和参数与父类一样,子类与父类函数体内容不一样。子类返回的类型必须与父类保持一致;子类方法访问修饰符的限制一定要大于父类方法的访问修(public>protected>private)子类重写方法一定不能抛出新的检查异常或者比被父类方法申明更加宽泛的检查型异常。
- 重写实现于子类中。重写可以有两种,直接重写成员函数和重写虚函数,只有重写了虚函数的才能算作是体现了C++多态性。
class commodity {
public:
virtual void buy(){
printf("buy a commodity");
}
};
class milk :public commodity {
public:
void buy() {
printf("buy a milk");
}
};
重载
- 重载(Overload):是一个类中多态性的一种表现,指同一个类中不同的函数使用相同的函数名,但是函数的参数个数或类型不同。可以有不同的返回类型;可以有不同的访问修饰符;可以抛出不同的异常。调用的时候根据函数的参数来区别不同的函数。
- 重载实现于一个类中,重载则是允许有多个同名的函数,而这些函数的参数列表不同,允许参数个数不同,参数类型不同,或者两者都不同。编译器会根据这些函数的不同列表,将同名的函数的名称做修饰,从而生成一些不同名称的预处理函数,来实现同名函数调用时的重载问题。但这并没有体现多态性。
int max(int a,int b){
return a > b ? a : b;
};
double max(double a,double b){
return a > b ? a : b;
}
- 函数返回值类型与构成函数重载无任何关系;
- 类的静态成员函数与普通成员函数可以形成重载;
- 函数重载发生在同一作用域,如类成员函数之间的重载、全局函数之间的重载。
函数隐藏
函数隐藏指不同作用域中定义的同名函数构成函数隐藏(不要求函数返回值和函数参数类型相同)。比如派生类成员函数屏蔽与其同名的基类成员函数、类成员函数屏蔽全局外部函数。请注意,如果在派生类中存在与基类虚函数同返回值、同名且同形参的函数,则构成函数重写。
void func(char* s){
cout<<"global function with name:"<<s<<endl;
}
class A{
void func(){
cout<<"member function of A"<<endl;
}
public:
void useFunc(){
//func("lvlv");//A::func()将外部函数func(char*)隐藏
func();
::func("lvlv");
}
virtual void print(){
cout<<"A's print"<<endl;
}
};
class B:public A{
public:
void useFunc(){ //隐藏A::void useFunc()
cout<<"B's useFunc"<<endl;
}
int useFunc(int i){ //隐藏A::void useFunc()
cout<<"In B's useFunc(),i="<<i<<endl;
return 0;
}
virtual int print(char* a){
cout<<"B's print:"<<a<<endl;
return 1;
}
//下面编译不通过,因为对父类虚函数重写时,需要函数返回值类型,函数名称和参数类型全部相同才行
// virtual int print(){
// cout<<"B's print:"<<a<<endl;
// }
};
对比函数隐藏与函数重载的定义可知:
- 派生类成员函数与基类成员函数同名但参数不同。此时基类成员函数将被隐藏(注意别与重载混淆,重载发生在同一个类中)
- 函数重载发生在同一作用域,函数隐藏发生在不同作用域。
虚函数
虚函数 是在基类中使用关键字 virtual 声明的函数。在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数。
我们想要的是在程序中任意点可以根据所调用的对象类型来选择调用的函数,这种操作被称为动态链接,或后期绑定。
纯虚函数
可能想要在基类中定义虚函数,以便在派生类中重新定义该函数更好地适用于对象,但是您在基类中又不能对虚函数给出有意义的实现,这个时候就会用到纯虚函数。
class Shape {
protected:
int width, height;
public:
Shape( int a=0, int b=0)
{
width = a;
height = b;
}
// pure virtual function
virtual int area() = 0;
};
virtual int area() = 0;
来告诉编译器函数没有主体,这个虚函数是纯虚函数
注意:
- 只有类的成员函数才能声明为虚函数,虚函数仅适用于有继承关系的类对象。普通函数不能声明为虚函数。
- 静态成员函数不能是虚函数,因为静态成员函数不受限于某个对象。
- 内联函数(inline)不能是虚函数,因为内联函数不能在运行中动态确定位置。
- 构造函数不能是虚函数。
- 析构函数可以是虚函数,而且建议声明为虚函数。
三者 | 作用域 | 有无virtual | 函数名 | 形参列表 | 返回值类型 |
---|---|---|---|---|---|
重载 | 相同 | 可有可无 | 相同 | 不同 | 可同可不同 |
隐藏 | 不同 | 可有可无 | 相同 | 可同可不同 | 可同可不同 |
重写 | 不同 | 有 | 相同 | 相同 | 相同(协变) |
实现方式
多态是面向对象的最主要的特性之一,是一种方法的动态绑定,实现运行时的类型决定对象的行为。多态的表现形式是父类指针或引用指向子类对象,在这个指针上调用的方法使用子类的实现版本。
在C++中通过虚函数表的方式实现多态,每个包含虚函数的类都具有一个虚函数表(virtual table),在这个类对象的地址空间的最靠前的位置存有指向虚函数表的指针。在虚函数表中,按照声明顺序依次排列所有的虚函数
class Base {
public:
virtual void f() {
printf("Base::f()");
}
virtual void g() {
printf("Base::g()");
}
};
class Derived: public Base {
public:
virtual void f() {
printf("Derived::f()");
}
};
由于C++在运行时并不维护类型信息,所以在编译时直接在子类的虚函数表中将被子类重写的方法替换掉,这个方法会被放到虚函数表中原来父函数在的位置。由于在编译时就确定了虚函数在虚表中的下标,所以在进行虚函数调用时,直接根据下标进行访问。
在调用b->f()时,内部会转化成(*b->vptr[1])(),由于虚函数表需要完成RTII,所以虚函数表的第一个slot存放的是type info,虚函数下标从1开始。实际上,虚函数表记录了这个类的所有虚函数的具体实现(就是在运行时确切要调用的),编译时就可以确定,不需要动态查找,效率较高。