问题是:
请问下面的代码中,test1和test2函数各输出什么?
class A
{
public:
int _a;
public:
A()
{
_a = 1;
}
void set(int a)
{
_a = a;
}
void get()
{
return _a;
}
};
class B : public A
{
public:
int _a;
public:
B()
{
_a = 2;
}
};
void test1()
{
B b;
printf("%d",b.get());
}
void test2()
{
B b;
b.set(3);
printf("%d",((A*)(&b))->_a);
}
int main()
{
test1();
test2();
return 0;
}
这里,先给出答案,test1输出为1;test2输出3;
可能你答对了,但你可能不知道深层次的原因,如果你有把握说你知道里里外外每个细节那就请绕行吧 :)
很好,你能看到这里说明我还有观众,那我也会本着负责任的态度把这个问题解释清楚。让你不再停留于记忆,“子类成员覆盖父类成员会发生什么事,子类继承父类函数而没有改写的情况下,用子类对象调用这个函数会发生什么事”,等等。
我们先要弄清楚本例中的两个对象的布局,这两个对象是子类对像和父类对象,父类对象会在子类对象构造之前暗中构造出来,这点应该都清楚。继承的语义告诉我们,一个子类对象会蕴含一个父类对象,它们在内存中的结构如下:
这个图蕴含着两个有意义的知识:
1.成员函数不属于对象(在上面的内存布局图中并没有出现成员函数的身影),实际上,成员函数是共享的,它具有全局的概念
2.A对象与B对象开始地址一样,也就是说藉由一个B对象的地址转换为A对象地址是一个很自然的事情,但我们还要意识到,从A对象地址转换指向B对象地址是不合法的,这里因为编译器无法确定A对象是否蕴含于一个存在的B对象。
如果以B的结构去解释以A开始的内存区,可能会存在问题,因为无法确定那个“?”所在的内存区是否真的应该被解读为B的_a成员!因为可能这个臆测的B对象都不存在!
(如果对上述内容不理解,可以参考我之前写的一篇文章,里面讲述了更详细的对象内存布局,http://blog.csdn.net/lizhihaoweiwei/article/details/16115713)。
说完了数据的问题,再说一说成员函数的问题,我们知道,成员函数不在对象的内存里面,那么,它存放在哪里呢?你可以想像它存放在一个全局访问区,可供访问。那么,你可能就有疑问了,成员函数是如何访问成员变量的呢?这个问题比较容易解释,你应该还记得,所有的成员函数(不包括static成员函数)里面有一个隐含的this指针,它指代的就是操作当前函数的对象,现在惟一的问题就是,每次调用成员函数时,如何明确这个this指针的指代物呢?答案就是,C++对于每个类的成员函数的调用方式是this call,也就是隐含地将对象的指针作为成员函数的一个参数传入并赋给this,举个例子,void A::set(int a) 这个函数等价于:void set(A* pA,int a),于是,对于成员函数里面访问到的成员变量,其实都是采用this -> 的方式去访问。如对于 _a的访问就是:this -> _a。
下面,我们还需要了解一个编译器层面的东西。set 函数定义的内容只有一句话:
_a =a;根据上面的分析,这句话会被改写为下面的代码:this -> _a = a;那么,进一步地,编译器会怎么将这句话转换为汇编语言?等价的说法是,你要把这句话关于寻址的细节勾勒出来。让我们再具体一点,_a 应该怎样寻址 ?我们肯定知道它必须要通过 this 寻址,而且现在我们已经知道了 this 的值,它就是指向的当前的操作这个函数的对象,那么我们要取到这个 _a 的地址,有什么办法呢?答案就是,_a属于一个对象,那么,它肯定相对于这个对象有一个固定的偏移,这个是在编译期就能决定的事实!在本例中,_a相对于A对象的偏移是多少呢?这种说法与指向类的成员变量的指针的值是一个相关的问题。(注意这里我并没有说这两个问题是同一个问题,在文章最后面我会阐述这个问题)我们从上面的内存布局图可以知道,这个偏移是0,也就是说,_a的地址现在可以这么计算了:this + 0。所以,set函数被编译之后,会形成如下的伪代码:
void set(A *pA,int a)
{
this = pA;
int *p_a = (int*)(this + 0);
*p_a = a;
}
好啦,理论知识到这里就结束了,让我们转过来分析上面那道题。
test1()中,我们分两步来看b.get(),
首先,A.get会被编译为:
int get(A *pA)
{
this = pA;
int *p_a = (int *)(this + 0);
return *p_a;
}
再者,b.get()的调用会被转换为:get((A*)b),代入上面的编译后伪码:即相当于执行 return *((int *)(A*)b),所以它会返回A对象里面的 _a,即结果是打印1。
有了上面的基础,我们就可以知道test2()中的 set 其实是设置了A对象实体里的_a为3,而
(A*)(&b))->_a
即是指,按A的结构方式去解析b对象,并取出里面的 _a 成员,
即打印B对象里面蕴含的A对象的_a,所以,会打印3。
那么,还有一个问题没有解决,上文中提到,一个成员变量相对于对象的偏移并不与“指向类的成员变量的指针”等价。我们都知道,指向类的成员变量的指针总是可以被理解为该成员变量相对于对象的偏移。
看下面的代码:
A a;
int diff1 = &a._a - &a;
int diff2 = &A::_a;
也就是说上面的问题实质上是在问:diff1 和 diff2是否相等。
很遗憾,这个结论是不确定的,即它们可能相等,也可能不相等。
不相等的原因,引用自《Inside The C++ Object Model》里面的一句话,“如何区分一个没有指向任何 data member 的指针和一个指向第一个data member的指针”。
这里解释一下:在不考虑含有 virtual 函数或者虚继承的情况下,一个对象的第一个成员变量相对于这个对象的地址偏移为0,而一个空指针的指也为0,这两者如何区分呢?
即,有如下代码:
int A::*pa1 = 0;
int A::*pa2 = &A::_a;
那么,pa1到底应该不应该等于pa2呢?如果只从值上比较,这两个值都为0,所以是相等的,但这两者的意义相去甚远,一个没有意义,一个确实表示A中首个成员变量的偏移,这两者当然不应该相等。有些编译器的处理为,&A::_a = 实际偏移加1.
而相等的理由是,某些编译器实现了 &a::a = 实际偏移而且还解决了“区分一个没有指向任何 data member 的指针和一个指向第一个data member的指针”的问题。(据我所测,MS VC和g++都是这样实现的)
综上,这个问题不一定是等价的!如果在你的代码里面用到了&A::_a 这样的指向类成员变量指针的代码将有可能面临移值性问题,最好的办法就是不去使用它。
看下面的代码:
A a;
int diff1 = &a._a - &a;
int diff2 = &A::_a;
也就是说上面的问题实质上是在问:diff1 和 diff2是否相等。
很遗憾,这个结论是不确定的,即它们可能相等,也可能不相等。
不相等的原因,引用自《Inside The C++ Object Model》里面的一句话,“如何区分一个没有指向任何 data member 的指针和一个指向第一个data member的指针”。
这里解释一下:在不考虑含有 virtual 函数或者虚继承的情况下,一个对象的第一个成员变量相对于这个对象的地址偏移为0,而一个空指针的指也为0,这两者如何区分呢?
即,有如下代码:
int A::*pa1 = 0;
int A::*pa2 = &A::_a;
那么,pa1到底应该不应该等于pa2呢?如果只从值上比较,这两个值都为0,所以是相等的,但这两者的意义相去甚远,一个没有意义,一个确实表示A中首个成员变量的偏移,这两者当然不应该相等。有些编译器的处理为,&A::_a = 实际偏移加1.
而相等的理由是,某些编译器实现了 &a::a = 实际偏移而且还解决了“区分一个没有指向任何 data member 的指针和一个指向第一个data member的指针”的问题。(据我所测,MS VC和g++都是这样实现的)
综上,这个问题不一定是等价的!如果在你的代码里面用到了&A::_a 这样的指向类成员变量指针的代码将有可能面临移值性问题,最好的办法就是不去使用它。