周日下午,小伙伴们开开心心地到会议室来讨论。
开始,小孟给我们讲解关于C++中的多态。
我们知道,在c++中,基类与派生类是”is-a”的关系。在子类中,包含了所有基类的成员,以及这个子类所特有的成员。即子类可以完全作为基类来使用。
而基类通常并不能当做子类来使用。
但是小孟不走寻常路,拿出了这样的代码。
(注:当时我们是在VC6.0上运行的这些代码)
#include <iostream>
using namespace std;
class Base
{
int x;
public:
Base():x(1){};
virtual void B_f()
{}
};
class Derived:public Base
{
public:
void D_f()
{
printf("in D_f \n");
}
};
int main()
{
Base b1;
Derived *p_b1= (Derived *)&b1;
p_b1->D_f();
}
猜测一下这个程序的运行结果。
b1是一个基类的对象。
按道理来说,应该无法访问其子类的成员函数,我们通过一个强制转换,将b1的地址解释成了一个子类的指针,并且赋值给了p_b1
通过p_b1这个子类指针,去调用子类中的函数D_f();
开始我们大家觉得,不应该能够执行到子类的这个函数。
可是运行结果让我们很费解:
将一个基类对象指针强制转换成子类对象指针,竟然可以访问到子类的函数。
经过我们的讨论。
应该是每一个类的的指针,都能够访问到这个类的public成员函数
为了验证这个道理
我们新建立了一个类,里面啥都没有
class R
{};
修改了一下main函数
int main()
{
Base b1,b2,b3;
R r1;
Derived *p_b1= (Derived *)&r1;
p_b1->D_f();
}
果然我们又成功访问到了这个类里的函数D_f();
那么将这个指针指向整型变量的地址,发现也能够访问到函数D_f();
甚至这样玩,通过一个指向int *的变量p,将p解释成Derived型指针,仍然访问到了D_f
结论1:
对象对他的函数一无所知,反过来,是函数知道他具体属于哪一个对象。当用一个被解释成类的指针来访问这个类的成员函数时,通过这个指针,从而找到这个成员函数。如果你把这写成C语言的形式,就会变得明朗起来了。
(这里参考了文章http://blog.csdn.net/rockics/article/details/7018490)
比如这个类和调用
class A
{
int x;
public:
void f(){};
void g(int t){};
};
int main()
{
A d1;
A *p=&d1;
p->D_f();
}
写成c语言的形式,就是
(PS:在C++里面class和struct的唯一区别是默认的访问类型不同(后者是public),而c语言里面没有class关键字)
struct A
{
int x;
};
void f(struct A* this){};
void g(struct A* this,int t){};
int main()
{
A a1;
f(&a1);
}
然后又提出了一个问题:
假如派生类中有一个成员变量,那么能不能访问到?
开始我们都觉得应该不能吧。
修改了一下代码
class Derived:public Base
{
int y;
public:
Derived():y(2){};
void D_f()
{
y=3;
printf("in D_f() \n",y);
}
void B_f(){}
};
在派生类中加入了一个变量y,然后在D_f()中使用这个y.
居然又访问到了。
有点奇怪。但是很快我们就想出来,应该是指针越界访问到了之后的内存,为了证明这一点,我们查看了此时b1的大小。
int main()
{
Base b1,b2,b3;
Derived *p_b1= (Derived *)&b1;
printf("sizeof b1=%d\n",sizeof(b1));
p_b1->D_f();
}
这个b1的大小为8,查看b1的定义,发现只有一个int 型和一个虚函数。
根据我们上课学的知识,C++为有虚函数的类维护了唯一一个虚函数表。
而每个类中都存有这个虚函数表的位置指针。(对于同一个类,这个指针的值应该是一样的)
仔细查看b1这个对象的各个成员地址,发现
class Base
{
public:
int x;
virtual void B_f(){}
};
在这次运行中,B1的地首地址是764,而b1中的x地址是768,刚好差了4个字节,正好是一个指针的大小
由此可见V这个位置存储的内容就是base类中虚函数表的地址。
那么回到之前的问题,b1中既然没有Y,为什么能够输出y=2?
我们给出的解释是,指针既然解释成一个Derived 类型,而这个类型的变量应该是这样的
那么我们之前的访问,把一个Base变量试图解释成为一个Derived变量,就访问到了不该访问的地方。
就像是这样
为了验证这个结论,我们决定把这个程序给玩坏掉。于是申请了3个变量b1,b2,b3
因为系统在栈上面申请的变量一般是连续的(在debug下可能会在变量之间预留空间来进行越界检查,是导致debug比release生成的文件大很多原因之一。)
修改main函数:
int main()
{
Base b1,b2,b3;
Derived *p_b1= (Derived *)&b1;
printf("&b1.x=%d \n",&b1);
printf("&b2.x=%d \n",&b2);
printf("&b3.x=%d \n",&b3);
p_b1->D_f();
}
果然申请到了一段连续的内存。画在分布图上是这样的。
好吧,现在知道我们要怎么把它给玩坏了吧。
对,我们要通过之前的方法来对b2这个变量后面的不存在的y进行修改,导致b1这个内存位置的本该存放虚函数表的地址的内存被恶意修改了(。我们的确是恶意)。
然后再去访问b1的虚函数B_f()。看会不会报错。
int main()
{
Base b1;
Base b2;
Base b3;
Derived *p= (Derived *)&b2;
p->D_f();
b1.B_f();
}
果然出错了。。我们通过改变b2这个变量的y的值改掉了b1这个变量本来应该存放虚函数表的位置的内存地址(096~1000)的值,
导致对b1这个变量访问虚函数的时候找不到虚函数b_f();
所以说指针是一把双刃剑,可以用指针干出十分危险,十分隐晦的错误,在使用的时候一定要谨慎,可是为什么C++不干脆禁止这类危险的转换呢?
让我们更加理解了C++设计的初衷:
相信程序员,给予程序员充分的自由和权利,不阻止程序员做他们想做的事。
(作者 邓高山)