《征服C指针》摘录
补充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)中,也花费了一章的笔墨进行了讨论。
三
C 语言的变量具有区间性的作用域*。
* 在标准中,“作用域”( scope )和 “连接”(linkage)是分别定义的,用语句块包围的是作用域,static 和 extern 分别控制静态连接和外部连接。对于全局变量,作用域指文件作用域,链接指外部链接。对于程序员来说,这些方式都是控制命名空间的,它们没有什么不同。在本书中,我们统一使用“作用域”这种叫法。
在开发一些小程序的时候,也许我们并不在意作用域的必要性。可是,当你书写几万行,甚至几十万行的代码的时候,没有作用域肯定是不能忍受的。
C 语言有如下三种作用域。
1. 全局变量
在函数之外声明的变量,默认地会成为全局变量。全局变量在任何地方都是可见的。当程序被分割为多个源代码文件进行编译时,声明为全局变量的变量也是可以从其他源代码文件中引用的。
2. 文件内部的静态变量
就算对于像全局变量那样被定义在函数外面的变量,一旦添加了static,作用域就只限定在当前所在的源代码文件中。通过static指定的变量(包括函数),对于其他源代码文件是不可见的。在英语中,static是“静态的”的意思,我实在想不明白为什么这个功能莫名其妙地被冠以“static”,这一点可以算是 C 语言的一个未解之谜。
3. 局部变量
局部变量是指在函数中声明的变量。局部变量只能在包含它的声明的语句块(使用{}括起来的范围)中被引用。局部变量通常在函数的开头部分进行声明,但也可以在函数内部某语句块的开头进行声明。例如,在“交换两个变量的内容时,需要使用一下临时变量”的情况下,将局部变量声明放在当前语句块开头还是比较方便的。局部变量通常在它所在的语句块结束的时候被释放。如果你不想释放某个局部变量,可以在局部变量上加上static进行声明(在后面有详细说明)。
另外,除了作用域不同,C 的变量之间还有存储期(storage duration)的差别。
1. 静态存储期(static storage duration)全局变量、文件内的static变量、指定static的局部变量都持有静态存储期。这些变量被统称为静态变量。持有静态存储期的变量的寿命从程序运行时开始,到程序关闭时结束。换句话说,静态变量一直存在于内存的同一个地址上。
2. 自动存储期(auto storage duration)没有指定static的局部变量,持有自动存储期。这样的变量被称为自动变量。持有自动存储期的变量,在程序运行进入它所在的语句块时被分配以内存区域,该语句块执行结束后这片内存区域被释放*。
* 如果说明得细致一些,在几乎所有的处理环境中,并不是“程序执行进入语句块时”给自动变量分配内存区域,而是在“程序执行进入函数时”统一地进行内存区域分配的。这个特征通常使用“栈”的机制来实现。2.5 节中会对此做详细说明。
接下来就不是“变量”了。C 中可以使用malloc()函数动态分配内存。通过malloc()动态分配的内存,寿命一直延续到使用free()释放它为止。在程序中,如果需要保持一些数据,必须在内存中的某个场所取得相应大小的内存区域。
总结一下,在 C 中有三种内存区域的寿命。
1. 静态变量寿命从程序运行时开始,到程序关闭时结束。2. 自动变量寿命到声明该变量的语句块被执行结束为止。3. 通过malloc()分配的领域寿命到调用free()为止。