null指针和void指针

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指针和别的指针永远不会相等,不过,两个赋值为NULLvoid指针是相等的。

任何指针都可以被赋给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说明了内存布局,栈帧被推入栈中,堆用来动态分配内存,堆上面的区域用来存放全局/静态变量。这只是原理图,静态和全局变量一般放在与栈和堆所处的数据段不同的数据段中。

enter image description here

图1-6:全局和静态指针的内存分配

赞(3)

评论 抢沙发

评论前必须登录!

 

C指针