C语言:空指针详细解读
一个指针变量可以指向计算机中的任何一块内存,不管该内存有没有被分配,也不管该内存有没有使用权限,只把地址给他,它就可以指向,c语言没有一种机制来保证指向的内存的正确性,程序员必须自己提高警惕。
1. #include <stdio.h>
2.
3. int main(){
4. char *str;
5. gets(str);
6. printf("%s\\n", str);
7. return 0;
8. }
这段程序没有语法错误,能够通过编译和链接,但当用户输入完字符串并按下回车键时就会发生错误,在linux下表现为段错误,在windows下程序直接崩溃,如果你足够幸运,或者输入的字符串少,也可能不报错,这都是未知的。
前面我们讲过,未初始化的局部变量的值是不确定的,c语言并没有对此作出规定,不同的编译器有不同的实现,不要直接用未初始化的局部变量,上面的代码中,str就是一个为初始化的局部变量,它的值是不确定的,究竟指向哪块内存也是未知的,大多胡情况下这块内存没有被分配或者没有读写权限,使用gets)函数向它里面写入数据显然是错误的。
强烈建议对没有初始化指针赋值NULL,例如:
char *str = NULL;
NULL是“零值,等于零”的意思,在c语言中表示指针,从表面上理解,空指针是不指向任何数据的指针,是无效指针,程序使用它不会产生效果。
注意区分大小写,null没有任何特殊含义,只是一个普通的标识符。
很多库函数都对传入的指针做了判断,如果是空指针就不做任何操作,或者给出提示信息,更改上面的代码,给str赋值NULL,看看会有什么效果
1. #include <stdio.h>
2.
3. int main(){
4. char *str = NULL;
5. gets(str);
6. printf("%s\\n", str);
7. return 0;
8. }
运行程序后发现,还未等用户输入任何字符,printf()就直接输出了(null),我们有理由据此推断,gets()和printf()都对空指针做了特殊处理:
- gets()不会让用户输入字符串,也不会向指针指向的内存中写入数据;
- printf()不会读取指针指向的内容,只是简单地给出提示,让程序员意识到用了一个空指针
我们在自己定义的函数中也可以进行类似的判断,列如:
1. void func(char *p){
2. if(p == NULL){
3. printf("(null)\\n");
4. }else{
5. printf("%s\\n", p);
6. }
7. }
这样能够从很大程度上增加程序的健壮性,防止对空指针进行无意义的操作。
其实,NULL 是在 stdio.h 中定义的一个宏,它的具体内容为:
#define NULL ((void *)0)
(void *)0 表示把数值 0 强制转换为 void *类型,最外层的( )把宏定义的内容括起来,防止发生歧义。从整体上来看,NULL 指向了地址为 0 的内存,而不是前面说的不指向任何数据。
在进程的虚拟地址空间中,最低地址处有一段内存区域被称为保留区,这个区域不存储有效数据,也不能被用户程序访问,将NULL指向这块区域很容易被检测到违规指针。
在大多数操作系统中,极小的地址通常不保存数据,也不允许程序访问,NULL可以指向这段地址区间的任何一个地址。
注意,c语言有没有规定NULL的指向,知识大部分标准库约定成俗地将NULL指向0,所以不要将NULL和0等同起来,例如下面的写法是不专业的:
int *p = 0;
而是应该坚持写为:
int *p = NULL;
注意:NULL和NUL的区别:NULL表示空指针,是一个宏定义,可以在代码中直接使用,而NUL表示字符串的结束标志’\0’,它是ASCLL表中的第0个字符.null没有在c语言中定义,仅仅是对‘\0’的称呼,不能在代码中直接使用。
void指针
对于空指针NULL的宏定义内容,上面知识对(void*)0做了粗略的介绍,这里重点说一下void*的含义。
void用在函数定义中可以表示函数没有返回值或者没有参数形式,用在这里表示指针指向的数据的类型是未知的。
也就是说,void*表示一个有效指针,它确实指向实实在在的数据,知识数据类型尚未确定,在后续使用过程中一般要进行强制类型转换。
c语言动态内存分配函数malloc()的返回值就是void*类型,在使用时要进行强制类型转换,请看下面的例子:
1. #include <stdio.h>
2.
3. int main(){
4. //分配可以保存 30 个字符的内存,并把返回的指针转换为 char *
5. char *str = (char *)malloc(sizeof(char) * 30);
6. gets(str);
7. printf("%s\\n", str);
8. return 0;
9. }
运行结果:
c.biancheng.net↙
[c.biancheng.net](<http://c.biancheng.net/>)
void *它不是空指针的意思,而是实实在在的指针,只是指针指向的内存中不知道保存的是什么类型的数据。
数组!=指针
数组和指针不等价的一个典型案例就是求数组的长度,这个时候只能使用数组名,不能使用数组指针
1. #include <stdio.h>
2.
3. int main(){
4. int a[6] = {0, 1, 2, 3, 4, 5};
5. int *p = a;
6. int len_a = sizeof(a) / sizeof(int);
7. int len_p = sizeof(p) / sizeof(int);
8. printf("len_a = %d, len_p = %d\\n", len_a, len_p);
9. return 0;
10. }
运行结果:
len_a=6,len_p=1
数组是一系列数据的集合,没有开始和结束标志,p 仅仅是一个指向 int 类型的指针,编译器不知道它指向的是一个整数还是一堆整数,对 p 使用 sizeof 求得的是指针变量本身的长度。也就是说,编译器并没有把 p 和数组关联起来,p 仅仅是一个指针变量,不管它指向哪里,sizeof 求得的永远是它本身所占用的字节数。
站在编译器的角度讲,变量名、数组名都是一种符号,它们最终都要和数据绑定起来。变量名用来指代一份数据,数组名用来指代一组数据(数据集合),它们都是有类型的,以便推断出所指代的数据的长度。
对,数组也有类型,这是很多读者没有意识到的,大部分 C 语言书籍对这一点也含糊其辞!我们可以将 int、float、char 等理解为基本类型,将数组理解为由基本类型派生得到的稍微复杂一些的类型。sizeof 就是根据符号的类型来计算长度的。
对于数组 a,它的类型是 int [6],表示这是一个拥有 6 个 int 数据的集合,1 个 int 的长度为 4,6 个 int 的长度为 4×6 = 24,sizeof 很容易求得。
对于指针变量 p,它的类型是 int *,在 32 位环境下长度为 4,在 64 位环境下长度为 8。
归根结底,a 和 p 这两个符号的类型不同,指代的数据也不同,它们不是一码事,sizeof 是根据符号类型来求长度的,a 和 p 的类型不同,求得的长度自然也不一样。
对于二维数组,也是类似的道理,
例如 int a[3][3]={1, 2, 3, 4, 5, 6, 7, 8, 9};,它的类型是 int [3][3],长度是4×3×3 = 36,读者可以亲自测试。
站在哲学的高度看问题
编程语言的目的是为了将计算机指令抽象成人类能够理解的自然语言,让程序员能够更容易地管理和操作各种计算机资源,这些计算机资源最终表现为编程语言中的各种符号和语法规则。
整数、小数、数组、指针等不同类型的整数都是对内存的抽象,他们的名字用来指代不同的内存块,程序员在编程过程中不需要直接接面对内存,使用这些名字更加方便。
编译器在编译的过程中会创建一张专门的表格用来保存名字以及名字对应的数据类型、地址、作用域等信息,sizeof是一个操作符,不是函数,使用sizeof时可以从这张表格中查询符号的长度。
与普遍变量名相比,数组名既有一般也有特殊性;一般性表现在数组名也用来指代特定的内存块,也有类型和长度;特殊性表现在数组名有时候会转换为一个指针,而不是它所指代的数据本身的值