今天通过对实验二继承,重载,覆盖的学习,让我更深一步理解了这些概念的区别。
首先来明确一个概念,函数名即地址,也就是说函数名就是个指针。
编译阶段,编译器为每个函数的代码分配一个地址空间并编译函数代码到这个空间中,函数名就指向这个地址空间。
也即每个函数名都有自己唯一的代码空间。
同理,类的成员函数也是如此。
但是,有一点大家一定要记住,C++编译器编译CPP文件时,会根据"C++编译器的函数名修饰规则" 对函数名进行修饰。
(修饰规则大家自己去搜吧,我就不叙述了),前面讲到函数名称的作用是指向函数真实代码的指针。
知道了以上规则,那么我们对函数覆盖便不难理解了。
首先来看看百度百科中函数覆盖的中文描述是:
函数覆盖发生在父类与子类之间,其函数名、参数类型、返回值类型必须同父类中的相对应被覆盖的函数严格一致,
覆盖函数和被覆盖函数只有函数体不同,当派生类对象调用子类中该同名函数时会自动调用子类中的覆盖版本,
而不是父类中的被覆盖函数版本,这种机制就叫做函数覆盖。
我们来写一段函数覆盖的代码
class father
{
public:
void fun()
{cout<<"father‘s fun"<<endl;}
};
class son:public father
{
public:
void fun()
{cout<<"son‘s fun"<<endl;}
};
void main()
{
father Father,*pFather;
son Son,*pSon;
int i = sizeof(Father); //此行代码过后,i=1,此行代码无意义只是让大家知道普通成员函数不占用类的实例空间。
// Father.fun(); //此行注释与下行注释是正常的调用函数覆盖相信大家都能理解,所以不在解释
// Sun.fun();
pSon = (son*)&Father; //子类指针指向父类实例是危险的,此例中并没涉及到任何越界,并且为了展示区别
//所以才这样使用,但大家要明白,这样做是危险的。
pFather = (father*)&Son;
pSon->fun(); //函数调用执行了father‘s fun
pFather->fun(); //函数调用执行了son‘s fun
}
此时有些人可能就不能理解为什么会出现这种调用结果了,那么大家是否还记得我上面曾提到的"C++编译器的函数名修饰规则"?
我们来根据"C++编译器的函数名修饰规则"再来想想原因。
根据规则,编译器把父类father中的fun函数名编译为"[email protected]@@QAEZ",子类son中的fun函数名编译为"[email protected]@@QAEZ"。
当pSon->fun();调用时,编译器会把pSon所存地址值的类型转化成当前指针类型,而pSon的当前类型为son(这句话不多余,
因为类型是可以随意转换的),
所以表达式"pSon->fun()"全部展开以后得到的函数名称即"[email protected]@@QAEZ"
同理表达式"pFather->fun()"全部展开以后得到的函数名称即"[email protected]@@QAEZ",
既然得出了函数名,那么也就可以根据函数名称,跳转到真实的函数代码实体位置了。
根据以上分析,我觉得"函数覆盖(英文名不知到叫什么只能用中文了)"这个词汇真的容易把人带入歧途,
我的语文又不好,所以还希望哪位语文好的兄弟,来重新翻译下"函数覆盖"这个词汇。
呃,真费劲啊,函数覆盖算是讲完,不知道大家有每有看懂,如果还看不懂的话,我是真没招了。
下面在来讲个虚函数吧。
有了前面的基础,大家应该对函数有了充分的了解。
那么问大家一个问题,为什么一个类实力化后普通成员函数不影响实例的大小?
呵呵,如果一个新手能回答出来,那我这篇东西就不算白写。
正确答案嘛,是因为不需要它来影响实例大小,因为编译器会根据"C++编译器的函数名修饰规则"与"表达式的地址类型",
自动的把成员函数展开成完整的函数名,也就找到了函数的真实地址,所以普通成员函数是不影响实力大小的。
我再来问大家一个问题,你们认为虚函数需不需要影响类的实例大小?
哈哈,这次的答案是需要。
这次我们来写一段虚函数的代码瞧瞧 因为只有在实例内部添加一个指针,才能够完成例如,
class father
{
public:
virtual void fun()
{cout<<"father‘s fun"<<endl;}
};
class son:public father
{
public:
void fun()
{cout<<"son‘s fun"<<endl;}
};
void main()
{
father Father,*pFather;
son Son,*pSon;
int i = sizeof(Father); //此行代码过后,i=4,此行代码无意义只是让大家知道虚函数占用类的实例空间。
Father.fun(); //函数执行结果 "father‘s fun"
Son.fun(); //函数执行结果 "son‘s fun" 这句简单点说,就是很多人说的动态绑定,我们下面会具体分析。
pSon = (son*)&Father; //再次强调,子类指针指向父类实例是危险的,一旦越界操作,就会引发异常。
pFather = (father*)&Son;
pSon->fun(); //函数执行结果 "father‘s fun"
pFather->fun(); //函数执行结果 "son‘s fun" 这两句也是动态绑定,相信还是有不少人不理解,下面具体分析。
}
恩,大家需要调式一下上面的程序,对Father添加监视,你会发现Father实例中莫名其妙的多了一个vftable类型指针对象vfptr。
是了,虚函数的实现靠的就是这个东西了。
IED帮我们在我们的实例外部实例化了一个vftable对象(我知道这句话很绕,但是我不知道该怎么更好的解释了),
同时为我们的实例Father添加了一个指向vftable对象的指针vfptr。
我们继续把监视中的指针vfptr展开,可以看到一个叫[0]的函数指针(别问我这名为啥长成这样,我也没搞清楚),
哈哈,找到了,这里存储了一个地址,这个地址就是一个函数真实地址(如果用的VS2010编译器,你会直观的看到这个地址
所对应的是father::fun这个函数)。
然后,我再来明确一个虚函数规则,就是当你的实例调用虚函数时,最终调用的就是这个vftable类型的成员[0]所存储地址。
那么好了,我们知道子类是完全继承父类的,所以那个vftable类型指针对象vfptr也同时被继承了下来。
IDE同样为我们实例化一个vftable对象,让vfptr来指向这个vftable对象。
而如果我们的子类重写了虚函数,那么IDE在实例化vftable对象时,就会把[0]这个指针重写为新的子类中那个虚函数地址。
如果我们的子类没有重写这个虚函数,那么IDE就会找到距离这个子类关系最近的一个实现了虚函数的父类,
把这个父类中的虚函数地址,写入到子类的[0]中。
这样子类在调用虚函数的时候,就可以实现动态绑定了。
另外父类指针指向子类实例时,因为有了vfptr指针占位,所以当父类指针调用虚函数时,寻址到的vfptr是子类实例的。
而子类的vfptr指向子类自己的vftable对象,所以父类最终调用的会是子类对象的中[0],所以[0]中存的是哪个函数地址。
父类指针最终调用的就会是哪个函数了。