C++的多态主要通过函数重载和虚函数实现。
1.重载
重载属于静态多态,因为重载是编译时期完成的。
C++允许函数名相同,但是参数列表不同。比如定义func(int a),func(int b)函数,便可以通过传入的参数不同实现不同的处理,即实现多态。
具体来讲,编译器会通过将函数的函数名,参数列表(包括参数类型以及限定符const,某些时刻const可能无效,具体看下面),函数的限定符(包括const,volatile)组合为一个唯一的函数标识符,这就允许编程时定义相同的函数名而不影响汇编代码。
需要注意的是,函数返回值类型不在重载的范围。并且,如果对形参进行const限定也是不可以重载的(函数形参无法改变实际的值,故const无意义)。
class C:public A,public B{
public:
int func2(const int a){ //无法重载,因为对一个形参进行const限定无意义
cout<<"c"<<endl;
}
int func2(int a){
return 1;
}
int func1(int &a){
return 1;
}
int func1(const int &a){//可以重载,参数为引用时可能会改变参数的值,因而const有意义。
return 1;
}
};
2.虚函数
虚函数允许运行时根据对象指针的实际内容来确定调用父类还是子类的同名方法。属于动态多态。
我们先从类的存储方式学起。如果我定义一个没有虚函数的类,那么该类的对象的内存分布为:
---成员变量1--
--成员变量2--
-- ...--
--成员变量n--
而该类的成员函数统一存储在代码区域,一个类只有一份,多个对象共享。
当一个类继承另一个类时,派生类对象内存为下:
--父类成员变量1--
--父类成员变量2--
--...
--父类成员变量n--
--子类成员变量1--
--...
--子类成员变量n--
两个类的成员函数仍保存在代码区域。访问某个对象的某个类即访问对象地址加变量偏移得到的地址。因而我们可以发现,无论是把子类指针赋给父类指针,还是把父类指针赋给子类指针(由于子类指针可能访问只属于它的变量,这种转变是不安全的,编译器不允许直接转换,可以使用dynamic_cast转换或以void *作为中间变量来实现),都是可以的,因为父类和子类中相同的成员变量偏移是一致的。
再来看多继承,派生类对象的内存如下:
--父类1成员变量1--
--..
--父类1成员变量n--
--父类2成员变量1--
--..
--父类2成员变量n--
--...
--子类成员变量n--
父类成员按其继承顺序排布,最后加上子类成员。
需要注意的是,多继承时如果用该派生类指针赋给某个父类指针,父类指针实际上会指向其成员变量开始的位置,如上述例子:父类2指向父类2成员变量1。因此,多继承下,即使两个父类指针指向同一变量也不代表这两个指针值相同。
我们加入虚函数,虚函数使用virtual关键字声名。引入虚函数的某个类的对象储存方式如下
--虚函数表指针vfptr
--成员变量1--
--成员变量2--
-- ...--
--成员变量n--
虚函数表指针指向一个虚函数表,表内的每一项都是一个函数的地址。调用虚函数时,会先找到该类对于的虚函数,再转入并执行。
让我们再看一下继承的情况:假设一个类继承了基类重写了父类的虚函数,并且自己写了一个虚函数。情况如下:
--虚函数表指针vfptr
--成员变量1--
--成员变量2--
-- ...--
--成员变量n--
虚表为: --父类虚函数1地址(实际指向自己重写的函数)
--子类虚函数1地址(指向自己声名的函数)
这样,将一个子类指针赋给父类指针并调用虚函数1时,会在虚函数表里找到虚函数1的地址,父类对象指向父类实现的,子类对象指向子类实现的,这就实现了多态(根据实际存储类型来确定调用函数)
对于多继承,每个父类会有自己的虚函数表指针,子类新加的虚函数只会添加进第一个父类的虚函数表,其他父类的虚函数表里子类重载的虚函数指向的地址可能会先对this指针进行操作(因为子类this和父类指针可能不同,子类重写的应该传子类的this指针)。