C语言之指针

说到指针,估计还是有很多小伙伴都还是云里雾里的,有点“知其然,而不知其所以然”。但是,不得不说,学了指针,C语言才能算是入门了。指针是C语言的精华,可以说,对对指针的掌握程度,直接决定了你C语言的编程能力。

C语言之指针

在讲指针之前,我们先来了解下变量在内存中是如何存放的。

在程序中定义一个变量,那么在程序编译的过程中,系统会根据你定义变量的类型来分配相应尺寸的内存空间。那么如果要使用这个变量,只需要用变量名去访问即可。

通过变量名来访问变量,是一种相对安全的方式。因为只有你定义了它,你才能够访问相应的变量。这就是对内存的基本认知。但是,如果光知道这一点的话,其实你还是不知道内存是如何存放变量的,因为底层是如何工作的,你依旧不清楚。

那么如果要继续深究的话,你就需要把变量在内存中真正的样子是什么搞清楚。内存的最小索引单元是1字节,那么你其实可以把内存比作一个超级大的字符型数组。在上一节我们讲过,数组是有下标的,我们是通过数组名和下标来访问数组中的元素。那么内存也是一样,只不过我们给它起了个新名字:地址。每个地址可以存放1字节的数据,所以如果我们需要定义一个整型变量,就需要占据4个内存单元。

那么,看到这里你可能就明白了:其实在程序运行的过程中,完全不需要变量名的参与。变量名只是方便我们进行代码的编写和阅读,只有程序员和编译器知道这个东西的存在。而编译器还知道具体的变量名对应的内存地址,这个是我们不知道的,因此编译器就像一个桥梁。当读取某一个变量的时候,编译器就会找到变量名所对应的地址,读取对应的值。

初识指针和指针变量
那么我们现在就来切入正题,指针是个什么东西呢?

所谓指针,就是内存地址(下文简称地址)。C语言中设立了专门的指针变量来存储指针,和普通变量不一样的是,指针变量存储的是地址。

定义指针
指针变量也有类型,实际上取决于地址指向的值的类型。那么如何定义指针变量呢:

很简单:类型名* 指针变量名

charpa;//定义一个字符变量的指针,名称为paintpb;//定义一个整型变量的指针,名称为pbfloat*pc;//定义一个浮点型变量的指针,名称为pc

C

注意,指针变量一定要和指向的变量的类型一样,不然类型不同可能在内存中所占的位置不同,如果定义错了就可能导致出错。

取地址运算符和取值运算符

获取某个变量的地址,使用取地址运算符&,如:

charpa=&a;intpb=&f;

如果反过来,你要访问指针变量指向的数据,那么你就要使用取值运算符*,如:

printf("%c, %d\n",*pa,*pb);

这里你可能发现,定义指针的时候也使用了*,这里属于符号的重用,也就是说这种符号在不同的地方就有不同的用意:在定义的时候表示定义一个指针变量,在其他的时候则用来获取指针变量指向的变量的值。

直接通过变量名来访问变量的值称之为直接访问,通过指针这样的形式访问称之为间接访问,因此取值运算符有时候也成为间接运算符。

比如:

//Example
01//代码来源于网络,非个人原创#include<stdio.h>intmain(void){chara=‘f’;intf=123;charpa=&a;intpf=&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(“sizeof pa =
%d\n”,sizeof(pa));printf(“sizeof pf = %d\n”,sizeof(pf));printf(“the
addr of a is: %p\n”,pa);printf(“the addr of f is: %p\n”,pf);return0;}

程序实现如下:

//Consequence 01

a = f

f = 123

now, a = c

now, f = 124

sizeof pa = 4

sizeof pf = 4

the addr of a is: 00EFF97F

the addr of f is: 00EFF970

避免访问未初始化的指针

voidf(){int*a;*a=10;}

像这样的代码是十分危险的。因为指针a到底指向哪里,我们不知道。就和访问未初始化的普通变量一样,会返回一个随机值。但是如果是在指针里面,那么就有可能覆盖到其他的内存区域,甚至可能是系统正在使用的关键区域,十分危险。不过这种情况,系统一般会驳回程序的运行,此时程序会被终止并报错。要是万一中奖的话,覆盖到一个合法的地址,那么接下来的赋值就会导致一些有用的数据被莫名其妙地修改,这样的bug是十分不好排查的,因此使用指针的时候一定要注意初始化。

C语言之指针

指针和数组
有些读者可能会有些奇怪,指针和数组又有什么关系?这俩货明明八竿子打不着井水不犯河水。别着急,接着往下看,你的观点有可能会改变。

数组的地址

我们刚刚说了,指针实际上就是变量在内存中的地址,那么如果有细心的小伙伴就可能会想到,像数组这样的一大摞变量的集合,它的地址是啥呢?

我们知道,从标准输入流中读取一个值到变量中,用的是scanf函数,一般貌似在后面都要加上&,这个其实就是我们刚刚说的取地址运算符。如果你存储的位置是指针变量的话,那就不需要。

//Example
02intmain(void){inta;int*p=&a;printf(“请输入一个整数:”);scanf("%d",&a);//此处需要&printf(“a
= %d\n”,a);printf(“请再输入一个整数:”);scanf("%d",p);//此处不需要&printf(“a = %d\n”,a);return0;}

程序运行如下:

//Consequence 02

请输入一个整数:1

a = 1

请再输入一个整数:2

a = 2

在普通变量读取的时候,程序需要知道这个变量在内存中的地址,因此需要&来取地址完成这个任务。而对于指针变量来说,本身就是另外一个普通变量的地址信息,因此直接给出指针的值就可以了。

试想一下,我们在使用scanf函数的时候,是不是也有不需要使用&的时候?就是在读取字符串的时候:

//Example
03#include<stdio.h>intmain(void){charurl[100];url[99]=’\0’;printf(“请输入TechZone的域名:”);scanf("%s",url);//此处也不用&printf(“你输入的域名是:%s\n”,url);return0;}

程序执行如下:

//Consequence 03

请输入TechZone的域名:www.techzone.ltd

你输入的域名是:www.techzone.ltd

因此很好推理:数组名其实就是一个地址信息,实际上就是数组第一个元素的地址。咱们试试把第一个元素的地址和数组的地址做个对比就知道了:

//Example 03
V2#include<stdio.h>intmain(void){charurl[100];printf(“请输入TechZone的域名:”);url[99]=’\0’;scanf("%s",url);printf(“你输入的域名是:%s\n”,url);printf(“url的地址为:%p\n”,url);printf(“url[0]的地址为:%p\n”,&url[0]);if(url==&url[0]){printf(“两者一致!”);}else{printf(“两者不一致!”);}return0;}

程序运行结果为:

//Comsequense 03 V2

请输入TechZone的域名:www.techzone.ltd

你输入的域名是:www.techzone.ltd

url的地址为:0063F804

url[0]的地址为:0063F804

两者一致!

这么看,应该是实锤了。那么数组后面的元素也就是依次往后放置,有兴趣的也可以自己写代码尝试把它们输出看看。

C语言之指针

指向数组的指针
刚刚我们验证了数组的地址就是数组第一个元素的地址。那么指向数组的指针自然也就有两种定义的方法:

…char*p;//方法1p=a;//方法2p=&a[0];

指针的运算

当指针指向数组元素的时候,可以对指针变量进行加减运算,+n表示指向p指针所指向的元素的下n个元素,-n表示指向p指针所指向的元素的上n个元素。并不是将地址加1。

如:

//Example
04#include<stdio.h>intmain(void){inta[]={1,2,3,4,5};int*p=a;printf("*p
= %d, *(p+1) = %d, (p+2) = %d\n",p,(p+1),(p+2));printf("*p -> %p, *(p+1) -> %p, *(p+2) -> %p\n",p,p+1,p+2);return0;}

执行结果如下:

//Consequence 04

*p = 1, *(p+1) = 2, *(p+2) = 3

*p -> 00AFF838, *(p+1) -> 00AFF83C, *(p+2) -> 00AFF840

有的小伙伴可能会想,编译器是怎么知道访问下一个元素而不是地址直接加1呢?

其实就在我们定义指针变量的时候,就已经告诉编译器了。如果我们定义的是整型数组的指针,那么指针加1,实际上就是加上一个sizeof(int)的距离。相对于标准的下标访问,使用指针来间接访问数组元素的方法叫做指针法。

其实使用指针法来访问数组的元素,不一定需要定义一个指向数组的单独的指针变量,因为数组名自身就是指向数组第一个元素的指针,因此指针法可以直接作用于数组名:

…printf(“p -> %p, p+1 -> %p, p+2 -> %p\n”,a,a+1,a+2);printf(“a = %d,
a+1 = %d, a+2 = %d”,a,(a+1),*(a+2));…

执行结果如下:

p->00AFF838,p+1->00AFF83C,p+2->00AFF840b=1,b+1=2,b+2=3

现在你是不是感觉,数组和指针有点像了呢?不过笔者先提醒,数组和指针虽然非常像,但是绝对不是一种东西。

甚至你还可以直接用指针来定义字符串,然后用下标法来读取每一个元素:

//Example
05//代码来源于网络#include<stdio.h>#include<string.h>intmain(void){char*str=“I
love
TechZone!”;inti,length;length=strlen(str);for(i=0;i<length,i++){printf("%c",str[i]);}printf("\n");return0;}

程序运行如下:

//Consequence 05

I love TechZone!

在刚刚的代码里面,我们定义了一个字符指针变量,并且初始化成指向一个字符串。后来的操作,不仅在它身上可以使用字符串处理函数,还可以用下标法访问字符串中的每一个字符。

当然,循环部分这样写也是没毛病的:

…for(i=0,i<length,i++){printf("%c",*(str+i));}

这就相当于利用了指针法来读取。

指针和数组的区别
刚刚说了许多指针和数组相互替换的例子,可能有的小伙伴又开始说:“这俩货不就是一个东西吗?”

随着你对指针和数组越来越了解,你会发现,C语言的创始人不会这么无聊去创建两种一样的东西,还叫上不同的名字。指针和数组终究是不一样的。

比如笔者之前看过的一个例子:

//Example 06//代码来源于网络#include<stdio.h>intmain(void){charstr[]=“I love
TechZone!”;intcount=0;while(*str++!=’\0’){count++;}printf(“总共有%d个字符。\n”,count);return0;}

当编译器报错的时候,你可能会开始怀疑你学了假的C语言语法:

//Error in Example 06

错误(活动) E0137 表达式必须是可修改的左值

错误 C2105 “++”需要左值

str++比*str的优先级更高,但是自增运算符要在下一条语句的时候才能生效。所以这个语句的理解就是,先取出str所指向的值,判断是否为\0,若是,则跳出循环,然后str指向下一个字符的位置。

看上去貌似没啥毛病,但是,看看编译器告诉我们的东西:表达式必须是可修改的左值

++的操作对象是str,那么str到底是不是左值呢?

如果是左值的话,那么就必须满足左值的条件。

拥有用于识别和定位一个存储位置的标识符

存储值可修改

第一点,数组名str是可以满足的,因为数组名实际上就是定位数组第一个元素的位置。但是第二点就不满足了,数组名实际上是一个地址,地址是不可以修改的,它是一个常量。如果非要利用上面的思路来实现的话,可以将代码改成这样:

//Example 06 V2//代码来源于网络#include<stdio.h>intmain(void){charstr[]=“I
love
TechZone!”;char*target=str;intcount=0;while(*target++!=’\0’){count++;}printf(“总共有%d个字符。\n”,count);return0;}

这样就可以正常执行了:

//Consequence 06 V2

总共有16个字符。

这样我们就可以得出:数组名只是一个地址,而指针是一个左值。

指针数组?数组指针?

看下面的例子,你能分辨出哪个是指针数组,哪个是数组指针吗?

int*p1[5];int(*p2)[5];

单个的我们都可以判断,但是组合起来就有些难度了。

答案:

int*p1[5];//指针数组int(*p2)[5];//数组指针

我们挨个来分析。

指针数组
数组下标[]的优先级是最高的,因此p1是一个有5个元素的数组。那么这个数组的类型是什么呢?答案就是int*,是指向整型变量的指针。因此这是一个指针数组。

那么这样的数组应该怎么样去初始化呢?

你可以定义5个变量,然后挨个取地址来初始化。

不过这样太繁琐了,但是,并不是说指针数组就没什么用。

比如:

//Example
07#include<stdio.h>intmain(void){char*p1[5]={“人生苦短,我用Python。”,“PHP是世界上最好的语言!”,“One
more
thing…”,“一个好的程序员应该是那种过单行线都要往两边看的人。”,“C语言很容易让你犯错误;C++看起来好一些,但当你用它时,你会发现会死的更惨。”};inti;for(i=0;i<5;i++){printf("%s\n",p1[i]);}return0;}

结果如下:

//Consequence 07

人生苦短,我用Python。

PHP是世界上最好的语言!

One more thing…

一个好的程序员应该是那种过单行线都要往两边看的人。

C语言很容易让你犯错误;C++看起来好一些,但当你用它时,你会发现会死的更惨。

这样是不是比二维数组来的更加直接更加通俗呢?

数组指针
()和[]在优先级里面属于同级,那么就按照先后顺序进行。

int(*p2)将p2定义为指针, 后面跟随着一个5个元素的数组,p2就指向这个数组。因此,数组指针是一个指针,它指向的是一个数组。

但是,如果想对数组指针初始化的时候,千万要小心,比如:

//Example
08#include<stdio.h>intmain(void){int(p2)[5]={1,2,3,4,5};inti;for(i=0;i<5;i++){printf("%d\n",(p2+i));}return0;}

Visual Studio 2019报出以下的错误:

//Error and Warning in Example 08

错误(活动) E0146 初始值设定项值太多

错误 C2440 “初始化”: 无法从“initializer list”转换为“int (*)[5]”

警告 C4477 “printf”: 格式字符串“%d”需要类型“int”的参数,但可变参数 1 拥有了类型“int *”

这其实是一个非常典型的错误使用指针的案例,编译器提示说这里有一个整数赋值给指针变量的问题,因为p2归根结底还是指针,所以应该给它传递一个地址才行,更改一下:

//Example 08
V2#include<stdio.h>intmain(void){inttemp[5]={1,2,3,4,5};int(p2)[5]=temp;inti;for(i=0;i<5;i++){printf("%d\n",(p2+i));}return0;}

//Error and Warning in Example 08 V2

错误(活动) E0144 “int " 类型的值不能用于初始化 "int ()[5]” 类型的实体

错误 C2440 “初始化”: 无法从“int [5]”转换为“int (*)[5]”

警告 C4477 “printf”: 格式字符串“%d”需要类型“int”的参数,但可变参数 1 拥有了类型“int *”

可是怎么还是有问题呢?

我们回顾一下,指针是如何指向数组的。

inttemp[5]={1,2,3,4,5};int*p=temp;

我们原本以为,指针p是指向数组的指针,但是实际上并不是。仔细想想就会发现,这个指针实际上是指向的数组的第一个元素,而不是指向数组。因为数组里面的元素在内存中都是挨着个儿存放的,因此只需要知道第一个元素的地址,就可以访问到后面的所有元素。

但是,这么来看的话,指针p指向的就是一个整型变量的指针,并不是指向数组的指针。而刚刚我们用的数组指针,才是指向数组的指针。因此,应该将数组的地址传递给数组指针,而不是将第一个元素的地址传入,尽管它们值相同,但是含义确实不一样:

//Example 08 V3//Example 08
V2#include<stdio.h>intmain(void){inttemp[5]={1,2,3,4,5};int(p2)[5]=&temp;//此处取地址inti;for(i=0;i<5;i++){printf("%d\n",(*p2+i));}return0;}

程序运行如下:

//Consequence 08

1

2

3

4

5

C语言之指针