Yebangyu's Blog

Fond of Concurrency Programming and Machine Learning

空指针(NULL)不能用吗?

我们常常被告知,使用指针前需要判断是否为NULL;如果是NULL而你去使用它就会出问题。真相果真是这样吗?

同事颜廷帅(微博:@颜挺帅)给我看以下一个程序,问我,这段程序执行后,有问题吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include<iostream>
#include<cstdlib>
using namespace std;
class Test1
{
private:
  int a;
public:
  void f()
  {
    cout<<"Test1: Core or not ? "<<endl;
  }
};
int main()
{
  Test1 *p = NULL;
  p->f();//会core吗?会出大事吗?
  return 0;
}

这里,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
#include<iostream>
#include<cstdlib>
using namespace std;
static int global = 1;
class Test2
{
private:
  int a;
public:
  void f()
  {
    cout<<"Test1: Core or not ? "<<global<<endl;//access global
  }
};
int main()
{
  Test2 *p = NULL;
  p->f();//会core吗?会出大事吗?
  return 0;
}

也没问题。

嗯,你可能已经知道了真相。通过空指针访问东西,只要那个东西是确实存在的,就不会有问题。

怎么理解“确实存在”?它是一个实体,看得见,摸得着。

这得说到C++程序中对象的内存布局。

C++中,成员函数、静态变量是独立于对象存放的;而普通的数据成员是和对象相关的。

Test1 obj1;
Test1 obj2;

obj1obj2是共用函数f的,函数fobj1obj2是相同的,内存中只有一份实体;而obj1obj2有自己的实体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
#include<iostream>
#include<cstdlib>
using namespace std;
static int global1 = 1; //DATA段
static int global2; //BSS段
class Test3
{
private:
  int a;
public:
  void f()
  {
    cout<<"Test1: Core or not ? "<<endl;
  }
};
Test3 obj3; //DATA段 (调用默认构造函数进行初始化)
int main()
{
  Test3 obj1; //stack段。定义obj1,这时候自然为a分配内存了
  Test3 *obj2 = new Test3();//heap段,也为a分配内存了。
  Test3 *p;//只是声明,没有定义。没有对象,也没有a。
  return 0;
}

受王杰兄(微博:@skyline09_)启发和提示,其实我们可以换一种形式来理解。

我们知道,C++中,类的非静态成员函数会被编译器改写:

void Test::f()被改写为类似于void Test__f(Test *const this)

Test *p = NULL;

p->f();

将被编译器改写为

Test *p = NULL;
Test__f(p);

因此Test::f中带有一个值为NULLthis指针(p),如果通过这个空指针p读写数据,就会崩溃。否则,安然无事。

那么,this指针可以操纵哪些东西呢?哦,类的非静态数据成员。而类的静态数据成员,全局变量等,是不会通过this指针访问的,因此,上例中,访问a崩溃,访问global则安全。

最后,我们看一个问题。这个问题,最早我是从杜克伟兄(微博:@小伙伴-小伙伴儿)那里听到的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Test4
{
public:
  void f()
  {
    cout<<&(*this)<<endl; // 有问题吗?
  }
};
int main()
{
  Test4 *p = NULL;
  p->f();
  return 0;
}

没问题。&(*this)就是this,值和p相等。因此上面会输出0

综上,使用空指针并不一定会发生问题,关键是怎么用。遇到问题得理性分析,不要想当然。

纸上学来终觉浅,绝知此事要躬行。