一、什么是空指针?

空指针 是一个特殊的指针值。

空指针 是指可以确保没有向任何一个对象的指针。通常使用宏定义 NULL 来表示空指针常量值。

空指针 确保它和任何非空指针进行比较都不会相等,因此经常作为函数发生异常时的返回值使用。另外,对于第 5 章的链表来说,也经常在数据的末尾放上一个空指针来提示:“请注意,后面已经没有元素了哦。”

在如今的操作系统下,应用程序一旦试图通过空指针引用对象,就会马上招致一个异常并且当前应用程序会被操作系统强制终止。因此,如果每次都使用 NULL 来初始化指针变量,在错误地使用了无效(未初始化)的指针时,
我们就可以马上发现潜在的 bug。

通常,我们可以根据指针指向的数据类型来明确地区别指针的类型。如果将“指向 int 的指针”赋给“指向 double 的指针”,如今的编译器会报前面提到的警告。但是,只有 NULL,无论对方指向什么类型的变量,都可以被赋值和比较。

偶尔会见到先将空指针强制转型,然后进行赋值、比较操作的程序,这不但是徒劳的,甚至还会让程序变得难以阅读。

二、区分 NULL、'\0' 和 0

经常有一种错误的程序写法:使用 NULL 来结束字符串。

/*
 * 通常,C 的字符串使用 '\0' 结尾,可是因为 strncpy() 函数在 src 的长度大于 len 
 * 的情况下没有使用 '\0' 来结束,所以一板一眼写了一个整理成 C 的字符串形成的函数(企图)
 */
void my_strncpy(char *dest, char *src, int len)
{
    strncpy(dest, src, len);
    dest[len] = NULL;    /* 错误:使用 NULL 来结束字符串!! */
}

上面的代码,尽管在某些运行环境下能跑起来,但无论怎样它就是错误的。因为字符串是使用“空字符”来结束的,而不是用空指针来结束。

在 C 语言标准中,空字符的定义为“所有的位为 0 的字节称为 空字符(null character)”。也就是说,空字符是值为 0 的字符。

空字符在表现上通常使用'\0'。因为'\0'是常量,所以实际上它等同于 0。也许有些吓到你了,'\0'呀'a'呀什么的,它们的数据类型其实并不是 char,而是 int。

另外,在我的环境下,NULL 在 stdio.h 里的定义如下:

#define NULL 0

看到这个,你可能会说:“说来说去,那还不都是 0 嘛。”确实在大部分的情况下是这样的,但背后的事情却异常复杂。

正如前面说的那样,写成 '\0' 和 写成常量的 0 其实是一样的。使用 '\0' 只不过是习惯使然。如果想让代码容易读,遵从习惯是非常重要的。

将 0 当作空指针来使用,除了极其例外的情况,通常是不会发生错误的。

但是,如果在字符串的最后使用 NULL,就必然会发生错误。

标准允许将 NULL 定义成 (void*)0,所以在 NULL 被定义成 (void*)的时候,如果使用 NULL 来结束字符串,编译器必然会提示警告。

看到刚才的关于 NULL 的定义,可能有人会产生下面的推测:

啥呀?所谓的空指针,不就是为 0 的地址嘛。

在 C 中,为 0 的地址上应该是不能保存有效数据的吧?放什么都起不到任何作用,这没什么大不了的。

这种推测好像颇有道理,但也是有问题的。

确实在大多数的环境中,空指针就是为 0 的地址。但是,由于硬件状况等原因,世上也存在值不为 0  的空指针。

偶尔会有人在获得一个结构体之后,先使用 memset() 将它的内存区域清零后再使用。此外,虽然 C 语言提供了动态分配函数 malloc() 和 calloc(),但是抱着“清零后比较好”的观点,偏爱 calloc() 的倒有很多。这样也许可以避免一些难以再现的 bug。

使用 memset() 和 calloc() 将内存区域清零,其实就是单纯地使用 0 来填位。通过这种处理,当结构体的成员中包含指针的时候,这个指针能不能作为空指针来使用,最终是由运行环境来决定的。

顺便说一下,对于 浮点数,即使它的位模式为 0,值也不一定为 0。

说到这里,

哦,原来这样啊,所以要使用宏定义的 NULL  呢。对于空指针的值不为 0 的运行环境,NULL 的值应该被 #define 成别的值吧。

可能会有人产生以上的想法。实际上,这种想法也是有偏差的,这涉及问题的内部根源。

比如,尝试编译下面的代码

int *p = 3;

在我的环境里,会出现以下警告:

warning: initialization makes pointer from integer without a cast

因为 3 无论怎么说都是 int 型,指针和 int 型是不一样的,所以编译器会提示警告。尽管在我的环境里指针和 int 的长度都是 4字节,但还是出现了警告。如今的编译器,几乎都是这样的。

继续,让我们尝试编译下面的代码:

int *p = 0;

这一次没有警告。

如果说将 int 型的值赋予指针就会得到一个警告,那么为什么值为 3 的时候出现警告,值为 0 的时候却没有警告呢?简直匪夷所思!

这是因为在 C 语言中,“当常量 0 处于应该作为指针使用的上下文时,它就作为空指针使用”。上面的例子中,因为接受赋值的对象指针,编译器根据上下文判断出“0 应该作为指针使用”,所以将常数 0 作为空指针来读取。

无论如何,编译器都会针对性地对待“需要将 0 作为指针进行处理的上下文”,所以即便是空指针的值不为 0 的情况下,使用常量 0 来代替空指针也是合法的。

此外,如上所述,有的环境中像下面这样定义 NULL

#define NULL ((void*)0)

ANSI C 中,根据“应该将 0 作为指针进行处理的上下文”的原则,将常量 0 作为指针来处理。因此,显式将 0 强制转型成 void* 是没有意义的。但是在某些情况下,编译器也可能会理解不了“应该将 0 作为指针进行处理的上下文”。

这些情况是:

(1)、没有原型声明的函数的参数

(2)、可变长参数函数中的可变部分的参数

ANSI C 中,因为引入了原型声明,只有在你确实做了原型声明的情况下,编译器才能知道你“想要传递指针”。

可是,对于以 printf() 为代表的可变长参数函数,其可变部分的参数的类型编译器是不能理解的。另外糟糕的是,在可变长参数的函数中,还经常使用常量 NULL 来表示参数的结束(比如 UNIX 的系统调用 execl()函数)。

以上情况下,简单地传递常量 0,会降低程序的可移植性。

因此,通常使用宏定义 NULL 来将 0 强制转型成 void*,可以显式地告之编译器当前的 0 为指针。

延伸阅读:

《征服 C 指针》摘录1:什么是空指针?区分 NULL、0 和 '\0'

《征服 C 指针》摘录2:C变量的 作用域 和 生命周期(存储期)

《征服 C 指针》摘录3:数组 与 指针

《征服 C 指针》摘录4:函数 与 指针

《征服 C 指针》摘录5:函数形参 和 空的下标运算符[]

《征服 C 指针》摘录6:解读 C 的声明

《征服 C 指针》摘录7:练习——挑战那些复杂的声明

《征服 C 指针》摘录1:什么是空指针?区分 NULL、0 和 '\0'的更多相关文章

  1. 《征服 C 指针》摘录2:C变量的 作用域 和 生命周期(存储期)

    在开发一些小程序的时候,也许我们并不在意作用域的必要性.可是,当你书写几万行,甚至几十万行的代码的时候,没有作用域肯定是不能忍受的. C 语言有如下 3 种作用域. 1.全局变量 在函数之外声明的变量 ...

  2. 《征服 C 指针》摘录3:数组 与 指针

    一.数组 和 指针 的微妙关系 数组 是指将固定个数.相同类型的变量排列起来的对象. 正如之前说明的那样,给指针加 N,指针前进“当前指针指向的变量类型的长度 X N”. 因此,给指向数组的某个元素的 ...

  3. 《征服 C 指针》摘录4:函数 与 指针

    一.指向函数的指针 函数名可以在表达式中被解读成“指向函数的指针”,因此,正如代码清单 2-2 的实验那样,写成 func 就可以取得指向函数的指针. “指向函数的指针”本质上也是指针(地址),所以可 ...

  4. 《征服 C 指针》摘录5:函数形参 和 空的下标运算符[]

    一.函数的形参的声明 C 语言可以像下面这样声明函数的形参: void func(int a[]) {     // ... } 对于这种写法,无论怎么看都好像要向函数的参数传递数组. 可是,在 C ...

  5. 《征服 C 指针》摘录6:解读 C 的声明

    一.混乱的声明——如何自然地理解 C 的声明? 通常,C 的声明 int hoge; 这样,使用“类型 变量名;”的形式进行书写. 可是,像“指向 int 的指针”类型的变量,却要像下面这样进行声明: ...

  6. 《征服 C 指针》笔记6:练习——挑战那些复杂的声明

    应该是小试牛刀的时候了. 在 ANSI C 的标准库中,有一个 atexit()函数.如果使用这个函数,当程序正常结束的时候,可以回调一个指定的函数. atexit()的原型定义如下: int ate ...

  7. 《征服C指针》读书笔记

    本文同时发布在我的个人博客上,欢迎访问~ www.seekingdream.cn 在读完K&R之后,对C的认识就是指针.数组.网上的人们对指针也有些“敬而远之”的感觉.最近从同学处淘得< ...

  8. 空指针、NULL指针、零指针

    1. 空指针.NULL指针.零指针 1.1 什么是空指针常量 0.0L.'\0'.3 - 3.0 * 17 (它们都是“integer constant expression”)以及 (void*)0 ...

  9. 聊一聊c++中指针为空的三种写法 ----->NULL, 0, nullptr

    看到同事用了一下nullptr.不是很了解这方面东东,找个帖子学习学习 http://www.cppblog.com/airtrack/archive/2012/09/16/190828.aspx N ...

随机推荐

  1. OpenStack 企业私有云的若干需求(3):多租户和租户间隔离(multi-tenancy and isolation)

    本系列会介绍OpenStack 企业私有云的几个需求: 自动扩展(Auto-scaling)支持 多租户和租户隔离 (multi-tenancy and tenancy isolation) 混合云( ...

  2. print输出格式总结

    妈的,今天又被printf坑了一回...看来需要一次性总结书所有结果,省的又出现这样那样的麻烦.. #include<stdio.h> #include<string.h> # ...

  3. maven中央仓库访问速度太慢的解决办法

    方法一:修改settings.xml eclipse中集成的maven的settings.xml文件,找了半年也没找到,我们放弃eclipse中的maven,下一个最新的maven,并在eclipse ...

  4. Redis学习笔记~conf自主集群模式

    回到目录 Redis自主提供了集群模式,当然也只是比较简单的读写分离模式,或者叫主从模式,它在各个redis服务端自己做数据同步机制,当然就是将主服务端的信息同步到各个slave服务器上,在客户端集成 ...

  5. WPF中RadioButton绑定数据的正确方法

    RadioButton一般用于单选的时候,也就是从一组值中选择一个值. 比如性别有“男”和“女”两种取值,而对于一个员工的实例来说,性别的取值要么是男,要么是女. 这种时候一般就会用到RadioBut ...

  6. 【抓包工具】wireshark

    wireshark下载地址:http://download.csdn.net/detail/victoria_vicky/8819777 一.wireshark优劣势 wireshark劣势:只能查看 ...

  7. Java构造和解析Json数据

    BaseResult wyComany = propertyService.getWyCompanyById(CommunityInfos.getWyCompany());//这里返回的是json字符 ...

  8. localStorage与sessionStorage 的区别

    通过一枚页面计数器来区别localStorage与sessionStorage. 通过一个计数变量pageconut,每刷新页面,增加的是localStorage的数量,而sessionStorage ...

  9. Asp.Net 自定义储存Session方式

    介绍 由于针对于自定义Session存储方式比较少,所以整理了使用自定义Session的方式.用于构建自定义会话存储提供程序代码,而不是使用默认的 SessionStore 介绍 背景 本文使用的是m ...

  10. jQuery给动态添加的元素绑定事件的方法

    我们在开发过程会遇到无法给动态元素添加绑定事件,解决方案如下: 例如 <div id="testdiv">   <ul></ul> </d ...