在谈这个话题之前呢,还是得了解一下内存布局,以x86的32位系统为例:
然后得明确一点,NULL指针是无法访问的,如果强行访问,则会引发异常
然而空对象指针有时候却能够调用成员函数
class C
{
public:
int a;
static int b;
static void f1()
{
cout << "fl" << endl;
}
void f2()
{
cout << "f2" << endl;
}
void f3()
{
cout << "f3:" << a << endl;
}
void f4()
{
cout << "f4:" << b << endl;
}
virtual void f5()
{
cout << "virtual f5" << endl;
}
};
int C::b = 10;
int main()
{
C* p = NULL;
p->f1(); //正确
p->f2(); //正确
p->f3(); //错误
p->f4(); //成功
p->f5(); //错误
return 0;
}
假设现在new了一个对象,并把它赋值给指针p,此时指针p则保存在栈上,对象中的a保存在堆上,b保存在.data段中(数据段),f1、f2、f3、f4和f5都保存在代码段,也就是说非静态成员变量在运行时储存在对象内部,成员函数并不储存在对象内部,而是位于程序的二进制代码里,所以与对象的地址无关。
为了方便理解,可以大致的认为编译会把以上代码转换为以下代码
class C
{
public:
int a;
virtual void f5()
{
cout << "virtual f5" << endl;
}
};
static int b = 10;
static void f1(C* p)
{
cout << "fl" << endl;
}
void f2(C* p)
{
cout << "f2" << endl;
}
void f3(C* p)
{
cout << "f3:" << p->a << endl;
}
void f4(C* p)
{
cout << "f4:" << b << endl;
}
f1和f2调用成功,就是因为函数与对象的地址无关,即使是空地址,也不会去访问。
f4调用成功,因为除了函数与对象的地址无关之外,静态变量的地址也与对象无关。
f3调用失败,因为非静态成员变量在运行时储存在对象内部,在访问时,需要先对p解引用,然后才能访问找到堆中的数据,但是p是空指针,所以报错。
f5调用失败,因为f5是虚函数,在调用虚函数时,会通过虚函数表去查找函数的地址,而虚函数表(存放在.rodata中)可以看成是对象内的隐藏变量,因此也需要对指针p解引用,但是p是空指针,所以报错。
总的来说,空指针能够调用成员函数,其本质就是没有对这个空指针进行解引用,从而引发访问异常,在Linux下,又称为段错误(segmentation fault)