多态,继承,引用指针对象一顿扒.
先把实验代码写到前面
#include <iostream>
using namespace std;
class A
{
private:
int a;
public:
A(int x = 0):a(x)
{
cout << "A constructor" << endl;
}
A(A & ya)
{
a = ya.a;
cout << "A copy constractor" << endl;
}
~A()
{
cout << "A 析构" << endl;
}
void setval(int x)
{
a = x;
}
virtual void showinf()
{
cout << a <<",A 调用" << endl;
}
void print()
{
cout << "so complicated!" << endl;
}
};
class B:public A
{
private:
int b;
public:
B(int x = 0,int y = 0):A(x),b(y)
{
cout << "B constractor" << endl;
}
B(B & yb):A(yb) //必须用初始化列表来完成对继承自父类部分的数据,因为有私有成员
{
this->b = yb.b;
cout << "B copy constractor" << endl;
}
~B()
{
cout << "B 析构" << endl;
}
virtual void showinf()
{
cout << b << ",B 调用" << endl;
}
};
void f(A a)
{
a.showinf();
}
void g(A * pa)
{
pa->showinf();
}
void h(A & a)
{
a.showinf();
}
void main()
{
A a(1); //A constructor
A* pa = &a;
a.showinf();//1,A 调用 //对象方式:直接去代码区找函数A::showinf入口
f(a); //1,A 调用 //对象方式:通过拷贝构造函数打造一个新临时对象传入作为实参,直接去代码区找函数A::showinf入口,函数调用完成调用析构释放栈中的临时对象
g(pa); //1,A 调用 //指针方式:通过a对象内部的vfptr[0]找函数A::showinf()入口地址
h(a); //1,A 调用 //引用方式:类似上一行指针方式(引用底层其实也是传一个this的指针)
B b(2,5); //A constructor,B constractor //里面文章不少
pa = &b;
b.showinf(); //5,B 调用 原理:同名覆盖,和虚函数没半毛钱关系,直接代码区找函数showinf入口,此时B::showinf覆盖了或者说叫隐藏了A::showinf,但A::showinf()是肯定存在的,下行代码我们将进行验证
b.A::showinf(); //2,A 调用 原理:调用被子类同名覆盖的父类中的函数(虽为虚函数)
f(b); //2,A 调用 原理:真切割通过A类的拷贝构造函数打造一个临时对象,并将B类型的对象隐式转换为A类型,问题:此时构造的临时对象其vfptr[0]为什么指向了A::showinf(),b的vfptr[0]还是指向的B::showinf()的呢? 此调用不是通过vfptr[0]来调用的而是直接找code区入口是么?
g(pa); //5,B 调用 原理:多态(void) pa->(*vf[0](pa))? 类型又怎办?
h(b); //5,B 调用 原理:多态
pa->print();
}
开扒心得:
好,我来就这段代码来讲讲其中的八卦,撇开两旁美丽的姑娘,请跟我来,我带你去梦游
在分析中我将按照代码的执行顺序,逐步的扒每步其中的玄机
1. A a(1); //A constructor
在实例化a对象是传入1整型实参,调用A::A(int x = 0)带默认值的构造函数打造a对象.如图可见此时this中的地址为0x0013ff50,问题1:此地址为什么不是a的地址?
然后输出”A constractor”信息,没多少可说的
按F10单步继续,可见&a为0x0013ff50,此时a的地址才最终确定,和构造函数中的this地址是一致的,这是对问题1的释疑,但这是为什么呢?由图可见a对象中有一个__vfptr值为0x00417810,它指向一个函数指针数组共一个元素,第一个元素即[0]的值为0x0041114f,也即虚函数A::showing(void)的入口地址.
2.按F10单步走把 A* pa = &a;执行完可以发现
pa的值为0x0013ff50和第1步中的a对象的地址相符.
3.按F10单步执行a.showinf();
此时用a对象直接调用showinf(),程序将直接去代码去找A::showinf()的入口地址.显示"1,A 调用"
4.按F11进入f(a);的内部,由f函数的原型void f(A a)可知其实参为一个对象,此时首先要调用A的拷贝构造函数打造一个临时对象,我们不妨称它为a`,由图可见this此时的值为0x0013fe38,这个地址即为临时对象的地址,它是一个无名对象.
然后输出”A copy constractor”
5.按F11进入void f(A a)函数,此时实参a即地址为0x0013fe38的对象,然后执行a.showinf(),此时调用的找寻函数入口的方式仍然是直接到code代码区进入.输出”1,A 调用”,然后出void f(A a)的函数时,临时对象(以0x0013fe38为首的A类型的对象)要析构,调用A::~A()函数发生析构,输出”A 析构”
6.执行g(pa);由函数原型void g(A * pa)可知实参是一个A*指针,看图pa的值为0x0013ff50和第一步的地址符合,没问题.然后执行pa->showinf();此时showinf()调用方法是通过pa找到对象地址,然后通过__vfptr[0]找到A::showinf()的入口地址0x0041114f,从面完成调用A::showinf(),输出”1,A 调用”
6.下面说下h(a);由函数原型void h(A & a)可知实参为A类型的一个引用
看图
注意看图中的黄箭头,这是程序目前下步要执行的代码,监视中可见此是&a,即实参的地址为0x0013ff50,和第一步创建的对象a的地址相符.
至于调用showninf和指针传参类似,也是调用__vfptr[0]来找到函数入口的地址的.这里说下引用传参本质上讲是传递了一个指向对象的指针.更深了先不说,也说不明白.
我们发现实参用引用,并未调用拷贝构造函数打造临时对象就完成了函数调用,从面提高了程序的效率,避免了不必要的内存开销.
输出”1,A 调用”.至此基类对象函数调用说完了,下面着手多态.
7.准备执行B b(2,5);按F11进入B(int x = 0,int y = 0):A(x),b(y),可见程序走到B类的初始化列表,在初始化列表中写了A(x)即基类名(蕨类构造实参)的方式进一步初始化基类的数据成员.
注意此时编译时B::showinf(void)的地址将覆盖__vfptr[0]原来的A::showinf(void).因为基类函数是虚函数且公有继承且子类重写了该函数(重写需要返回值及参数都要与基类中的函数严格一致,这里再延伸下如果基类函数声明为void A::showinf(void) const子类原型为void B::showinf(void)则不为重写,是同名覆盖?应该是.我记得侯老师说的是.).这是多态的得以实现的最核心最重要环节.
依次输出”A constractor”,”B constractor”
这里值得说的是在初始化b对象中继承自A类的东东时,this和b的地址是一样的,只不过它指向的对象长度较短,即此时this是A*类型的,面b是B类型的.这也验证了全面继承父类的东西且排在子对象的前端.
8. pa = &b;让基类类型的指针的值保存为子类对象的地址.这是实现多态的基础,b的地址为0x0013ff30.
9.执行b.showinf();将调用B::showinf(),在调用该函数进操作系统将直接去code代码区找寻B::showinf(),我们不妨看下此时b对象内部的结构,看图
由图可知.继承自父类A的部分排在对象的首部.且继承自A的部分仍然有一个__vfptr只不过此时__vfptr[0]函数指针型元素保存的是B::showinf(void)的入口的地址0x411114a,不再是A::showinf(void)的地址0x0041114f.
执行b.showinf();时操作系统将直接去代码区直接找函数入口地址0x411114a,但不是通过__vfptr[0].此时起作用的是同名覆盖,即子类void showinf()覆盖了父类的virtual void showinf(),虽然父类带一个virtual仍然是同名覆盖,即b.showinf()时看到的只能是子类B::showinf(),父类的A::showinf()被隐藏了.但父类的A::showinf()是一定存在于对象a中的(可能此种说法不严谨,就是那个意思吧),下面我们将验证这点.
b.showinf();输出”5,B 调用”
10. b.A::showinf();输出”2,A 调用”,调用被子类同名覆盖的父类中的函数(虽为虚函数)
11.准备执行f(b);再看函数原型void f(A a)可见实参期望一个A类型的对象,而现在给出的是一个b,即一个B类型的对象.而B类型公有继承自A,所以可以调用A类的拷贝构造函数打造一个临时对象形成实参.
按F11进入A::A(A& ya)
看图
注意看this(指向临时对象的指针)的__vfptr[0]的值为0x0041114f A::showinf(void)却是A的showninf()为什么呢?我想这是因为你的A::A(A& ya)的写法是a = ya.a;的缘故.如果memcpy(this,&ya,sizeof(A))此时就能使__vfptr[0]的值就能保存成0x0041114a B::showinf(void).
进入f函数
void f(A a)
{
a.showinf();
}
调用a.showinf()注意此时a是内存栈中的临时对象,对它的描述是按A类型切割过的B类型对象,且经过A的拷贝构造函数处理过的对象.那么仍然是操作系统直接直接对代码区A::showinf(void)入口地址,和__vfptr[0]没半毛钱关系,因为此时是对象直接调用(深入的原因?不知道).
f(b);依次输出”A copy constractor”,”2,A 调用”,”A 析构”.
这再多说两句:如果A类的拷贝构造函数写成了memcpy(this,&ya,sizeof(A)),且f函数写成了
void f(A a)
{
a.showinf();
A * pa = &a;
pa->showinf(); //注意这里
}
这样一来, pa->showinf();将调用B::showinf(void),而此时pa->b数据成员已经被切割掉,B::showinf()又要去访问pa->b成员,那么将会访问到一个没有任何价值的随机数.
12. 执行g(pa);将b的地址0x0013ff30传入g(pa);作为实参
这里之所以有两个__vfptr是因为B::showinf(void)也是一个虚函数,不小心写上的,和本讨论主题不甚相关,请忽略.
然后执行pa->showinf();此时操作系统将通过__vfptr[0]得到0x0041114a即B::showinf(void)的入口地址,从而实现多态.(再深入的我先不说,比如函数指针调用时隐含第一个参数this指针,还说的不太能自圆其说,so..)
g(pa);输出”5,B 调用”;
13. h(b); 也输出”5,B 调用”;实现原理:多态.
14.最后说下pa->print(),如果print()是A类的成员函数则是可以的,但如果是B类的成员函数编译将通不过.再若将其设计为virtual B::print();也不行,因为pa->print();将会自继承自父类的__vfptr[]中找该函数,而virtual B::print()地址将保存在B对象自己的__vfptr[]中.
最后输出结果:(print函数输出信息未收集)
qq:94338240
smzh0302@16.com欢迎C,C++方面交流
发布者:admin,转转请注明出处:http://www.yc00.com/news/1692818292a646393.html
评论列表(0条)