说到指针,估计还是有很多小伙伴都还是云里雾里的,有点“知其然,而不知其所以然”。但是,不得不说,学了指针,C语言才能算是入门了。指针是C语言的「精华」,可以说,对对指针的掌握程度,「直接决定」了你C语言的编程能力。
在讲指针之前,我们先来了解下变量在「内存」中是如何存放的。
在程序中定义一个变量,那么在程序编译的过程中,系统会根据你定义变量的类型来分配「相应尺寸」的内存空间。那么如果要使用这个变量,只需要用变量名去访问即可。
通过变量名来访问变量,是一种「相对安全」的方式。因为只有你定义了它,你才能够访问相应的变量。这就是对内存的基本认知。但是,如果光知道这一点的话,其实你还是不知道内存是如何存放变量的,因为底层是如何工作的,你依旧不清楚。
那么如果要继续深究的话,你就需要把变量在内存中真正的样子是什么搞清楚。内存的最小索引单元是1字节
,那么你其实可以把内存比作一个超级大的「字符型数组」。在上一节我们讲过,数组是有下标的,我们是通过数组名和下标来访问数组中的元素。那么内存也是一样,只不过我们给它起了个新名字:地址
。每个地址可以存放「1字节」的数据,所以如果我们需要定义一个整型变量,就需要占据4个内存单元。
那么,看到这里你可能就明白了:其实在程序运行的过程中,完全不需要变量名的参与。变量名只是方便我们进行代码的编写和阅读,只有程序员和编译器知道这个东西的存在。而编译器还知道具体的变量名对应的「内存地址」,这个是我们不知道的,因此编译器就像一个桥梁。当读取某一个变量的时候,编译器就会找到变量名所对应的地址,读取对应的值。
第一:初识指针和指针变量
那么我们现在就来切入正题,指针是个什么东西呢?
所谓指针,就是内存地址(下文简称地址)。C语言中设立了专门的「指针变量」来存储指针,和「普通变量」不一样的是,指针变量存储的是「地址」。
1、定义指针
指针变量也有类型,实际上取决于地址指向的值的类型。那么如何定义指针变量呢:
很简单:类型名* 指针变量名
char* pa;//定义一个字符变量的指针,名称为pa
int* pb;//定义一个整型变量的指针,名称为pb
float* pc;//定义一个浮点型变量的指针,名称为pc
注意,指针变量一定要和指向的变量的类型一样,不然类型不同可能在内存中所占的位置不同,如果定义错了就可能导致出错。
2、取地址运算符和取值运算符
获取某个变量的地址,使用取地址运算符&
,如:
char* pa = &a;
int* pb = &f;
如果反过来,你要访问指针变量指向的数据,那么你就要使用取值运算符*
,如:
printf("%c, %d\n", *pa, *pb);
这里你可能发现,定义指针的时候也使用了*
,这里属于符号的「重用」,也就是说这种符号在不同的地方就有不同的用意:在定义的时候表示「定义一个指针变量」,在其他的时候则用来「获取指针变量指向的变量的值」。
直接通过变量名来访问变量的值称之为直接访问
,通过指针这样的形式访问称之为间接访问
,因此取值运算符有时候也成为「间接运算符」。
比如:
//Example 01 //代码来源于网络,非个人原创 #include int main(void) { char a = 'f'; int f = 123; 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("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); return 0; }
程序实现如下:
//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
3、避免访问未初始化的指针
void f() { int* a; *a = 10; }
像这样的代码是十分危险的。因为指针a到底指向哪里,我们不知道。就和访问未初始化的普通变量一样,会返回一个「随机值」。
但是如果是在指针里面,那么就有可能覆盖到「其他的内存区域」,甚至可能是系统正在使用的「关键区域」,十分危险。不过这种情况,系统一般会驳回程序的运行,此时程序会被「中止」并「报错」。
要是万一中奖的话,覆盖到一个合法的地址,那么接下来的赋值就会导致一些有用的数据被「莫名其妙地修改」,这样的bug是十分不好排查的,因此使用指针的时候一定要注意初始化。
第二:指针和数组
有些读者可能会有些奇怪,指针和数组又有什么关系?这俩货明明八竿子打不着井水不犯河水。别着急,接着往下看,你的观点有可能会改变。
1、数组的地址
我们刚刚说了,指针实际上就是变量在「内存中的地址」,那么如果有细心的小伙伴就可能会想到,像数组这样的一大摞变量的集合,它的地址是啥呢?
我们知道,从标准输入流中读取一个值到变量中,用的是scanf
函数,一般貌似在后面都要加上&
,这个其实就是我们刚刚说的「取地址运算符」。如果你存储的位置是指针变量的话,那就不需要。
//Example 02 int main(void) { int a; int* p = &a; printf("请输入一个整数:"); scanf("%d", &a);//此处需要& printf("a = %d\n", a); printf("请再输入一个整数:"); scanf("%d", p);//此处不需要& printf("a = %d\n", a); return 0; }
程序运行如下:
//Consequence 02 请输入一个整数:1 a = 1 请再输入一个整数:2 a = 2
在普通变量读取的时候,程序需要知道这个变量在内存中的地址,因此需要&来取地址完成这个任务。而对于指针变量来说,本身就是「另外一个」普通变量的「地址信息」,因此直接给出指针的值就可以了。
试想一下,我们在使用scanf函数的时候,是不是也有不需要使用&的时候?就是在读取「字符串」的时候:
//Example 03 #include int main(void) { char url[100]; url[99] = '\0'; printf("请输入TechZone的域名:"); scanf("%s", url);//此处也不用& printf("你输入的域名是:%s\n", url); return 0; }
程序执行如下:
//Consequence 03 请输入TechZone的域名:www.techzone.ltd 你输入的域名是:www.techzone.ltd
因此很好推理:数组名其实就是一个「地址信息」,实际上就是数组「第一个元素的地址」。
咱们试试把第一个元素的地址和数组的地址做个对比就知道了:
//Example 03 V2 #include int main(void) { char url[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("两者不一致!"); } return 0; }
程序运行结果为:
//Comsequense 03 V2 请输入TechZone的域名:www.techzone.ltd 你输入的域名是:www.techzone.ltd url的地址为:0063F804 url[0]的地址为:0063F804 两者一致!
这么看,应该是实锤了。那么数组后面的元素也就是依次往后放置,有兴趣的也可以自己写代码尝试把它们输出看看。
2、指向数组的指针
刚刚我们验证了数组的地址就是数组第一个元素的地址。那么指向数组的指针自然也就有两种定义的方法:
... char* p; //方法1 p = a; //方法2 p = &a[0];
3、指针的运算
当指针指向数组元素的时候,可以对指针变量进行「加减」运算,+n
表示指向p指针所指向的元素的「下n个元素」,-n
表示指向p指针所指向的元素的「上n个元素」。并不是将地址加1。
如:
//Example 04 #include int main(void) { int a[] = { 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); return 0; }
执行结果如下:
//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 -> 00AFF840 b = 1, b+1 = 2, b+2 = 3
现在你是不是感觉,数组和指针有点像了呢?不过笔者先提醒,数组和指针虽然非常像,但是绝对「不是」一种东西。
甚至你还可以直接用指针来定义字符串,然后用下标法来读取每一个元素:
//Example 05 //代码来源于网络 #include #include int main(void) { char* str = "I love TechZone!"; int i, length; length = strlen(str); for (i = 0; i < length, i++) { printf("%c", str[i]); } printf("\n"); return 0; }
程序运行如下:
//Consequence 05 I love TechZone!
在刚刚的代码里面,我们定义了一个「字符指针」变量,并且初始化成指向一个字符串。后来的操作,不仅在它身上可以使用「字符串处理函数」,还可以用「下标法」访问字符串中的每一个字符。
当然,循环部分这样写也是没毛病的:
... for (i = 0, i < length, i++) { printf("%c", *(str + i)); }
这就相当于利用了指针法来读取。
4、指针和数组的区别
刚刚说了许多指针和数组相互替换的例子,可能有的小伙伴又开始说:“这俩货不就是一个东西吗?”
随着你对指针和数组越来越了解,你会发现,C语言的创始人不会这么无聊去创建两种一样的东西,还叫上不同的名字。指针和数组终究是「不一样」的。
比如笔者之前看过的一个例子:
//Example 06 //代码来源于网络 #include int main(void) { char str[] = "I love TechZone!"; int count = 0; while (*str++ != '\0') { count++; } printf("总共有%d个字符。\n", count); return 0; }
当编译器报错的时候,你可能会开始怀疑你学了假的C语言语法:
//Error in Example 06 错误(活动) E0137 表达式必须是可修改的左值 错误 C2105 “++”需要左值
我们知道,*str++ != ‘\0’
是一个复合表达式,那么就要遵循「运算符优先级」来看。具体可以回顾《C语言运算符优先级及ASCII对照表》。
str++
比*str
的优先级「更高」,但是自增运算符要在「下一条语句」的时候才能生效。所以这个语句的理解就是,先取出str
所指向的值,判断是否为\0
,若是,则跳出循环,然后str
指向下一个字符的位置。
看上去貌似没啥毛病,但是,看看编译器告诉我们的东西:表达式必须是可修改的左值
++
的操作对象是str
,那么str
到底是不是「左值」呢?
如果是左值的话,那么就必须满足左值的条件。
❝❞
- 拥有用于识别和定位一个存储位置的标识符
- 存储值可修改
第一点,数组名str
是可以满足的,因为数组名实际上就是定位数组第一个元素的位置。但是第二点就不满足了,数组名实际上是一个地址,地址是「不可以」修改的,它是一个常量。如果非要利用上面的思路来实现的话,可以将代码改成这样:
//Example 06 V2 //代码来源于网络 #include int main(void) { char str[] = "I love TechZone!"; char* target = str; int count = 0; while (*target++ != '\0') { count++; } printf("总共有%d个字符。\n", count); return 0; }
这样就可以正常执行了:
//Consequence 06 V2 总共有16个字符。
这样我们就可以得出:数组名只是一个「地址」,而指针是一个「左值」。
详解C语言指针的使用方法(下)https://developer.aliyun.com/article/1389160