说到指针,估计还是有很多小伙伴都还是云里雾里的,有点“知其然,而不知其所以然”。但是,不得不说,学了指针,C语言才能算是入门了。指针是C语言的「精华」,可以说,对对指针的掌握程度,「直接决定」了你C语言的编程能力。
在讲指针之前,我们先来了解下变量在「内存」中是如何存放的。
在程序中定义一个变量,那么在程序编译的过程中,系统会根据你定义变量的类型来分配「相应尺寸」的内存空间。那么如果要使用这个变量,只需要用变量名去访问即可。
通过变量名来访问变量,是一种「相对安全」的方式。因为只有你定义了它,你才能够访问相应的变量。这就是对内存的基本认知。但是,如果光知道这一点的话,其实你还是不知道内存是如何存放变量的,因为底层是如何工作的,你依旧不清楚。
那么如果要继续深究的话,你就需要把变量在内存中真正的样子是什么搞清楚。内存的最小索引单元是1字节,那么你其实可以把内存比作一个超级大的「字符型数组」。在上一节我们讲过,数组是有下标的,我们是通过数组名和下标来访问数组中的元素。那么内存也是一样,只不过我们给它起了个新名字:地址。每个地址可以存放「1字节」的数据,所以如果我们需要定义一个整型变量,就需要占据4个内存单元。
那么,看到这里你可能就明白了:其实在程序运行的过程中,完全不需要变量名的参与。变量名只是方便我们进行代码的编写和阅读,只有程序员和编译器知道这个东西的存在。而编译器还知道具体的变量名对应的「内存地址」,这个是我们不知道的,因此编译器就像一个桥梁。当读取某一个变量的时候,编译器就会找到变量名所对应的地址,读取对应的值。
初识指针和指针变量那么我们现在就来切入正题,指针是个什么东西呢?
所谓指针,就是内存地址(下文简称地址)。C语言中设立了专门的「指针变量」来存储指针,和「普通变量」不一样的是,指针变量存储的是「地址」。
定义指针指针变量也有类型,实际上取决于地址指向的值的类型。那么如何定义指针变量呢:
很简单:类型名*指针变量名
char*pa;//定义一个字符变量的指针,名称为paint*pb;//定义一个整型变量的指针,名称为pbfloat*pc;//定义一个浮点型变量的指针,名称为pc
注意,指针变量一定要和指向的变量的类型一样,不然类型不同可能在内存中所占的位置不同,如果定义错了就可能导致出错。
取地址运算符和取值运算符获取某个变量的地址,使用取地址运算符,如:
char*pa=a;int*pb=f;
如果反过来,你要访问指针变量指向的数据,那么你就要使用取值运算符*,如:
printf("%c,%d\n",*pa,*pb);
这里你可能发现,定义指针的时候也使用了*,这里属于符号的「重用」,也就是说这种符号在不同的地方就有不同的用意:在定义的时候表示「定义一个指针变量」,在其他的时候则用来「获取指针变量指向的变量的值」。
直接通过变量名来访问变量的值称之为直接访问,通过指针这样的形式访问称之为间接访问,因此取值运算符有时候也成为「间接运算符」。
比如:
//Example01//代码来源于网络,非个人原创#includestdio.hintmain(void){chara=f;intf=;char*pa=a;int*pf=f;printf("a=%c\n",*pa);printf("f=%d\n",*pf);*pa=c;*pf+=1;printf("now,a=%c\n",*pa);printf("now,f=%d\n",*pf);printf("sizeofpa=%d\n",sizeof(pa));printf("sizeofpf=%d\n",sizeof(pf));printf("theaddrofais:%p\n",pa);printf("theaddroffis:%p\n",pf);return0;}
程序实现如下:
//Consequence01a=ff=now,a=cnow,f=sizeofpa=4sizeofpf=4theaddrofais:00EFF97Ftheaddroffis:00EFF避免访问未初始化的指针
voidf(){int*a;*a=10;}
像这样的代码是十分危险的。因为指针a到底指向哪里,我们不知道。就和访问未初始化的普通变量一样,会返回一个「随机值」。但是如果是在指针里面,那么就有可能覆盖到「其他的内存区域」,甚至可能是系统正在使用的「关键区域」,十分危险。不过这种情况,系统一般会驳回程序的运行,此时程序会被「中止」并「报错」。要是万一中奖的话,覆盖到一个合法的地址,那么接下来的赋值就会导致一些有用的数据被「莫名其妙地修改」,这样的bug是十分不好排查的,因此使用指针的时候一定要注意初始化。
指针和数组有些读者可能会有些奇怪,指针和数组又有什么关系?这俩货明明八竿子打不着井水不犯河水。别着急,接着往下看,你的观点有可能会改变。
数组的地址我们刚刚说了,指针实际上就是变量在「内存中的地址」,那么如果有细心的小伙伴就可能会想到,像数组这样的一大摞变量的集合,它的地址是啥呢?
我们知道,从标准输入流中读取一个值到变量中,用的是scanf函数,一般貌似在后面都要加上,这个其实就是我们刚刚说的「取地址运算符」。如果你存储的位置是指针变量的话,那就不需要。
//Example02intmain(void){inta;int*p=a;printf("请输入一个整数:");scanf("%d",a);//此处需要printf("a=%d\n",a);printf("请再输入一个整数:");scanf("%d",p);//此处不需要printf("a=%d\n",a);return0;}
程序运行如下:
//Consequence02请输入一个整数:1a=1请再输入一个整数:2a=2
在普通变量读取的时候,程序需要知道这个变量在内存中的地址,因此需要来取地址完成这个任务。而对于指针变量来说,本身就是「另外一个」普通变量的「地址信息」,因此直接给出指针的值就可以了。
试想一下,我们在使用scanf函数的时候,是不是也有不需要使用的时候?就是在读取「字符串」的时候:
//Example03#includestdio.hintmain(void){charurl[];url[99]=\0;printf("请输入TechZone的域名:");scanf("%s",url);//此处也不用printf("你输入的域名是:%s\n",url);return0;}
程序执行如下:
//Consequence03请输入TechZone的域名: