边玩崩程序边理解C++(一) 类和指针

周日下午,小伙伴们开开心心地到会议室来讨论。

开始,小孟给我们讲解关于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 Athis){};

void g(struct Athis,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++设计的初衷:

相信程序员,给予程序员充分的自由和权利,不阻止程序员做他们想做的事。


(作者 邓高山)


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值