引言: 在c++中司空见惯的事情就是:能够经过指针和引用能够实现多态,而对象不能够。 那为何?让咱们来解开这神秘的暗纱!c++
在一个类的实例中,只会存放非静态的成员变量。 若是该类中存在虚函数的话,再多加一个指向虚函数列表指针—vptr。函数
例如声明以下两个类,并分别实例化两个对象,它们的内存分配大体以下:(vptr具体在什么位置,与编译器有关,大多数都在开始处)性能
class base { public: virtual ~base() {}; virtual string GetName() { return "base"; } GetA(); int a; }; class derived : public base { public: virtual ~derived() {}; virtual string GetName() { return "derived";} GetB(); int b; };
base B1, B2;
derived D1, D2;
内存分布大体以下: spa
1. 类对象中,只有成员变量与vptr.指针
2. 普通成员函数在内存的某一位置放着。它们与c语言中定义的普通函数没有区别。 当咱们经过对象或对象指针调用普通成员函数时, 编译器会拿到它。怎么拿到呢?固然是经过名字了,编译器都会对咱们写的函数的名字进行修饰映射,让它们变成内存中惟一的函数名。code
3. 不管基类仍是子类,每一种类类型的虚函数表只有一份,它里面存放了基类的类型信息和指向基类中的虚函数的指针。 某一类类型的全部对象都指向了相同的虚函数表。对象
例如:下面的 base类型的对象B1或指针pB1,只能使用GetName() 和GetA()方法。 不管它们是如何来的!!!!!blog
// 直接构造获得 base B1; base* pB1 = new base(); // 即便从子类转换而来, 经过B1或pB1也永远访问不到GetB()方法。 derived d1; B1 = d1; pB1 = new derived();
本质上它们没有任何区别,在32/64位系统中都是4/8字节的一个变量。 惟一不一样的就是编译器解释它们的方式,即经过指针来寻址出来的对象类型不一样,大小不一样 ,指针类型来告诉编译器如何解释该指针。内存
有代码以下 :编译器
derived* _pD = new derived(); base* _pB = _pD; _pB.GetName(); // 返回 derived.
想要知道如何经过指针来实现的多态,就要看看对基类指针赋值是发生了什么! 具体来讲 以下图所示:
咱们会发现,对指针的赋值,仅仅是让基类指针_pB指向的子类对象的地地址。 当咱们使用基类指针调用GetName()函数(该函数是虚函数,它的地址在函数表中)时, 会由_pB指向的地址找到子类的虚函数表指针vptr_上海,再由vptr_上海在虚函数表中找到子类的GetName(),从而调用它。就这样实现了多态。
有代码以下:
base B1; derived D1; B1 = D1; B1.GetName(); // 返回 base base B2 = D1 B2.GetName(); // 返回 base
上面代码中不管赋值操做仍是赋值构造时, 只会处理成员变量,一个类对象里面的vptr永远不会变,永远都会指向所属类型的虚函数表,操做以下图所示:
所以,经过对象调用虚函数时,就没有必要进行动态解析了,白白增长了间接性,浪费性能。编译器直接在编译时就能够确认具体调用哪个函数了,所以没有所谓的多态。
补充说明:
1. 引用本质上也是经过指针的解引用(即*_point)来实现的,能够<<参考std源码剖析》一本书,因此引用也能够实现多态。
2. 即便经过 基类的指针调用基类的虚函数 或 经过子类的指针调用子类的虚函数 以及经过子类指针调用基类的虚函数, 也是经过多态机制来完成的(即一步步的间接性来完成)。
3. 一个空的class的对象的大小为1个字节, 编译器之因此要这么作,是为了区别同一个类类型的不一样对象!