【C++】特殊字符“\0”,以及NULL相关

我们都知道,’\0’是字符串的结束标记。因此,执行这段代码:

#include<bits/stdc++.h>
using namespace std;
int main(){
    cout<<"ab\0cd";
}

 输出结果:ab

这是因为,cout默认判断字符串到结束符号\0,认为字符串结束了,因此就停止。

事实上,\0是一个非打印字符,也就是不能被打印出来的字符。如果直接尝试使用cout或者putchar输出\0,什么也不会发生。ascii码为0-31之间的字符都是非打印字符。

 

下面内容引用自《征服C指针》,是NULL相关内容。注意0对应的字符不是NULL,NULL表示空指针,不是字符。

(NULL是来自stdio的一个宏定义,一般是这样:#define NULL ((void*)0) )

 

补充NULL、0 和'\0'

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

/*通常,C 的字符串使用''结尾,可是因为strncpy()函数在 src 的长度大于len的情况下没有使用'\0'来结束,所以一板一眼地写了一个整理成C 的字符串形式的函数(企图)/

void my_strncpy(char dest, char src, int len) { 

strncpy(dest, src, len); 

dest[len] = NULL; ←使用NULL 来结束字符串!!

}

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

在 C 语言标准中,空字符的定义为“所有的位为 0 的字节称为空字符(nullcharacter)”(5.2.1)。也就是说,空字符是值为 0 的字符。空字符在表现上通常使用'\0'。因为'\0'是常量,所以实际上它等同于 0。也许有些吓到你了,'\0'呀'a'呀什么的,它们的数据类型其实并不是char,而是int*。

* 如果是C++,就不是这个结论了。

另外,在我的环境中,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 作为指针进行处理的上下文”。这些情况是:

没有原型声明的函数的参数

可变长参数函数中的可变部分的参数

ANSI C 中,因为引入了原型声明,只有在你确实做了原型声明的情况下,编译器才能知道你“想要传递指针”。可是,对于以printf()为代表的可变长参数函数,其可变部分的参数的类型编译器是不能理解的。

另外糟糕的是,在可变长参数的函数中,还经常使用常量NULL来表示参数的结束(比如 UNIX 的系统调用execl()函数)。以上情况下,简单地传递常量 0,会降低程序的可移植性。因此,通过使用宏定义NULL来将 0 强制转型成void*,可以显式地告之编译器当前的0 为指针*。

* 关于这个话题,在 C 语言 FAQ(http://www.catnet.ne.jp/kouno/c_faq/c_faq.htm)中,也花费了一章的笔墨进行了讨论。

posted @ 2021-08-02 21:29  计算机知识杂谈  阅读(5036)  评论(1编辑  收藏  举报