目录
遇到了c++钻石继承(菱形继承)的问题,看看类继承中的内存分布情况吧,有助于理解
https://www.jianshu.com/p/02183498a2c2
虚函数表
早期实验
我们来观察一下类的内存分布,大部分编译器都提供了查看C++代码中类内存分布的工具,在Visual Studio中,右击项目,在属性(Properties)-> C/C++ -> 命令行(Command Line)-> 附加选项(Additional Options)
中输入/d1 reportAllClassLayout
即可在输出窗口中查看类的内存分布。对于上述代码中的Animal类和Human类,内存的分布如下:
如果写上/d1 reportSingleClassLayoutXXX(XXX为类名),则只会打出指定类XXX的内存布局。近期的VS版本都支持这样配置。
先定义了四个类分别是 动物、人类、狗和人狗,因为下面有涉及到钻石继承的问题
class Animal {
public:
char *name;
void breathe() {
cout << "Animal breathe" << endl;
}
virtual void eat() {
cout << "Animal eat" << endl;
}
};
class Human : public Animal {
public:
int race;
void breathe() {
cout << "Human breathe" << endl;
}
void eat() {
cout << "Human eat" << endl;
}
};
class dog :public Animal {
public:
int run;
/*
void eat() {
cout << "Dog eat" << endl;
}
*/
};
class humandog :public Human, public dog {
public:
int work;
void eat() {
cout << "Humandog eat" << endl;
}
};
好,现在我们打印出dog类的humandog类的内存分布:
1>
1>class dog size(12): 狗类
1> +---
1> 0 | +--- (base class Animal)
1> 0 | | {vfptr}
1> 4 | | name
1> | +---
1> 8 | run
1> +---
1>
1>dog::$vftable@:
1> | &dog_meta
1> | 0
1> 0 | &Animal::eat
1>
1>class humandog size(28): 人狗类
1> +---
1> 0 | +--- (base class Human)
1> 0 | | +--- (base class Animal)
1> 0 | | | {vfptr}
1> 4 | | | name
1> | | +---
1> 8 | | race
1> | +---
1>12 | +--- (base class dog)
1>12 | | +--- (base class Animal)
1>12 | | | {vfptr}
1>16 | | | name
1> | | +---
1>20 | | run
1> | +---
1>24 | work
1> +---
1>
1>humandog::$vftable@Human@:
1> | &humandog_meta
1> | 0
1> 0 | &humandog::eat
1>
1>humandog::$vftable@dog@:
1> | -12
1> 0 | &thunk: this-=12; goto humandog::eat
1>
1>humandog::eat this adjustor: 0
可以看到,dog类就简单地继承了animal类的成员函数和成员变量,没有重写成员函数,因此他的内存分布是:类animal的虚指针、name变量,然后才是自己的run变量。
再看humandog类:分别是:human类的虚指针、变量;dog类的虚指针、变量;最后才是自己的work变量,注意,在虚函数表中eat是他自己的实现,所以指向humandog的成员函数。这里的human类有问题,因为他钻石继承了human和dog类,这导致他的breath方法不可用,因为指向不明确,因此我们要用到虚继承。
有上面的内存分布可以看出:
- 一个类中的某个方法被声明为虚函数,则它将放在虚函数表中。
- 当一个类继承了另一个类,就会继承它的虚函数表,虚函数表中所包含的函数,如果在子类中有重写,则指向当前重写的实现,否则指向基类实现。若在子类中定义了新的虚函数,则该虚函数指针在虚函数表的后面(如Human类中的breathe,在eat的后面)。
- 在继承或多级继承中,要用一个祖先类的指针调用一个后代类实例的方法,若想体现出多态,则必须在该祖先类中就将需要的方法声明为虚函数,否则虽然后代类的虚函数表中有这个方法在后代类中的实现,但对祖先类指针的方法调用依然是早绑定的。
将human和dog改成虚继承于animal:
1>class dog size(16):
1> +---
1> 0 | {vbptr}
1> 4 | run
1> +---
1> +--- (virtual base Animal) 虚继承的话是放在后面的
1> 8 | {vfptr}
1>12 | name
1> +---
1>
1>dog::$vbtable@:
1> 0 | 0
1> 1 | 8 (dogd(dog+0)Animal)
1>
1>dog::$vftable@:
1> | -8
1> 0 | &Animal::eat
1>vbi: class offset o.vbptr o.vbte fVtorDisp
1> Animal 8 0 4 0
1>
1>class humandog size(28):
1> +---
1> 0 | +--- (base class Human)
1> 0 | | {vbptr}
1> 4 | | race
1> | +---
1> 8 | +--- (base class dog)
1> 8 | | {vbptr}
1>12 | | run
1> | +---
1>16 | work
1> +---
1> +--- (virtual base Animal) 祖先类放在最后
1>20 | {vfptr}
1>24 | name
1> +---
1>
1>humandog::$vbtable@Human@:
1> 0 | 0
1> 1 | 20 (humandogd(Human+0)Animal)
1>
1>humandog::$vbtable@dog@:
1> 0 | 0
1> 1 | 12 (humandogd(dog+0)Animal)
1>
1>humandog::$vftable@:
1> | -20
1> 0 | &humandog::eat 除了他自己重写的eat方法,其他的都继承,注意breath方法继承于human中重写的那一份
1>
1>humandog::eat this adjustor: 20
1>vbi: class offset o.vbptr o.vbte fVtorDisp
1> Animal 20 0 4 0
除了他自己重写的eat方法,其他的都继承,注意breath方法继承于human中重写的那一份
但是,如果中间类human和dog中都重写了breathe方法,那么还是会有指定不明确的问题,所以只能有一个中间类重写,但是这种情况也确实不应该出现,humandog怎么能继承两个拥有完全一样的成员方法的类呢,这样他就不知道要跟着谁breathe了
还有,虚继承的话祖先类的内容是放在最后的。
我们可以观察到,一个子类虚继承自另一个基类,它不再像普通继承那样直接拥有一份基类的内存结构,而是加了一个虚表指针vbptr指向虚基类,这个虚基类在msvc中被放在的类的内存空间的最后。这样,当出现类似这里的菱形继承时,基类Animal在子类Humandog中出现一次,子类Humandog所包含的Human类和dog类各有一个虚基类指向虚基类。从而避免了菱形继承时的冲突。见下图:
总之,C++多态的核心,就是用一个更通用的基类指针指向不同的子类实例,为了能调用正确的方法,我们需要用到虚函数和虚继承。在内存中,通过虚函数表来实现子类方法的正确调用,通过虚基类指针,仅保留一份基类的内存结构,避免冲突。
所谓虚,就是把“直接”的东西变“间接”。成员函数原先是由静态的成员函数指针来定义的,而虚函数则是由一个虚函数表来指向真正的函数指针,从而达到在运行时,间接地确定想要的函数实现。继承原先是直接将基类的内存空间拷贝一份来实现的,而虚继承则用一个虚基类指针来指向虚基类,避免基类的重复。
问题:一个虚表是属于一个类的还是属于一个对象的?
结论:如果一个类有虚函数,那么这个类的所有对象共享一个虚函数表。
这位老兄分析得很好: https://blog.youkuaiyun.com/a_big_pig/article/details/78018194
同时,多态的实现就是通过子类重写父类的虚函数,在虚表中替换掉父类的函数指针来实现的,即子类的这个函数指针指向的地址是它自己实现的那个。
其实不管有多少类的对象,虚函数表就这么固定的几个,是与类的种类个数相同的(所以说相同类所有对象共享的虚函数表),因为是固定的东西,那么在编译的时候产生就可以了, 而多态的实现是通过对象中的vptr指针指向不同的虚函数表实现的,在运行的时候指针指向是可以有变化的,所以需要在程序运行的时候变化!
2019.9.3又做了一次:
首先是没有虚继承的情况:
#include <iostream>
#include <string>
#include <vector>
using namespace std;
class grandfather {
public:
virtual void run() {
cout << "grandfather run" << endl;
}
void eat() {
cout << "grandfather eat" << endl;
}
int a;
};
class uncle : public grandfather {
public:
virtual void run() {
cout << "uncle run" << endl;
}
void eat() {
cout << "uncle eat" << endl;
}
};
class daddy :public grandfather {
public:
virtual void run() {
cout << "daddy run" << endl;
}
void eat() {
cout << "daddy eat" << endl;
}
};
class son :public uncle, public daddy {
public:
int b;
};
int main() {
son test;
//test.eat();
system("pause");
return 0;
}
1>class son size(20):
1> +---
1> 0 | +--- (base class uncle)
1> 0 | | +--- (base class grandfather)
1> 0 | | | {vfptr}
1> 4 | | | a
1> | | +---
1> | +---
1> 8 | +--- (base class daddy)
1> 8 | | +--- (base class grandfather)
1> 8 | | | {vfptr}
1>12 | | | a
1> | | +---
1> | +---
1>16 | b
1> +---
1>
1>son::$vftable@uncle@:
1> | &son_meta
1> | 0
1> 0 | &uncle::run
1>
1>son::$vftable@daddy@:
1> | -8
1> 0 | &daddy::run
uncle类:
1>class uncle size(8):
1> +---
1> 0 | +--- (base class grandfather)
1> 0 | | {vfptr}
1> 4 | | a
1> | +---
1> +---
1>
1>uncle::$vftable@:
1> | &uncle_meta
1> | 0
1> 0 | &uncle::run
1>
1>uncle::run this adjustor: 0
可以看到,son类的虚表有两个run函数,上方内存分布中分别继承了uncle和daddy两份成员函数和成员变量,包括int a和 eat(),因此当我们要调用时就不知道要找哪个,即下面这个语句编译不过:
son test;
test.eat();
test.run();
虚继承情况:
class grandfather {
public:
virtual void run() {
cout << "grandfather run" << endl;
}
void eat() {
cout << "grandfather eat" << endl;
}
int a;
};
class uncle : virtual public grandfather {
public:
virtual void run() {
cout << "uncle run" << endl;
}
void eat() {
cout << "uncle eat" << endl;
}
};
class daddy :virtual public grandfather {
public:
virtual void run() {
cout << "daddy run" << endl;
}
void eat() {
cout << "daddy eat" << endl;
}
};
class son :public uncle, public daddy {
public:
virtual void run() {
cout << "son run" << endl;
}
int b;
};
daddy类:
1>class daddy size(12):
1> +---
1> 0 | {vbptr}
1> +---
1> +--- (virtual base grandfather)
1> 4 | {vfptr}
1> 8 | a
1> +---
1>
1>daddy::$vbtable@:
1> 0 | 0
1> 1 | 4 (daddyd(daddy+0)grandfather)
1>
1>daddy::$vftable@:
1> | -4
1> 0 | &daddy::run
1>
1>daddy::run this adjustor: 4
1>vbi: class offset o.vbptr o.vbte fVtorDisp
1> grandfather 4 0 4 0
uncle类:
1>class uncle size(12):
1> +---
1> 0 | {vbptr}
1> +---
1> +--- (virtual base grandfather)
1> 4 | {vfptr}
1> 8 | a
1> +---
1>
1>uncle::$vbtable@:
1> 0 | 0
1> 1 | 4 (uncled(uncle+0)grandfather)
1>
1>uncle::$vftable@:
1> | -4
1> 0 | &uncle::run
1>
1>uncle::run this adjustor: 4
1>vbi: class offset o.vbptr o.vbte fVtorDisp
1> grandfather 4 0 4 0
son类:
1>class son size(20):
1> +---
1> 0 | +--- (base class uncle)
1> 0 | | {vbptr}
1> | +---
1> 4 | +--- (base class daddy)
1> 4 | | {vbptr}
1> | +---
1> 8 | b
1> +---
1> +--- (virtual base grandfather)
1>12 | {vfptr}
1>16 | a
1> +---
1>
1>son::$vbtable@uncle@: //继承来的虚基类指针
1> 0 | 0
1> 1 | 12 (sond(uncle+0)grandfather)
1>
1>son::$vbtable@daddy@: //继承来的虚基类指针
1> 0 | 0
1> 1 | 8 (sond(daddy+0)grandfather)
1>
1>son::$vftable@:
1> | -12
1> 0 | &son::run
1>
1>son::run this adjustor: 12
1>vbi: class offset o.vbptr o.vbte fVtorDisp //虚基类表
1> grandfather 12 0 4 0
虚继承用于解决多继承条件下的菱形继承问题(浪费存储空间、存在二义性)。
底层实现原理与编译器相关,一般通过虚基类指针和虚基类表实现,每个虚继承的子类都有一个虚基类指针(占用一个指针的存储空间,4字节)和虚基类表(不占用类对象的存储空间)(需要强调的是,虚基类依旧会在子类里面存在拷贝,只是仅仅最多存在一份而已,并不是不在子类里面了);当虚继承的子类被当做父类继承时,虚基类指针也会被继承。
实际上,vbptr 指的是虚基类表指针(virtual base table pointer),该指针指向了一个虚基类表(virtual table),虚表中记录了虚基类与本类的偏移地址;通过偏移地址,这样就找到了虚基类成员,而虚继承也不用像普通多继承那样维持着公共基类(虚基类)的两份同样的拷贝,节省了存储空间。
可以看到,每个类的最后都会有一个vbi虚基类表,表中记录了虚基类与本类的偏移地址,通过他就能找到虚基类的成员和函数。在这个地方,虚基类表中记录的是grandfather类相对于son类的偏移,即son类要重写run的话,是直接取自grandfather的(不知道对不对,望指正)
这里放一个网上的完整例子,讲的很不错
虚继承之前:
1> Animal::$vftable@: //爷类
1> | &Animal_meta
1> | 0
1> 0 | &Animal::breathe
1>
1> class LandAnimal size(12): //父类
1> +---
1> 0 | +--- (base class Animal)
1> 0 | | {vfptr}
1> 4 | | name
1> | +---
1> 8 | numLegs
1> +---
1>
1> LandAnimal::$vftable@:
1> | &LandAnimal_meta
1> | 0
1> 0 | &Animal::breathe
1> 1 | &LandAnimal::run
1>
1> class Mammal size(12): //父类
1> +---
1> 0 | +--- (base class Animal)
1> 0 | | {vfptr}
1> 4 | | name
1> | +---
1> 8 | numBreasts
1> +---
1>
1> Mammal::$vftable@:
1> | &Mammal_meta
1> | 0
1> 0 | &Animal::breathe
1> 1 | &Mammal::milk
1>
1> class Human size(28): //孙类
1> +---
1> 0 | +--- (base class Mammal) //直接拷贝父类和爷类的内存结构
1> 0 | | +--- (base class Animal)
1> 0 | | | {vfptr}
1> 4 | | | name
1> | | +---
1> 8 | | numBreasts
1> | +---
1> 12 | +--- (base class LandAnimal)
1> 12 | | +--- (base class Animal)
1> 12 | | | {vfptr}
1> 16 | | | name
1> | | +---
1> 20 | | numLegs
1> | +---
1> 24 | race
1> +---
1>
1> Human::$vftable@Mammal@:
1> | &Human_meta
1> | 0
1> 0 | &Animal::breathe
1> 1 | &Human::milk
1>
1> Human::$vftable@LandAnimal@:
1> | -12
1> 0 | &Animal::breathe
1> 1 | &Human::run
虚继承之后:
1> class Animal size(8): //爷类
1> +---
1> 0 | {vfptr}
1> 4 | name
1> +---
1>
1> Animal::$vftable@:
1> | &Animal_meta
1> | 0
1> 0 | &Animal::breathe
1>
1> class LandAnimal size(20): //父类
1> +---
1> 0 | {vfptr}
1> 4 | {vbptr}
1> 8 | numLegs
1> +---
1> +--- (virtual base Animal)
1> 12 | {vfptr}
1> 16 | name
1> +---
1>
1> LandAnimal::$vftable@LandAnimal@:
1> | &LandAnimal_meta
1> | 0
1> 0 | &LandAnimal::run
1>
1> LandAnimal::$vbtable@:
1> 0 | -4
1> 1 | 8 (LandAnimald(LandAnimal+4)Animal)
1>
1> LandAnimal::$vftable@Animal@:
1> | -12
1> 0 | &Animal::breathe
1>
1> class Mammal size(20): //父类
1> +---
1> 0 | {vfptr}
1> 4 | {vbptr}
1> 8 | numBreasts
1> +---
1> +--- (virtual base Animal)
1> 12 | {vfptr}
1> 16 | name
1> +---
1>
1> Mammal::$vftable@Mammal@:
1> | &Mammal_meta
1> | 0
1> 0 | &Mammal::milk
1>
1> Mammal::$vbtable@:
1> 0 | -4
1> 1 | 8 (Mammald(Mammal+4)Animal)
1>
1> Mammal::$vftable@Animal@:
1> | -12
1> 0 | &Animal::breathe
1>
1> class Human size(36): //孙类
1> +---
1> 0 | +--- (base class Mammal)
1> 0 | | {vfptr}
1> 4 | | {vbptr}
1> 8 | | numBreasts
1> | +---
1> 12 | +--- (base class LandAnimal)
1> 12 | | {vfptr}
1> 16 | | {vbptr}
1> 20 | | numLegs
1> | +---
1> 24 | race
1> +---
1> +--- (virtual base Animal)
1> 28 | {vfptr}
1> 32 | name
1> +---
1>
1> Human::$vftable@Mammal@:
1> | &Human_meta
1> | 0
1> 0 | &Human::milk
1>
1> Human::$vftable@LandAnimal@:
1> | -12
1> 0 | &Human::run
1>
1> Human::$vbtable@Mammal@:
1> 0 | -4
1> 1 | 24 (Humand(Mammal+4)Animal)
1>
1> Human::$vbtable@LandAnimal@:
1> 0 | -4
1> 1 | 12 (Humand(LandAnimal+4)Animal)
1>
1> Human::$vftable@Animal@: //虚基类指针直接指向爷类的内存,用上偏移量
1> | -28
1> 0 | &Human::breathe
总结:
我们可以观察到,一个子类虚继承自另一个基类,它不再像普通继承那样直接拥有一份基类的内存结构,而是加了一个虚表指针vbptr指向虚基类,这个虚基类在msvc中被放在的类的内存空间的最后。这样,当出现类似这里的菱形继承时,基类Animal在子类Human中出现一次,子类Human所包含的Mammal类和LandAnimal类各有一个虚基类指向虚基类。从而避免了菱形继承时的冲突。
总之,C++多态的核心,就是用一个更通用的基类指针指向不同的子类实例,为了能调用正确的方法,我们需要用到虚函数和虚继承。在内存中,通过虚函数表来实现子类方法的正确调用,通过虚基类指针,仅保留一份基类的内存结构,避免冲突。
所谓虚,就是把“直接”的东西变“间接”。成员函数原先是由静态的成员函数指针来定义的,而虚函数则是由一个虚函数表来指向真正的函数指针,从而达到在运行时,间接地确定想要的函数实现。继承原先是直接将基类的内存空间拷贝一份来实现的,而虚继承则用一个虚基类指针来指向虚基类,避免基类的重复。
就是父类和孙类都会有一个虚基类指针来指向虚基类,而不是像普通继承那样将基类的内存保留一份在自己的空间中。这样就能保证孙类中不会出现指向模糊的问题。
重载、继承、隐藏
重载(overload)和覆盖(override)以及隐藏(重定义):
重载是要求函数在使用重载时只能通过不同的参数列表(不能根据返回值类型)。例如,不同的参数类型,不同的参数个数,不同的参数顺序。overload编译时的多态,是在同一个类中产生的。
覆盖是运行时的多态,是通过父类子类间的虚函数重写实现的
隐藏:(1)如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。
(2)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual 关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)。
至于为啥不能根据返回值来重载,原因见下:
若有两个函数
int fun();
void fun();
若这样调用,是可以的:int i=fun();
但是这样调用,就不知道要用哪个了: fun();
一点实验
1.若是发生了隐藏,再用父类指针指向子类对象,此时调用的是父类还是子类中的方法?
答案:父类的
#include<iostream>
using namespace std;
class A {
public:
A() {};
~A() {};
virtual void withdefault(int a, int b = 0) {
cout << "this is A's withdefault" << endl;
}
void func() {
cout << "this is A's func" << endl;
}
};
class B :public A {
public:
B() {};
~B() {};
void func() {
cout << "this is B's func" << endl;
}
void withdefault(int a , int b) {
cout << "this is B's withdefault" << endl;
}
};
int main() {
A * p = new B;
p->func();
p->withdefault(1);
system("pause");
return 0;
}
p指针调用p->func();会发生:
B的内存分布:显然,虚函数表中没有func函数的位置
1>class B size(4):
1> +---
1> 0 | +--- (base class A)
1> 0 | | {vfptr}
1> | +---
1> +---
1>
1>B::$vftable@:
1> | &B_meta
1> | 0
1> 0 | &B::withdefault
1>
1>B::withdefault this adjustor: 0
但是为啥A类的指针指向B类的对象会调用A的函数?
很多同学知道了有隐藏这么回事,但是不清楚隐藏触发后会发生什么。 隐藏机制触发之后,指针的调用取决于指针的类型。如果定义的是派生类指针,则该基类成员不可见(隐藏),但是若为基类指针,该基类成员仍然是可见的啊!
但是如果定义个派生类指针pb,如下:
B *pb= new B;
pb->fun();
这时只会调用派生类的fun(),虽然B继承自A,但是基类的fun()会被隐藏。
但是一旦foo()里有参数的时候,你就会大吃一惊!
假设A中为void foo(float a),B中为void foo(int a):
做如下调用:
B *pb=&b;
pb->foo(3.14);
到底会调用谁?你可能会想: 首先foo()成 员不是虚函数,但是B继承A,B中有两个foo(),调用foo(3.14)时根据参数类型应该匹配基类的void foo(float)成员。
然而并不是!
因为触发了隐藏机制,基类的void foo(float)会被隐藏,所以即使你调用foo(3.14)仍然只会调用派生类的void foo(int)成员。
你的惊讶正好解释了隐藏机制存在的意义。
总结:
1.判断要点:如果不是重载也不是覆盖,派生类和基类中一旦出现同名函数,一定触发隐藏机制(这是个简便判断技巧,你可以考虑除去重载和覆盖的任何同名函数情况,一定满足隐藏机制触发的两条规则)。
2.隐藏触发的结果:指针对成员的函数调用取决于指针类型。
若本身是基类指针(不管指向基类还是派生类)则仍然调用基类成员(不会牵扯到派生类,此处是隐藏,和多态没关系,按第1点已说明隐藏的触发可以首先排除覆盖,也就是多态问题);
若本身是派生类指针,这时你就会看到隐藏的威力!此时不是简单地继承基类的成员,然后根据参数匹配调用,而是隐藏基类成员,只会调用派生类成员。
问题2:父类函数有参数默认值,子类同名函数没写,算不算重载
答案:算。 运行结果看上面。
这个应该涉及到参数默认值的内容。
在 C++ 中,声明一个函数时,可以为函数的参数指定默认值。当调用有默认参数值的函数时,可以不写出参数,这时就相当于以默认值作为参数调用该函数。
默认参数的声明:默认参数在函数声明中提供,当又有声明又有定义时,定义中不允许默认参数。如果函数只有定义,则默认参数才可出现在函数定义中。例如:
void point(int=3,int=4); //声明中给出默认值
void point(intx,inty) //定义中不允许再给出默认值
{
cout <<x<<endl;
cout <<y<<endl;
}
默认参数的顺序规定
如果一个函数中有多个默认参数,则形参分布中,默认参数应从右至左逐渐定义。当调用函数时,只能向左匹配参数。例如:
void func(int a=1,int b,int c=3, int d=4); //error
void func(int a, int b=2,int c=3,int d=4); //ok
对于第2个函数声明,其调用的方法规定为:
func(10,15,20,30); //ok:调用时给出所有实参
func(); //error:参数a没有默认值
func(i2,12); //ok:参数c和d默认
func(2,15,20); //error:只能从右到左顺序匹配默认