我们在前面已经认识了各种各样不同的指针变量,有存放int数据类型地址的指针变量int*,也有存放double类型地址的指针变量double*……虽然在C语言里面并不存在所谓的int*,double*这样的数据类型,但是为了阐述问题和描述原理的方便,我们姑且将它们都称之为" 指针类型 "。
一、指针类型及其意义:
很多初学C语言的小伙伴看到这里会想:这些指针类型其一呢,它不决定大小(在上一篇博客里面我们已经知道:指针变量有多大,和这个指针类型没有任何关系,只取决于当前编译器的环境架构);其二呢,如果你细心的话你会发现其实只要是一个变量的地址,你就可以将它交给一个任意指针类型的指针变量,什么意思:下面这种代码你写出来是没有任何问题的:
既然这样,当初在C语言设计的时候,为什么不直接给个统一的指针类型呢?实际上,虽然指针类型不能决定指针变量的大小,但是在指针的运算场景中它却是一个不容忽视的存在,指针类型可以直接影响我们指针运算的结果:
-
指针运算法则一:指针的解引用
用户去访问一个指针所指向的空间里面的内容的过程叫做解引用。需要用到符号是 “ * ”(称之为指针解引用操作符),它的操作对象是一个指针变量。这显然和前面我们定义一个指针变量时的用到的那个 * ,虽然是同一个符号,却有着截然不同的意义。
同时对于不同类型的指针变量来说,每次解引用操作访问的内存空间大小是不一样的。我们不妨来看一下下面这个例子:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
int a = 0x11111111;
//一个字节:0x11 =1*16^0+1*16^1=17
//两个字节:0x1111 =17+1*16^2+1*16^3=4368
//四个字节:0x11111111 =4368+1*16^4+1*16^5+1*16^6+1*16^7=286331153
char* p1 = &a; //p1的指针类型是char*,解引用一次访问一个字节
short* p2 = &a; //p2的指针类型是short*,解引用一次访问两个字节
int* p3 = &a; //p3的指针类型是int*,解引用一次访问四个字节
double* p4 = &a; //p4的指针类型是double*,解引用一次访问八个字节
printf("p1解引用得到的结果是%d\n", *p1);
printf("p2解引用得到的结果是%d\n", *p2);
printf("p3解引用得到的结果是%d\n", *p3);
printf("p4解引用得到的结果是%d\n", *p4);
return 0;
}
运行结果:
从运行结果我们不难发现对于不同指针类型的指针变量而言,在每次进行解引用操作时它能够访问的内存空间的大小是不同的。
同时在代码中有一个值得我们注意的地方,就是注释里面给出的访问空间的大小实际上是这个指针类型的所能访问的最大值,也就是说实际上访问的空间大小可以小于或者等于该值。就拿代码中的第四个指针变量p4而言,虽然理论上它一次解引用操作可以访问8个字节的内存空间的大小,但奈何此时的指针变量p4里头存放着的是一个int类型变量a的内存地址,它只有4个有效的内存空间,因此对p4进行解引用时也就只访问了4个字节空间。
虽然在访问的空间大小上各有差异,但是我们也不需要着重地去记忆它们——因为我们仔细观察,不难发现一个规律:一个指针变量每次解引用操作访问的空间的大小,基本上取决于它应指向变量的数据类型,亦或是说取决于指针类型。
-
指针运算法则二:指针+/-整数
指针可以加上或者减去一个整数,让指针的所指向空间跳过若干个字节,得到的结果是一个新的内存地址编号。然后你可以用一个新的指针变量将其存储起来亦或是在此基础上进行解引用操作等等。
同时对于不同指针类型的指针变量来说,加上同一个整数,它们各自所跳过的空间大小也是不一样的:对于 char* 指针类型的指针变量而言,每次加一只跳过一个char类型变量的空间大小,也就是一个字节的空间大小;而对于 int* 指针类型的指针变量而言,每次加一则能跳过一个int类型变量的空间大小,也就是四个字节的空间大小。
可以说:指针类型决定了指针加一减一操作时可以跳过多少个字节,即指针走一步到底可以走多远。
而且这种指针的运算场景往往会和数组结合在一起使用,我们一起来看下面这个应用场景:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
//数组名表示数组首元素的地址,因此可以直接赋值给一个指针变量
char* p1 = arr;
short* p2 = arr;
int* p3 = arr;
double* p4 = arr;
printf("%d\n", *arr);
printf("%d\n", *(p1 + 1));
printf("%d\n", *(p2 + 1));
printf("%d\n", *(p3 + 1));
printf("%d\n", *(p4 + 1));
return 0;
}
运行结果:
分析:1的十六进制表示形式是0x00 00 00 01,又因为在Visual Studio这个编译器下,是小端字节序存储(如果不太清楚这一部分内容的小伙伴,可以看博主的《数据在计算机中的存储方式(二)——整数在计算机中的存储进阶篇》,链接地址:CSDN 。里面对于大小端字节序的概念有着详细的介绍)。
所以会将变量a的第一个字节“ 01 ”存储在内存中的低地址处。然后在这个编号地址的后面依次存放着00,00,00;02,00,00,00…如图所示:
因此arr表示数组首元素的地址,直接进行解引用操作会得到1的结果
由于p1指针类型是char* ,所以p1+1只会跳过一个字节的空间,即00的位置,因此会打印0;
p2指针类型是short* ,所以p2+1跳过两个字节的空间,仍然是00的位置,因此会打印0;
p3指针类型是int* ,所以p3+1会跳过四个字节的空间,来到02的位置,因此得到结果2;
p4指针类型是double* ,所以p4+1会跳过八个字节的空间,来到03的位置,因此会打印3。
最后这个问题应该就很简单了:如果我希望p1和p2指针每次都可以跳过一个数组元素的空间,我应该让它们每次都加几呢?答案是分别为+4和+2,如图所示:
那么我们有没有办法让p4指针每次只跳过一个数组元素的空间呢?实际上由于C语言只对指针加整数做了标准,该数组又是int类型,一个元素只有四个字节的大小,因此我们没有办法让p4也实现一次只跳过一个数组元素的空间,它只能是两个两个地跳。
-
指针运算法则三:指针-指针
C语言不允许指针进行加法的运算,但是C语言允许两个指针之间进行减法的运算,前提是两个指针指向同一个数组,对应着内存中同一块空间。那么这其中细节的运算规律是怎么一回事呢?我们不妨一起来完成下面这些个测试:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
//p1和p1plus都是char*类型的指针,其中p1plus是相对较大的指针
char* p1 = arr;
char* p1plus = arr + 3;
//p2和p2plus都是int*类型的指针,其中p2plus是相对较大的指针
int* p2 = arr;
int* p2plus = arr + 3;
//测试一:测试大指针减去小指针 --需要总结出指针减去指针的规律
printf("TestOne's result is:\n");
printf("%d\n", p1plus - p1);
printf("%d\n\n", p2plus - p2);
//测试二:测试小指针减去大指针 --观察运行结果,和测试一进行对比
printf("TestTwo's result is:\n");
printf("%d\n\n", p2 - p2plus);
//测试三:测试两个大小相同但类型不同的指针相减 --观察运行结果
printf("TestThree's result is:\n");
printf("%d\n\n", p2 - p1);
//测试四:测试两个大小不同且类型不同的指针相减 --观察运行结果,并总结规律
printf("TestFour's result is:\n");
printf("%d\n", p1plus - p2);
printf("%d\n\n", p2plus - p1);
return 0;
}
运行结果:
测试规律总结如下:
1. 测试一&测试二:
- 大指针减去小指针是正数,小指针减去大指针是负数;
- 指针p1 - 指针p2 = 数组元素个数 X sizeof (数组元素的数据类型) /sizeof(指针应指向变量的数据类型)——上述表达式是指针 - 指针的运算公式,其中公式中的元素个数表示:p1和p2两个指针之间相隔的数组元素个数。
2. 测试三&测试四:
- 不同的两个指针类型之间也可以进行减法。且如果两个指针相同,则结果为0;
- 对于两个不同指针类型的指针变量进行减法,则以公式中p1的指针类型为准进行计算。
这些规律是不是让我们全部记下来呢?实则不然,在实际学习和工作中,指针 - 指针的应用场景是十分有限的,只能用在指针和数组上面,且只能反映出两个指针之间数组元素的个数,所以并不需要我们掌握如此详细的内容。
对于其中的公式部分,我们往往会作以下简化:
即取特殊情况:“ 指针应指向变量的数据类型 ”和“ 数组元素的数据类型 ”一致,此时两个指针相减结果的绝对值即为两个指针之间数组元素的个数。
其二对于测试三和测试四,大家就当看个乐子吧,因为对于两个不同的指针类型之间做减法,这是C语言标准没有给出的,所以博主也只能保证在Visual Studio 2023上述代码是没有问题的。
二、特殊的指针类型——void*类型的指针:
在指针类型的大家族,还有一个非常非常特殊的成员,那便是 void* 类型。要认识这个指针类型我们先从void给大家说起:
void数据类型在C语言里面叫做空类型。常常出现在函数的返回值部分,表示该函数不返回任何值
因此 void*类型的指针也被叫做“空类型指针”。空类型指针不能作为前面指针运算的运算数——即解引用,指针加减整数,指针减指针。且常常被应用于“指针类型”,“ 函数的返回类型”,“函数的参数”等这些地方。
如果一个函数的参数部分设计成void*的指针类型,那么这个参数可以接受任意数据类型的指针或者说地址。
三、补充——指针运算法则四:指针间的比较运算:
这是最后一个指针间的运算法则,至于为什么放在这里讲述,是因为它和前面的指针运算不同的是,前面的指针运算或多或少都会受到来自指针类型的影响或者是限制(不能使用void*类型作为运算数),但是这种指针间的运算则完全不受来自指针类型的约束。
这种指针间的比较运算,往往要求两个被比较的指针它们所指向的是内存中同一块连续的内存空间,这固然可以是一个多字节变量里面不同的地址编号之间的比较,但是这种指针运算的应用场景你见到最多的还是指针和数组之间。如图所示:
我们发现两个指针变量进行比较,即便两个的指针类型不一致也是可以的。
最后其实关于C语言指针比较这个方面,虽然要求是两个指针指向的是内存中同一块连续的内存空间。但是C语言标准却允许我们的用户用指向数组的指针与指向该数组最后一个元素后的地址的指针进行比较,但是不允许与指向第一个元素前的地址的指针进行比较。C语言标准没规定的东西,那会跑成什么样就完全取决于你的编译器了。
这些标准的测试迫于篇幅和设备条件的约束等原因,博主就不带着大家一起实践了,感兴趣的小伙伴,博主已将上图代码放在下面,喜欢测试什么的小伙伴都可以在这个代码的基础上进行修改和测试哦,咱们下期再见!😘
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
void* p1 = arr;
int* p2 = arr+3;
if (p1 > p2)
{
printf("Yes!");
}
else
{
printf("No!");
}
return 0;
}