我们常常被告知,使用指针前需要判断是否为NULL;如果是NULL而你去使用它就会出问题。真相果真是这样吗?
同事颜廷帅(微博:@颜挺帅)给我看以下一个程序,问我,这段程序执行后,有问题吗?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
这里,p是一个空指针,通过这个空指针,我们访问了函数f。没core,没问题,成功输出了 Test1: Core or not ?
发生了什么事?空指针也能用?
如果我们把f稍作修改,程序其他地方不做任何变动:
void f()
{
cout<<"Test1: Core or not ? "<<a<<endl;//access a
}
那么程序运行后分分钟core掉了。
有没有感觉了?嗯,相信有了。我们继续往下看:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
也没问题。
嗯,你可能已经知道了真相。通过空指针访问东西,只要那个东西是确实存在的,就不会有问题。
怎么理解“确实存在”?它是一个实体,看得见,摸得着。
这得说到C++程序中对象的内存布局。
在C++中,成员函数、静态变量是独立于对象存放的;而普通的数据成员是和对象相关的。
Test1 obj1;
Test1 obj2;
obj1和obj2是共用函数f的,函数f对obj1和obj2是相同的,内存中只有一份实体;而obj1和obj2有自己的实体a。
然而,注意到Test1 *p = NULL;
仅仅是声明式,而非定义式。这时候,没有定义任何的对象出来,通过p如何访问a呢?哪来的a呢?a在内存里并不存在。因此,访问a必定core。
而函数f呢?它是独立于对象存放的,自然没问题。一般说来,f位于程序的代码段,而全局变量一般位于BSS段或者DATA段(这个比较复杂,和该全局变量是否初始化以及初始化为0还是非0有关)。而当我们定义对象时,才为该对象分配内存,才有数据成员a的存在:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
受王杰兄(微博:@skyline09_)启发和提示,其实我们可以换一种形式来理解。
我们知道,C++中,类的非静态成员函数会被编译器改写:
void Test::f()
被改写为类似于void Test__f(Test *const this)
而
Test *p = NULL;
p->f();
将被编译器改写为
Test *p = NULL;
Test__f(p);
因此Test::f
中带有一个值为NULL的this指针(p),如果通过这个空指针p读写数据,就会崩溃。否则,安然无事。
那么,this指针可以操纵哪些东西呢?哦,类的非静态数据成员。而类的静态数据成员,全局变量等,是不会通过this指针访问的,因此,上例中,访问a崩溃,访问global则安全。
最后,我们看一个问题。这个问题,最早我是从杜克伟兄(微博:@小伙伴-小伙伴儿)那里听到的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
没问题。&(*this)就是this,值和p相等。因此上面会输出0。
综上,使用空指针并不一定会发生问题,关键是怎么用。遇到问题得理性分析,不要想当然。
纸上学来终觉浅,绝知此事要躬行。