实用经验 31 臭名昭著空指针到底是什么?

C++语言定义说明,每一种指针类型都有一个特殊值“空指针” 它与同类型的其它所有指针值都不相同,它 “与任何对象或函数的指针值都不相等”。也就是说,取地址操作符 & 永远也不能得到空指针,同样对 malloc() 的成功调用也不会返回空指针,只有失败时malloc() 才返回空指针,这是空指针的典型用法:表示“未分配”或者“尚未指向任何地方”的指针。

空指针在概念上不同于未初始化的指针。空指针可以确保不指向任何对象或函数;而未初始化指针则可能指向任何地方(实用经验30中的野指针)。

每种指针类型都有一个空指针,而不同类型的空指针内部表示可能不尽相同。尽管程序员不必知道内部值,但编译器必须时刻明确需要那种空指针,以便在需要的时候加以区分。

注意:空指针不是任何变量的指针,同样空指针也不是野指针;每种指针都有一个空指针,而且每种变量的空指针也都不是此种变量的地址。

1.怎样在程序里获得一个空指针?

根据语言定义,在指针上下文中的常数0会在编译时转换为空指针。也就是说:在初始化、赋值或比较的时候,如果一边是指针类型的值或表达式,编译器可以确定另一边的常数0为空指针并生成正确的空指针值。因此下面的代码段完全合法:

  char *p = 0;
  if(p != 0)

然而,传入函数的参数不一定被当作指针环境,因而编译器可能不能识别未加修饰的 0“表示”指针。在函数调用的上下文中生成空指针需要明确的类型转换,强制把0看作指针。例如,Unix 系统调用 execl 接受变长的以空指针结束的字符指针参数。它应采用如下正确的调用:

execl("/bin/sh""sh""-c""date"(char *)0);

如果省略最后一个参数的(char *)转换,则编译器无法知道这是一个空指针,从而当作一个0传入。 如果在作用域内有函数原型,则参数传递变为“赋值上下文”,从而可以安全省略多数类型转换,因为原型告知编译器需要指针,使之把未加修饰的0正确转换为适当的指针。函数原型不能为变长参数列表中的可变参数提供类型。在函数调用时对所有的空指针进行类型转换可能是预防可变参数和无原型函数出问题的最安全的办法。0在使用过程中,是否需要强制类型转化可总结如表4-1所示。

表4-1 0使用强制类型转化说明
可使用未加修饰的0需要显示的类型转换
初始化函数调用且作用域内无原型
赋值变参函数调用的可变参数
比较固定参数的函数调用且在作用域内有原型

2.使用“if ( p )” 检查空指针是否可靠?

如果空指针的内部表达不是0会怎么样?当C在表达式中要求布尔值时,如果表达式等于0则认为该值为假,否则为真。换言之,只要写出

if(expr)

无论“expr”是任何表达式,编译器本质上都会把它做如下处理。

if((expr) != 0)

如果用指针 p 代替 “expr”则
if( p ) 等价于 if(p != 0)。
在此比较上下文中,编译器可以看出0实际上是一个空指针常数,并使用正确的空指针值。这里没有任何欺骗;编译器就是这样工作的,并为、二者生成完全一样的代码。空指针的内部表达无关紧要。
所以类似 if( p ) 这样的“缩写”,尽管完全合法,但被一些人认为是不好的风格。

3.NULL 是什么,它是怎么定义的?

作为一种风格,很多人不愿意在程序中到处出现未加修饰的0。因此编译器在stdio.h头文件中定义了预处理宏 NULL为空指针常数。

/* Define NULL pointer value */
#ifndef NULL
	#ifdef __cplusplus
		#define NULL    0
	#else
		#define NULL    ((void *)0)
	#endif
#endif

通过定义可以看出,NULL和0其实没有太大的区别;编译时预处理器会把所有的 NULL 都还原回 0, 而编译还是依照上文的描述处理指针上下文的0。特别是,在函数调用的参数里,NULL之前的类型转换还是需要。

延伸阅读
在使用非全零作为空指针内部表达的机器上, NULL 是如何定义的?

  • 跟其它机器一样:定义为 0 (或某种形式的0)。
  • 当程序员请求一个空指针时,无论写“0“还是“NULL”,都是有编译器来生成适合机器的空指针的二进制表达形式。因此,在空指针的内部表达不为0的机器上定义NULL为0跟在其它机器上一样合法:编译器在指针上下文看到的未加修饰的0都会被生成正确的空指针。

4.如果 NULL和0作为空指针常数是等价的,到底该用哪一个呢?

许多程序员认为指针上下文中都应该使用NULL,以表明该值应该被看作指针。另一些人则认为用一个宏来定义0,只会把事情搞得更复杂,反而令人困惑。所以倾向于使用未加修饰的0。
C程序员应该明白,在指针上下文中 NULL 和 0 完全等价,而未加修饰的0也完全可以接受。任何使用 NULL 的地方都应该看作一种温和的指针提示。 然而程序员并不能依靠它来区别指针0和整数0。

最佳实践:虽然NULL和0具有相同的功能,还是建议使用NULL替代0。这种实践有两个好处:
(1)你可能认为NULL 的值改变了,比如在使用非零内部空指针的机器上,用 NULL会比0有更好的兼容性。但并非如此。

(2)尽管符号常量经常代替数字使用以备数字的改变,但这不是用 NULL 代替 0 的原因。语言本身确保了源码中的 0 (用于指针上下文) 会生成空指针。NULL 只是用作一种格式习惯。

5.NULL 可以确保是 0, 但空指针却不一定?

空指针的内部(或运行期)表达形式,可能并不是全零,而且对不用的指针类型可能不一样。真正的值只有编译器开发者才关心。C++程序的作者永远看不到它们,所以也不用关系。但是这一点你还是需要明白的。

6.利用空指针(NULL),提高程序运行效率

先看下面这个例子,避免使用for循环,减少栈空间内存的使用和减少运行时的计算开销。

void printchar(char* array[]);       //打印字符串函数原形声明 

void  main(void) 
{ 
	char* test[]={"abc","cde","fgh",NULL} ;//添加一个NULL,表示不指向任何地址,值为0
	printchar(test); 
	cin.get();
}

// 打印字符串
void print_char(char* array[]) 
{
	while(*array!=NULL)
	{
		cout<<*array++<<endl; 
	}
}

上例中引用NULL指针不但可以提升程序执行效率,更重要的是它采用的一种编程思想。即通过数组最后一个元素自行决定数组是否结束,可很好的解决数组越界的问题。

请谨记

  • 空指针必须是指向不能被变量分配到的地址。
  • 程序员请求一个空指针时,无论写“0“还是“NULL”,都是有编译器来生成适合机器的空指针的二进制表达形式
  • 利用空指针(NULL)而非0,提高程序运行效率
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值