null
很有趣,但有时候会被误解。之所以会造成迷惑,是因为我们会遇到几种类似但又不一样的概念,包括:
null
概念;null
指针常量;NULL
宏;- ASCII字符
NUL
; null
字符串;null
语句。
NULL
被赋值给指针就意味着指针不指向任何东西。null
概念是指指针包含了一个特殊的值,和别的指针不一样,它没有指向任何内存区域。两个null
指针总是相等的。尽管不常见,但每一种指针类型(如字符指针和整数指针)都可以有对应的null
指针类型。
null
概念是通过null
指针常量来支持的一种抽象。这个常量可能是也可能不是常量0,C程序员不需要关心实际的内部表示。
NULL
宏是强制类型转换为void
指针的整数常量0。在很多库中定义如下:
#define NULL ((void *)0)
这就是我们通常理解为null
指针的东西。这个定义一般可以在多种头文件中找到,包括stddef.h、stdlib.h和stdio.h。
如果编译器用一个非零的位串来表示null
,那么编译器就有责任在指针上下文中把NULL
或0当做null
指针,实际的null
内部表示由实现定义。使用NULL
或0是在语言层面表示null
指针的符号。
ASCII字符NUL
定义为全0的字节。然而,这跟null
指针不一样。C的字符串表示为以0值结尾的字符序列。null
字符串是空字符串,不包含任何字符。最后,null
语句就是只有一个分号的语句。
接下来我们会看到,null
指针对于很多数据结构的实现来说都是很有用的特性,比如链表经常用null
指针来表示链表结尾。
如果要把null
值赋给pi
,就像下面那样用NULL
:
pi = NULL;
注意
null
指针和未初始化的指针不同。未初始化的指针可能包含任何值,而包含NULL
的指针则不会引用内存中的任何地址。
有趣的是,我们可以给指针赋0,但是不能赋任何别的整数值。看一下下面的赋值操作:
pi = 0;
pi = NULL;
pi = 100; // 语法错误
pi = num; // 语法错误
指针可以作为逻辑表达式的唯一操作数。比如说,我们可以用下面的代码来测试指针是否设置成了NULL
。
if(pi) {
// 不是NULL
} else {
// 是NULL
}
注意 下面两个表达式都有效,但是有冗余。这样可能更清晰,但是没必要显式地跟
NULL
做比较。
如果这里pi
被赋了NULL
值,那就会被解释为二进制0。在C中这表示假,那么倘若pi
包含NULL
的话,else
分支就会执行。
if(pi == NULL) ...
if(pi != NULL) ...
注意 任何时候都不应该对
null
指针进行解引,因为它并不包含合法地址。执行这样的代码会导致程序终止。
1. 用不用NULL
使用指针时哪一种形式更好,NULL
还是0?无论哪一种都完全没问题,选择哪种只是个人喜好。有些开发者喜欢用NULL
,因为这样会提醒自己是在用指针。另一些人则觉得没必要,因为NULL
其实就是0。
然而,NULL
不应该用在指针之外的上下文中。有时候可能有用,但不应该这么用。如果代替ASCII字符NUL
的话肯定会有问题。这个字符没有定义在标准的C头文件中。它等于字符'\0'
,其值等于十进制0。
0的含义随着上下文的变化而变化,有时候可能是整数0,有时候又可能是null
指针。看一下这个例子:
int num;
int *pi = 0; // 这里的0表示null的指针NULL
pi = #
*pi = 0; // 这里的0表示整数0
我们习惯了重载的操作符,比如星号可以用来声明指针、解引指针或者做乘法。0也被重载了。我们可能觉得不舒服,因为还没习惯重载操作数。
2. void
指针
void
指针是通用指针,用来存放任何数据类型的引用。下面的例子就是一个void
指针:
void *pv;
它有两个有趣的性质:
void
指针具有与char
指针相同的形式和内存对齐方式;void
指针和别的指针永远不会相等,不过,两个赋值为NULL
的void
指针是相等的。
任何指针都可以被赋给void
指针,它可以被转换回原来的指针类型,这样的话指针的值和原指针的值是相等的。在下面的代码中,int
指针被赋给void
指针然后又被赋给int
指针:
int num;
int *pi = #
printf("Value of pi: %p\n", pi);
void* pv = pi;
pi = (int*) pv;
printf("Value of pi: %p\n", pi);
运行这段代码后,指针地址是一样的:
Value of pi: 100
Value of pi: 100
void
指针只用做数据指针,而不能用做函数指针。
注意 用
void
指针的时候要小心。如果把任意指针转换为void
指针,那就没有什么能阻止你再把它转换成不同的指针类型了。
sizeof
操作符可以用在void
指针上,不过我们无法把这个操作符用在void
上,如下所示:
size_t size = sizeof(void*); // 合法
size_t size = sizeof(void); // 不合法
3. 全局和静态指针
指针被声明为全局或静态,就会在程序启动时被初始化为NULL
。下面是全局和静态指针的例子:
int *globalpi;
void foo() {
static int *staticpi;
...
}
int main() {
...
}
图1-6说明了内存布局,栈帧被推入栈中,堆用来动态分配内存,堆上面的区域用来存放全局/静态变量。这只是原理图,静态和全局变量一般放在与栈和堆所处的数据段不同的数据段中。
图1-6:全局和静态指针的内存分配
评论前必须登录!
注册