所谓 空指针
,其实就是指内存地址0
。这个地址操作系统保留,是不能够被访问的。所以,尝试访问一个空指针
,程序就会崩溃。
而大家熟悉的 segmentation fault
(段错误,段异常),就是因为访问一个不存在的地址 (或尝试访问操作系统的地址)。
程序就会崩溃
,不是说程序运行不下去了。我们常见的Intel x86系列处理器在遇到这类错误时候回产生一个中断,而操作系统收到这个中断以后,就知道,现在运行的程序出现了错误,把他kill
(杀死,结束)掉吧。
Windows系列系统在遇到这类问题,会显示如下图的一个对话框。而Linux系统则更加直白,直接在屏幕上显示segmentation fault
,然后就没有然后了。
其实今天这个主题和空指针
的关系不算特别特别的大,但是也是非常大的(笑)。大家可以看下面这个简短而有趣的小程序:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct student {
char *name;
int class;
int grade;
};
void get_student(struct student *s);
int main(void) {
struct student s;
get_student(&s);
printf("student name: %s, class %d grade %d. \n",
s.name, s.class, s.grade);
return 0;
}
void get_student(struct student *s) {
struct student students[16];
char name[9] = "student A";
int i;
for (i = 0; i < 16; i++) {
students[i].name = malloc(sizeof(char) * 9);
strcpy(students[i].name, name);
students[i].grade = 6;
students[i].class = 2;
/* update student name */
name[8]++;
printf("new student name: %s\n", name);
}
s = students + i;
return;
}
(当然,大家可能一眼就能看出错误来…) 这是笔者曾经犯过的一个错误 (在Cunix的文件系统中出现, https://gitee.com/pengruiyang-cpu/lessons/blob/master/0x00000002/code/cfs.c或https://github.com/pengruiyang-cpu/lessons/blob/master/0x00000002/code/cfs.c,read_inode函数),当时甚至费了大功夫从汇编语言分析,最终错误原因把笔者一顿气。
到底是什么错误呢?大家可以先尝试编译运行上边的代码,在笔者的想象中,这段代码应该将main函数中的s变量设为get_value函数中的变量students的最后一个元素 (大吸气) ,可是运行结果却又是一个诡异的结果。
结果竟然是空?嗯,原因笔者是知道的,请听我细细道来…
类似main函数中s这样在函数结束后就没有用了的变量,编译器会将他们放在栈中。栈就是一个典型的先进后出的缓冲区,他的顶端地址由一个寄存器sp
保存。sp
的地址便是上一个进栈的元素的顶地址 (我敢打赌你没听懂)。
那么,上一个进栈元素的地址就是sp
- 这个元素的大小,这个这个元素的大小
一般为一个固定值,32位为2字节,64位为8字节,就是他们的最大字节数(或说成"字的大小")。
因为栈这玩意儿是固定大小的,所以,我们要想一直将他用下去,就得手动的(对于编译器来讲)将他们收回。只需要将sp
寄存器的值更改一下就可以了。
当函数执行完毕后,编译器就会自动的回收栈中使用的部分(平衡栈)。所以,所有在栈中的变量都是临时的,只要函数一结束,他们的姓命也就结束了。
我们再来看代码。在函数get_student
中,students是一个临时的变量。而函数的最后将参数s
赋值为它(s = students + i
),这样,赋值到的只是地址,而不是他的值。在函数get_student
返回后,s保存的内存地址是在栈中,这导致它获得了一个诡异的结果
。
知道了错误原因,解决办法就简单了。我们可以为get_student
函数中的students
申请一块不在栈中的内存,也可以将students
中的内容直接复制到s
里。第二种方法可以不用多申请内存,而第一种方法比较简单。具体的决定权在你手里头。
通过这次错误报告,我们知道了:
在栈中的临时变量不可以作为全局或看做是申请过的内存,其值是易失的。所以,我们必须采用申请或复制的方法来确保该值不会随着函数的返回,栈的回收而失去。
当然,更重要的是:
作为一个学C语言的,怎么可以不懂点汇编呢?