从C++使用者角度看多态
1 即使没有虚函数也能重写方法
多态需要两个特性:
(1)方法重写(override):父类与子类具有函数签名完全相同的方法。
(2)向上类型转换(upcasting):用一个父类指针指向子类对象的时候,假如调用的是虚函数,会自动暂时将该指针转换为子类类型的指针。
虚函数的存在就是为了类型转换,即使没有虚函数也能重写方法。虚函数并不是为了解决函数重写问题的。假如你去在父类和子类中都写上函数签名相同的方法,同样也能重写函数。
例如 我们先写一个没有虚函数的例子
#include <iostream>
using namespace std;
class Base
{
public:
void func(){cout<<"Base method!\n";};
};
class Derived: public Base
{
public:
void func(){cout<<"Derived method!\n";};
};
int main()
{
Base obj_base;
Derived obj_derived;
obj_base.func();
obj_derived.func();
}
结果为
Base method!
Derived method!
是完全没问题的!
是的,即使不加virtual,也能实现方法重写!
2 即使不用虚函数,子类对象也能调用父类同名方法
假如我用子类对象调用父类同名的方法,能行吗?
也是能的!(注意这种很特别的写法)
int main()
{
// Base obj_base;
Derived obj_derived;
// obj_base.func();
obj_derived.Base::func();
}
打印结果
Base method!
所以,完全不需要虚函数,也能用子类对象调用父类方法!
(当然,父类中不同名的方法,不需要增加作用域符号Base::就能直接调用)
3 没有虚函数的基类指针还是基类指针,不会类型转换
我们给定一个指针p,这个p是基类指针,指向的是子类对象。
int main()
{
Base *p = new Derived();
p->func();
}
结果是
Base method!
也就是说,假如没有虚函数,基类的指针还是指向基类的。基类的指针不知道子类对象。
这一点我们还可以进一步证明。
当我们在子类中增加一个子类特有的函数
void derived_unique(){cout<<"Derived_unique method!\n";};
然后调用该函数的时候
int main()
{
Base *p = new Derived();
p->derived_unique();
}
就会报错
error: 'class Base' has no member named 'derived_unique'
p->derived_unique();
所以不用virtual关键字,基类指针还是基类指针,即使它指向的是子类对象,它还是只知道基类的方法。
4 虚函数:只是做了指针类型转换
我们接下来只是增加virtual关键字。(和前面代码的唯一区别就是加了virtual)
#include <iostream>
using namespace std;
class Base
{
public:
virtual void func(){cout<<"Base method!\n";};
};
class Derived: public Base
{
public:
virtual void func(){cout<<"Derived method!\n";};
};
int main()
{
Base *p = new Derived();
p->func();
}
结果是
Derived method!
也就是说,virtual只是自动做了指针的类型转换。原本是基类的指针,因为有了virtual,现在变成了子类的指针。
注意,这个类型转换只是暂时的。而且是只有在调用虚函数的时候才出现的。也就是说,当编译器看到你的指针调用的是virtual方法的时候,自动的给你暂时转换为子类方法。而调用完毕,再自动转换回来。
这点很容易证明:
假如基类中有个基类才有的方法
void base_unique(){cout<<"base_unique method!\n";};
调用该方法
int main()
{
Base *p = new Derived();
p->base_unique();
}
那么完全没有问题
打印结果
base_unique method!
也就是说,这个类型转换只是暂时的。
总结
至此,我们总结一下。
多态具有两个特性:
(1)方法重写(override):父类与子类具有函数签名完全相同的方法。
(2)向上类型转换(upcasting):用一个父类指针指向子类对象的时候,假如调用的是虚函数,会自动暂时将该指针转换为子类类型的指针。
这两个特性完全是分开的。特性1即使不用任何virtual关键字也是能实现的。特性2才是借助了virtual关键字。所以,对于使用者来说:虚函数其实就是向上类型转换。而且这个类型转换是暂时的,只有调用虚函数的时候才暂时转换过去。
从C++设计者(内存)角度看多态
以上是从C++的使用者角度来看的,即使不知道虚表和虚指针,也完全不影响我们的使用。 现在我们从C++设计者角度(或从内存的角度)来看虚函数。通过从设计者的角度来看,我们就知道增加虚函数对我们的性能影响如何。
多态的向上转型真的是一种类型转换吗?
在使用者的眼中,向上类型转换是如此神奇的一个东西:你使用虚函数的时候,它就自动转型成具体的子类指针了,使用完了,再转换回去。你大概会以为:
他先做了一个
(Derived *)p
再做了一个
(Base*)p
但是,真的是这样吗?
答案是否定的。C++实现的方法是,通过对每个对象存入一个指针。这个指针从功能上讲是个二级指针。它被对象的指针(对外部调用者来说就是我们的p指针,对对象内部来说就是this指针)所指。它指向一个数组。
这个二级指针就是虚指针。
这个数组就是虚表。该数组中的每个元素都是指针。这些指针所存储的就是虚函数的地址。
从内存角度看虚指针
首先我们要记住:虚表和虚指针是为了多态性而存在的,假如类中没有虚函数(比如只有普通成员函数),那么就没有虚表和虚指针。
虚表是什么:只是一个数组,里面的每个元素存放的是虚函数的地址。
虚指针是什么:只是一个指针,指向虚表。
需要特别注意的是:虚表是对于类而言的,虚指针则是针对对象而言的。
也就是说,可以认为虚表内存存在于类内存中(代码区),每一个类只需要一份就可以了。虚指针则存在于对象的内存(堆栈区)里,每一个对象就有一个虚指针。假如某个类实例化了10000个对象,那么虚指针就要占用10000*8字节(假设每个指针占用8字节),而虚表的占用内存则完全不变。
因此:使用虚函数的开销就是每个对象增加了一个指针的内存开销。
假如只有普通成员函数
我们先来看看只有普通成员函数的对象内存布局。这时候,是没有虚表和虚指针的。
假如类A只有成员函数
class A
{
void func1();
void func2();
};
这时候去实例化A,其对象内存占用是1(任何空类的占用内存都是1)。
原因在于:普通成员函数的函数指针是不存放在对象内存内部的。可以理解为:从对象模型上讲,普通成员函数和全局函数没有什么区别。
我们要去调用某个对象a的成员函数的时候:
a.func1()
其实是把这个对象a的地址(也就是this指针)传递给func1,func1通过this指针就可以找到任何该对象的数据了。
虚函数的真正实现方法:虚指针和虚表
C++通过虚指针,是怎么实现多态性的呢?
我们只要当p指向父类对象的时候,指向的是父类的vptr;当p指向父类对象的时候,指向的是子类的vptr,
根据这个指针是子类指针还是父类指针,就会分别指向子类和父类的虚表。
这样,就实现了多态。
从抽象角度看多态:为什么要有多态?
1 继承是种属关系,指针是代词
我们先要理解,继承关系不是爸爸儿子的关系(这也是名字造成的通常的一个误解)。父类与子类之间是种属关系。例如狗和动物的关系就是种属关系。狗一定是一种动物。动物具有的特性狗都有。也就是说:父类与子类是抽象与具体的关系。这才叫做继承。
正因为狗也是一种动物,所以我们用一个代词指代狗的时候,既可以说:“那只狗。”
也可以说:“那只动物。”
但是没有人把这种关系反过来指代。我们不能指代任何一种动物为”那只狗“。因为显然动物不只有狗这一种。你只能用抽象的东西指代具体的东西,但是不能反过来用具体的东西指代抽象的东西。
那个代词,就是我们所用的指针。
所以,我们完全可以用”那只动物“这种抽象的代词来指代一只狗。也就是说,我们可以用基类指针来指代基类对象或者子类对象。
2 指代不清的问题
假如我们把多态的两个特性结合起来,就会出现一个问题:
既然我们的指针既能指向父类对象,也能指向子类对象,那编译器怎么知道该调用哪个方法?
我们先来看看为什么会有这个问题:
假如我们不用指针up casting的时候,完全就没有这个顾虑。也就是在第3节中的代码:
A a;
ASon aSon;
a.func1();
aSon.func1();
编译器完全能够分辨。因为我们直接用的是对象a和对象aSon。对象a一定是父类的对象,对象aSon一定是子类的对象。在不用代词,直呼其名的时候,是没有指代的混淆的。
例如我家的狗叫做小白。当我称呼小白的时候,它一定就是那条狗。用对象的名字称呼对象,没有任何不清楚的地方。
但是假如用代词称呼对象,那么就会出现指代不清的问题。比如我有一只猫,一只狗。我统一用”那只动物“来称呼他们。那就没法分辨指的是谁了。
也就是说:用指针去指代对象的时候,就会出现指代不清的问题。直接用对象名就没有这个问题。
所以,假如不用指针,直接用对象名,不会出现多态的问题。