指针,听起来很高大上吧,毫不夸张的说在我最初见到指针这一词时,我本以为就是学习操作屏幕上的鼠标指针……经过初阶的学习后对当时的天真感到很十分尴尬。指针这东西确实麻烦但不难理解,接下来我用盘子、牛角包、和手的关系来解释指针和指针应该怎么用。
一、地址是什么
首先我们先来了解地址。我们在写程序时,写入的任何一个数值,任何一个变量,任何一个自定义函数都要在内存开辟一块儿空间,为了更好更便捷的访问内存空间,我们把内存中一个字节大小的空间称为内存的最小单元,每一个单元都有一个属于他的编号,这个编号就是这个内存单元的地址。
若编译器选择32位虚拟地址空间(vs2019为例,选择X86的环境),CPU就会为内存单元开辟32bite位的地址序列,如下图是变量a的地址
而这串十六进制数字用二进制表示为:
若编译器选择64位虚拟地址空间,CPU就会为内存单元开辟64bite位的地址序列(这个我不举例了)所以我们发现地址的实质是一串二进制数,那么如何通过这一串二进制数找到相应的内存空间,这就是我接下来要讲的指针的作用。
二、指针是什么
简单地说,指针就是存放地址的变量,通过取地址符&将变量的地址取出,然后将地址赋给指针变量,通过解引用的方式就能找到a变量中所存放的值。我用一个例子示范
#include<stdio.h>
int main()
{
int a = 10;//在内存中开辟四个字节空间大小,用来存放int型的变量a
int *p = &a;//用取地址符&取出变量a的地址放入指针变量*p中
//此时注意,int所占四个字节空间,指针指向的地址是第一个字节的地址
int b = *p;//用解引用符将p指针指向的变量的内容取出,放入变量b中
return 0;
}
三、用牛角包和盘子解释指针
接下来,我分别写出两个功能函数来实现"交换两个变量的数值"这一程序
#include<stdio.h>
void func1(int p1, int p2)
{
int temp = p1;
p2 = p1;
p1 = temp;
}
void func2(int* p1, int* p2)//通过指针来接受传入的两个地址
{
int temp = *p1;
*p1 = *p2;
*p2 = temp;
}
int main()
{
int a = 2, b = 4;
func1(a, b);
printf("a的值为:%d b的值为:%d\n", a, b);//将a,b的值传入func1函数中
func2(&a, &b);
printf("a的值为:%d b的值为:%d\n", a, b);//将a,b的地址传入func2函数中
return 0;
}
运行程序我们发现,将两个数的数值传入,通过函数func1运算后a、b的数值并没有发生调换,而把a,b的地址传入func2中通过指针解引用的方式来交换数值却成功的发生了调换,这是为什么呢?是因为魔法吗?不不,是因为指针,接下来我通过类比的方式帮助大家理解。
我们把程序比作一场晚宴,把变量a、b比作供食客享用的牛角包,每一个牛角包必须存放在盘子里,盘子就相当于内存地址,这时指针就相当于服务生的手,我的手托起装着牛角包a的盘子,就相当于指针真指向了变量a的地址,我的手上就存放着这个盘子,也就相当于指针变量存放了变量a的地址;如果我想得到a、b的值,就要对指针解引用,相当于我要把盘子抽取掉,手上剩下的就是牛角包这个道理。
回到上述的程序,我将变量a、b的值传入func1函数中,通过临时创建的形参p1,p2接收,就好比我模仿着牛角包a、b的形状又做出两个一模一样的牛角包p1、p2,然后将他们存放在新的盘子里,此时服务生func1调换左右手盘子上面包的位置只是交换了牛角包p1和牛角包p2的位置,对牛角包a、b没有影响,所以说值传递根本不能来改变a、b变量本身。
而对于地址传递来说,就相当于我把装有牛角包a、b的盘子交给了服务生func2,他调换了左右手盘子上面包的位置,此时a盘装着牛角包b,b盘装着牛角包a,这就从本质上改变了a、b的值。对于func1函数我们还要认识到,函数形参只是临时变量,当函数运行完后形参自动销毁,甚至这个函数开辟的内存空间也会释放;相当于牛角包p1、p2被嘴馋的服务员func1吃完了,吃完后服务员func1也被辞退了(卖的还没吃得多)。
四、指针的正确用法
了解完了牛角包...哦不是,了解完指针后我们来掌握如何正确使用指针
1.指针的正确定义
在定义指针时需要三个部分,指针名、指针标识符*、指针类型
int a = 10;
int *p = &a;
如上图所示,指针名为p,' * ' 说明p为指针变量,int说明指针类型是整型的
有些同学说,说是*p才是指针,单独的p什么也不是。在这里我可以负责任的说' * '只有在定义指针时用来表明他是个指针变量,在定义完后任意情况里,*p都代表对指针解引用,也就是取得指针所指向地址的值。
2.指针的正确使用
1.指针在使用时一定要初始化,或者指向NULL,否则会指向内存中的随机值
指针的未初始化可能会造成内存的非法访问
2.指针在指向数组时,要注意指针的越界访问
#include<stdio.h>
int main()
{
int arr[4] = { 1,2,3,4 };
int* p = arr;
for (int i = 0; i < 6; i++)
//这里在arr[3]后,指针向后访问非数组成员,在打印时可能打印出随机值
{
printf("%d", arr[i]);
}
return 0;
}
3.在返回值为指针变量的函数中,在主函数中的指针不能接受返回值中的临时变量的地址
因为函数在调用完后,函数体,形参,返回值所占的内存空间都会被释放,当返回值为地址被释放时,刚才调用的函数在返回的这个地址上所赋的值已经被释放,所以这个地址所赋的值现在为随机值。
五.指针类型
很多同学都有这种感觉,就是指针变量和被指向的元素必须是同一类型,类型不同指针不能访问变量,这里我先把结论放出来
1.指针类型决定了,指针在解引用时访问变量内容字节数的权限。
2.在数组中或者其他地方指针做+-整数运算时(其实就是地址指向的移动)时指针类型决定了,指针向前或者向后移动了多少字节数。
具体是什么意思我举例帮助大家理解
#include<stdio.h>
int main()
{
int arr[10] = { 0 };
char* p = (char*)arr;
for (int i = 0; i < 40; i++)
{
*p++ = 0x11;
}
for (int i = 0; i < 10; i++)
{
printf("%#x\n", arr[i]);
}
return 0;
}
如上图代码,我们知道int类型的数据占4个字节的内存空间,所以数组arr[10]所占空间大小为40个字节,将arr数组的类型强转为char*,让一char类型的指针p指向arr数组,char类型的指针每次只能访问一个字节的数据,所以指针p的移动的距离为一字节,p解引用赋值将arr数组每一字节的数值改为0x11
上述代码的输出结果为:
再来看下面一串代码
#include<stdio.h>
int main()
{
char arr1[40] = { 0 };
int* p1 = (int*)arr1;
for (int i = 0; i < 10; i++)
{
*p1++ = 0x11;
}
for (int i = 0; i < 10; i++)
{
printf("%#x\n", arr1[i]);
}
return 0;
}
char类型的变量占1字节的内存空间,arr1数组总共占40个字节的内存空间,用int类型的指针p1指向arr1首元素的地址,p1++就向后访问4个字节空间的内存地址,然后将指向的第一个字节空间的值改变为0x11
运行结果为:
六.指针运算
指针在使用的过程中也存在着加减运算,在这里我介绍两种指针的运算
1.指针+ - 整数,就是指针在指向地址时的移动(移动的字节数取决于指针变量的类型)
2.指针-指针运算((地址-地址)/(类型所占字节数))得到的是指针与指针之间的元素个数,前提是两个指针必须指向同一空间,如下图
#include<stdio.h>
int main()
{
int arr[10] = { 0 };
int* p1 = &arr[0];
int* p2 = &arr[9];
printf("p2与p1之间的元素个数为:%d", p2 - p1);
return 0;
}
运行结果为: