在阅读本文之前,希望读者对WINDOWS下程序的运行方式以及内存管理机制有基本的了解。
一、NULL指针和零值指针(null pointer and zero value pointer)
我们查看一下C++标准库定义的NULL指针
// Define NULL pointer value
#ifndef NULL
# ifdef __cplusplus
# define NULL 0
# else
# define NULL ((void *)0)
# endif
#endif // NULL
也就是说,NULL是一个宏,在C++里面被直接被定义成了整数立即数类型的0,而在没有__cplusplus定义的前提下,就被定义成一个值是0的void *类型指针常量,因此很多程序员就会认为零值指针就是NULL(空)指针。
这种认识是错误的!
零值指针,如其名,是值是0的指针,可以是任何一种指针类型,可以是通用变体类型void *也可以是char *,int *等等
空指针,其实空指针只是一种编程概念,就如一个容器可能有空和非空两种基本状态,而在非空时可能里面存储了一个数值是0,因此空指针是人为认为的指针不提供任何地址讯息,类似于container.empty()。
因此,零值指针和空指针完全是两种不同的概念,所以本文题目是“零值指针指向何处”而非“空指针指向何处”,如果出现“空指针指向何处”的问题,就像在问“空的盒子里面有什么”一样荒谬。
那么为为什么标准里面会这样定义空指针呢?
由于C++里面,任何一个概念都要以一种语言内存公认的形式表现出来,例如std::vector会提供一个empty()子函数来返回容器是否为空,然 而对于一个基本数值类型(或者说只是一个类似整数类型的类型)我们不可能将其抽象成一个类(当然除了auto_ptr等只能指针)来提供其详细的状态说 明,所以我们需要一个特殊值来最为这种状态的表现。
C++标准同一公认地规定,当一个指针类型的数值是0时,认为这个指针是空的。(我们在其他的标准下或许可以使用其他的特殊值来定义我们需要的NULL实现,可以是1,可以是2,是随实现要求而定的,但是在标准C++下面我们用0来实现NULL指针)
标准的原版是这么写的:
[6.3.2.3-3] An integer constant expression with the value 0, or such an expression cast to type void *, is called a null pointer constant.
[6.3.2.3-3] If a null pointer constant is converted to a pointer type, the resulting pointer, called a null pointer, is guaranteed to compare unequal to a pointer to any object or function.所以在标准里面我们是用一个特殊值来代替这种概念而不是用值来定义这个概念。
所以我们还可以认为C++ STANDARD里面定义的NULL指针就是零指针,它可以是0,可以是0*17,甚至可以是'/0',C++标准没有一定要求其被转换成指针类型,只要 其代表的数值是0,赋值后就被认为是NULL的。至于系统选取哪种形式作为空指针常量使用,则是实现相关的。一般的 C 系统选择 (void*)0 或者 0 的居多(也有个别的选择 0L);至于 C++ 系统,由于存在严格的类型转化的要求,void* 不能象 C 中那样自由转换为其它指针类型,所以通常选 0 作为空指针常量,而不选择 (void*)0。
相关的概念还可以看到:
[6.3.2.3-Footnote] The macro NULL is defined in <stddef.h> (and other headers) as a null pointer constant
二、对空指针实现的保护政策
既然我们选择了0作为空的概念,在非法访问空的时候我们需要保护以及报错。因此,编译器和系统提供了很好的政策。
我们程序中的指针其实是WINDOWS内存段偏移后的地址,而不是实际的物理地址,所以不同的我们程序的程序中的零值指针指向的同一个0的地址,其实在内 存中都不是物理内存的开端的0,是分段的内存的开端,这里我们需要简单介绍一下WINDOWS下的内存分配和管理制度:
WINDOWS下,执行文件(PE文件)在被调用后,系统会分配给它一个额定大小的内存段用于映射这个程序的所有内容(就是磁盘上的内容)并且为这个段进 行新的偏移计算,也就是说我们的程序中访问的所有NEAR指针都是在我们“自家”的段里面的,当我们要访问FAR指针的时候,我们其实是跳出了“自家的院 子”到了他人的地方,我们需要一个段偏移地址来完成新的偏移(人家家里的偏移)所以我们的指针可能是OE02:0045就是告诉系统我们要访问0E02个 内存段的0045好偏移,然后WINDOWS会自动给我们找到0E02段的开始偏移,然后为我们计算真实的物理地址。
所以程序A中的零值指针和程序B中的零值指针指向的地方可能是完全不同的。
保护政策:
我们的程序在使用的是系统给定的一个段,我们程序中的零值指针指向这个段的开端,为了保证NULL概念,系统为我们这个段的开头64K内存做了苛刻的规 定,根据虚拟内存访问权限控制,我们程序中(低访问权限)访问要求高访问权限的这64K内存被视作是不容许的所以会必然引发 Access Volitation 错误,而这高权限的64K内存是一块保留内存(即不能被程序动态内存分配器分配,不能被访问,也不能被使用),就是简单的保留,不作任何使用。
三、关于NULL指针的其他
1)指针初始化
我们在直接定义一个指针后我们并不知道这个指针指向何处(而不是有些程序员认为的如同JAVA等语言会自动零值初始化)所以我们一旦非法地直接访问这些未 知地内容时,极其有可能会触碰到我们程序所不能触碰地内存(这时类似64K限制地保护政策又会起效,就如同你不仅随意闯入了陌生人的家(野指针),而且拿 着刀子要问他要钱(访问),警察(WINDOWS内存访问保护政策)当然请你去警察局(报错)谈谈),所以养成良好的指针初始化(赋值为NULL)[BU REN TONG]以及使 用FREE(或者时DELETE)之后立即再初始化为空是十分必要的!
2)malloc 函数在分配内存失败时返回 0 还是 NULL?
不同于C++里面的NEW再内存失败是会抛出一个BAD_ALLOC异常,malloc 函数是标准 C 规定的库函数。在标准中明确规定了在其内存分配失败时返回的是一个 “null pointer”(空指针):
[7.20.3-1] If the space cannot be allocated, a null pointer is returned.
对于空指针值,而非零值指针!
//--------------------------------------------------------------------------------------------------------------------------------------
首 先解答第一个问题,在windows核心编程第四版的windows的内存结构一章中,表13-1有提到NULL指针分配的分区。其范围是从 0x00000000到0x0000FFFF。这段空间是空闲的,对于空闲的空间而言,没有相应的物理存储器与之相对应,所以对这段空间来说,任何读写操 作都是会引起异常的。
有了上面的解答后,第二个问题就很容易解答了。NULL的定义出现以下几个地方:
stdio.h文件中
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
ios.h文件中
#ifndef NULL
#define NULL 0
#endif
windef.h文件中
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
可见,NULL的值,基本上是用0来表示的,是不是只能用0呢?在windows xp sp2的系统平台下,如果执行下面代码也是会发生异常的:
int * pAddr = (int *)0x0000ffff;
*pAddr = 1;
而下面的代码是不会出问题的:
int * pAddr = (int *)0x00010000;
*pAddr = 1;
为 什么呢?在windows xp sp2下发现0x00000000到0x0000FFFF是空闲区间,而0x00010000所处的是进程的私有区间。我想第二个问题应该已经解决了,我 想,空指针是程序无论在何时都没有物理存储器与之对应的地址。为了保障“无论何时”这个条件,需要人为划分一个空指针的区域,固有上面NULL指针分区。
在 第二个问题的基础上,要解答NULL指针的范围,那就相对来说容易了,对于在32位x86计算机上运行的windows xp sp2来说,就是从0x00000000到0x0000ffff。为什么分配如此大的空间?而在定义NULL的时候,只使用了 0x00000000这么一个值,这不是浪费吗?我想,这是操作系统地址空间的分配粒度相关的,windows xp sp2的分配粒度是64KB,为了达到对齐,空间地址需要从0x00010000开始分配,故空指针的区间范围有那么大。