一、什么是空指针?

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

空指针 是指可以确保没有向任何一个对象的指针。通常使用宏定义 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. Merge k Sorted Lists

    1. Merge Two Sorted Lists 我们先来看这个 问题: Merge two sorted linked lists and return it as a new list. The ...

  2. 【Python数据分析】工作日发文章比周末发文章访问量高?

    前言 看前面有位朋友分析了一下每天某个时间发文章的访问量区别,以讨论非系统性因素对文章访问量的影响.之所以进一步讨论工作日和周末发文对文章访问量的影响,一是觉得很有意思,二是毕业设计与此有很大关系,三 ...

  3. UVALive 4426 Blast the Enemy! --求多边形重心

    题意:求一个不规则简单多边形的重心. 解法:多边形的重心就是所有三角形的重心对面积的加权平均数. 关于求多边形重心的文章: 求多边形重心 用叉积搞一搞就行了. 代码: #include <ios ...

  4. 访问服务端的HttpProxy

    tip:加密部分暂时先注释掉 package com.zqc.share.manager.framework; import java.net.HttpURLConnection; import ja ...

  5. 微信小程序之页面路由(九)

    [未经允许,请勿以任何形式转载] 什么是路由? 我们通常理解的路由指分组数据包从源到目的地时,决定端到端路径的网络范围的进程: 借用上面的定义,我们可以理解小程序页面路由,根据路由规则(路径)从一个页 ...

  6. WPF实现物理效果 拉一个小球

    一直以来都对物理效果有神秘感,完全不知道怎么实现的.直到看到了周银辉在老早前写的一篇博客:http://www.cnblogs.com/zhouyinhui/archive/2007/06/23/79 ...

  7. elasticsearch suggest 的几种使用-completion 的基本 使用

    在lucene里面,suggest 的支持非常完善,可以随心所欲的定制: 但是在es中使用起来就没有那么方便了. es给suggest 分类4类:term :phrase: completion: c ...

  8. C#进阶系列——DDD领域驱动设计初探(七):Web层的搭建

    前言:好久没更新博客了,每天被该死的业务缠身,今天正好一个模块完成了,继续来完善我们的代码.之前的六篇完成了领域层.应用层.以及基础结构层的部分代码,这篇打算搭建下UI层的代码. DDD领域驱动设计初 ...

  9. js实现点击修改按钮之后单元格变成可编辑状态

    主要实现原理: 每一行有一个修改按钮 点击修改之后,获取行对象,通过行对象再获取行中单元格数组.然后把每一个单元格中的innerHTML替换成input输入框,并赋值value=原来单元格中的内容,鼠 ...

  10. C#、ASP.NET获取当前应用程序的绝对路径,获取程序工作路径 (转帖)

    C#.ASP.NET获取当前应用程序的绝对路径,获取程序工作路径   ============================================ 使用 Application.Start ...