前言:
指针是C语言的独特数据类型。在Java、Go等其他常见编程语言中,很少见到指针的身影。可以说,指针是C语言的一种特色。C语言本身作为最接近汇编语言的编程语言,指针多种运算也代表了汇编语言中对于地址的运算。如:
1、直接寻址
直接将地址放入寄存器或者内存单元,对应一级指针。
2、间接寻址
将地址放入寄存器或内存单元,根据寄存器或内存单元的内容找到最终的地址,对应多级指针。
3.、相对寻址
使用偏移量来计算最终的地址,对应指针的数量运算。
指针的使用为汇编语言提供了更灵活的操作方式,可以方便地处理复杂的内存布局和数据结构,并且可以大大提高程序的运行速度和效率。
事实上,整个C语言基本都是由指针构成的。你甚至可以打印出main函数、printf函数的指针。C语言只不过把指针稍微做了一点包装。我们要做的,就是揭开这层帷幕。
一、指针的含义
我们平时说的指针,其实和真正意义上的指针还是有一点区别的。当我们写出如下代码时:
int a=3;
int *p=&a;
其实就是将a的地址赋给了p,p的数据类型是指针变量。指针变量和指针是有区别的!
通常意义上,指针就是地址,指针变量是一个储存地址的容器。虽然上述例子中p是一个指针变量而非指针,但方便起见,我们常常把p叫做指针,即使它只是一个容器。在下面的讲解中,我们也将指针变量称为指针,但读者应该明白两者的区别。
二、指针的数据大小和存储方式
指针作为一种特殊的数据类型,也具有一定的大小。根据环境的不同,指针的大小也不同。在X86环境下(32位),指针大小为4个字节;在X64环境下(64位),指针大小为8个字节。许多初学者常常犯一个错误,就是认为指针指向的数据类型不同,指针的大小也不同。事实上,即使一个指针指向char类型,一个指针指向long long int类型,两个指针的大小也完全相同。
指针的大小只取决于编译环境!
指针在计算机中以十六进制整数的方式存储。每个指针指向的区域都对应内存中的一块存储空间。
三、指针指向的数据类型到底代表什么
既然指针指向的数据类型不会影响指针的大小,指针指向的区域也是固定的大小,那么指针指向的数据类型有什么用呢?
我们知道,指针可以通过解引用来访问指针指向的内存空间,获得内存空间里的数据。而内存空间的单位是字节。一个int型数据,一般占据4个字节。也就是说,为了访问int这个数据,我们至少需要4个指针才能得到完整的数据!那么问题来了,指针的存储需要4或8个字节,却只能指向一个内存空间,那么我们的内存条岂不是有80%的空间都用来当指针,只有20%的空间能够储存真实数据了吗?
定义指针的时候,指定指针所指向的数据类型,其实就是规定指针解引用的访问权限。一个int*类型的指针,解引用可以一次访问4个字节。如果这也不能很好地反应指针的作用,那么请看下面这个代码:
struct stu{
int grade;
char name[20];
char stuId[10];
};
我们在这里定义了一个结构体stu。一个这样的结构体数据占据的内存空间是比较大的。如果我们定义一个指针,指针指向的数据类型是struct stu的话,那么这个指针解引用能一次访问几十个字节。
当然这里还涉及到一个特殊的指针类型void*。这个类型的指针被称为泛指针。因为没有规定指针指向的数据类型,所以不能直接进行解引用操作,但这其实给了泛指针更大的灵活性。
比如在C语言库函数中的qsort函数中,就需要传入一个排序函数。这个排序函数是这样声明的。
compar(const void*p1,const void*p2);
通过传入泛指针,同时在函数内部进行强制类型转化,我们得到了一个具有泛用性的函数模型。
四、指针运算
指针运算是指针特色。首先,指针之间的加法是没有定义的。指针只能对整数进行加减操作,或者进行指针之间的减法。这些运算具有特殊的意义。
比如定义一个数组:
int a[10];
int *p=a;
将数组首元素的地址赋值给p。p+1则是指向a[1],即p+1与&a[1]等价。同样的,p+k与&a[k]等价。如果写出如下代码:
int *p1=&a[3];
int *p2=&a[6];
计算p2-p1的结果就是3,代表了两个指针指向的数据之间相隔多少个数据。
指针甚至可以进行比大小,比较的是地址的数字大小。
注意:只有相同类型的指针才可以进行相减操作!
void*指针无法进行指针相减操作以及整数加减操作!
五、野指针的处理
野指针是对一类访问了未授权内存空间的指针的统称。野指针的存在可能造成程序崩溃并抛出段错误,如果程序继续运行,可能导致数据损坏,并且难以排查bug的发生区域。
1.指针作为一种变量,遵循变量赋值的规律:全局变量和静态变量初始值为0,局部变量为随机值。
如果局部指针未初始化,可能导致野指针。可以通过给局部指针赋初值为0解决。
2.指针越界访问。比如定义了一个大小为5的整形数组,a[5]。将数组首元素的地址赋值给p。如果对p+5进行解引用,就会发生越界访问的问题。
3.指针指向的内存空间被释放。使用free函数可以释放指针指向的内存空间,销毁变量。但是注意,释放完成后请把指针的值设置为0。
4.断言assert。引用头文件<assert.h>。assert本质上是一个宏。assert(!p)可以用来检测p是否为空指针。如果是空指针的话,程序继续运行,否则报错。assert可以用于很多别的领域,括号内为真就继续,否则报错,可以有效避免野指针问题。
六、传值调用和传址调用
函数接受的参数都是形参,是实参的一份临时拷贝,并随着函数的生命周期结束被销毁。函数内部对形参的任意操作都无法影响实参。如果我们要对实参进行操作,可以通过传入地址的方式,在函数内部进行解引用来访问实参。有关内容这里不过多赘述。
七、多级指针
指向指针变量的指针叫做多级指针。通常以二级指针最为常用。读者可自行类比。值得一提的是,一维数组的本质是指针,对于不同下标的数组成员进行访问其实就是指针进行加减整数操作后解引用。二维数组的本质是先用一维数组储存多个指针,一个指针对应一个数组。通过不同指针来访问不同数组。通过不同下标来访问不同数组成员。
附录:各种类型指针的定义方法一览:
int *p;
char* p;//普通数据类型指针定义
void* p;//泛指针定义
int *p[10];//指针数组定义(储存指针的数组)
int (*p)[10];//数组指针定义(指向数组,解引用后访问的内存空间大小为数组大小)
int **p;//普通数据类型二级指针定义
int (*pf)(int,int);//返回类型为int,传入两个int型参数的函数所对应的指针(只需要把原函数名替换成*pf即可)