同学们,前面咱们把函数章节全部啃完了。今天开始,咱们要进入C语言里最刺激、最让人又爱又恨的部分——指针。
为啥说它让人又爱又恨?因为指针是C语言的灵魂,你学不会指针,C语言就等于白学。学明白了指针,你才能真正理解什么叫“直接操作内存”,才能在数据结构、操作系统、嵌入式这些领域站住脚。
但很多同学一听到“指针”两个字,心里就咯噔一下——“完了,这玩意儿听说特别难,各种*和&,绕来绕去的,脑子都晕了。”
别怕。今天咱们就把指针彻底拆开,用最朴素的大白话,从“你家住哪儿”这个问题开始讲起。
先想一个问题:变量到底住在哪儿?
咱们前面学了变量。你写int a = 10;,意思是在内存里找了一块地方,把10放进去。
那问题来了:这块地方在哪儿?内存那么大,它总得有个位置吧?
你可以把内存想象成一栋超级大的酒店,里面密密麻麻全是房间,每个房间都有一个独一无二的门牌号。这个门牌号,在计算机里就叫地址。
你定义了一个变量int a = 10;,就相当于酒店开了一个房间,门牌号是比如0x7ffd5e8a3c(这是十六进制的地址,你不用记这个具体的数字,知道它长这样就行),房间里住着一个客人,名字叫10。
所以,变量名是什么?是我们给一个内存地址起的好记的别名。 你写a,计算机就帮你找到那个地址,把里面的东西取出来。就像你说“前台”,酒店的人就知道是哪个房间。
那问题又来了:如果我想直接知道那个门牌号呢?比如我想把“a住在哪个房间”这个信息存下来,怎么办?
这时候,就轮到指针出场了。
指针是什么?
指针其实特别简单,它就是一个专门用来存地址的变量。
你想想:
-
你有一个
int变量,里面存的是整数。 -
你有一个
float变量,里面存的是小数。 -
那你要存地址,就得有一个专门存地址的变量,这就是指针变量。
打个比方你就明白了。你去酒店找朋友,你手里拿着一张纸条,上面写着“张三住1808”。这张纸条本身不是张三,也不是1808这个数字,它只是告诉你“怎么找到张三”。
指针变量就是这张纸条。它存的是“变量住在哪个地址”。
int a = 10; // 定义一个普通变量a,存的是10 int *p = &a; // 定义一个指针变量p,存的是a的地址
这里出现了两个新符号:*和&。别慌,咱们一个一个说。
符号一:& —— “帮我拿一下地址”
&叫取地址运算符。它的作用特别直接:放在一个变量前面,就能得到这个变量在内存里的地址。
int a = 10; printf("%p\n", &a); // 输出a的地址,比如 0x7ffd5e8a3c
%p是专门用来打印地址的格式控制符。输出的那一串0x...就是a住在内存里的门牌号。
你记住:&a读作“a的地址”。
符号二:* —— “帮我进去看看”
*有两种用法,咱们现在说的是第一种——在定义指针变量的时候用它。
int *p; // 定义一个指针变量p,它可以指向一个int类型的变量
这里的*不是在“做乘法”,它是在声明:p不是一个普通变量,它是一个指针,指向的是int类型的数据。
你可能会问:那这个p能指向float吗?不行。你声明的是int *p,它就专门用来指向int。就像你的酒店房卡上写着“标准间”,就不能拿去开总统套房的门。指针的类型必须跟它指向的变量类型一致。
那如果现在我已经有一个指针p,里面存了a的地址,我怎么通过p拿到a的值呢?
这时候就用到了*的第二种用法——解引用运算符。
int a = 10; int *p = &a; // p存了a的地址 printf("%d\n", *p); // 输出10
*p的意思就是:“顺着p里存的地址,找到那个房间,把里面的东西拿出来。” 因为p指向的是a,所以*p拿出来的就是a的值,也就是10。
你还可以通过*p修改a的值:
*p = 20; // 把p指向的那个房间里的东西改成20 printf("%d\n", a); // 输出20,a的值被改了
你看,我们没碰a,但通过指针p直接改了a的值。这就是指针的威力——它能直接操作内存里的数据。
咱们画个图,把这件事彻底搞明白
你写这三行代码:
int a = 10; int *p = &a; printf("%d\n", *p);
内存里发生了什么?咱们画出来:
第一步:int a = 10;
内存里分配了一个房间,门牌号比如是0x1000,里面住着10。
地址: 0x1000 变量: a 值: 10
第二步:int *p = &a;
内存里又分配了一个房间给p,门牌号比如是0x2000。&a取出a的地址0x1000,存进p的房间。
地址: 0x1000 地址: 0x2000 变量: a 变量: p 值: 10 值: 0x1000 ──指向──> 0x1000
第三步:*p
*p的意思是“顺着p里存的地址(0x1000)找过去,把那个房间里的值取出来”。找过去一看,里面是10,所以*p就是10。
你看,指针就这么点事儿。
指针变量的定义格式
咱们把指针变量的定义格式总结一下:
类型 *指针变量名;
比如:
int *p1; // 指向int的指针 float *p2; // 指向float的指针 char *p3; // 指向char的指针 double *p4; // 指向double的指针
你可以写int *p,也可以写int* p,甚至int * p,效果都一样。但我建议你写成int *p——因为*是跟着变量名走的,int *p读起来就是“p是一个指向int的指针”。
注意:指针变量本身也占内存。在64位系统上,指针占8个字节。因为它要存地址,地址就是8个字节的。不管它指向的是int还是char,指针本身的大小都是一样的。
几个常见的初学者误解
误解一:int *p;里的*p是在做乘法?
不是。这行代码只是在声明一个指针变量。这里的*只是告诉你“p是个指针”。只有当你写*p = 20;的时候,*才是解引用。
误解二:定义指针的时候忘了初始化,然后直接*p。
int *p; // p里存的是垃圾地址,不知道指向哪里 *p = 100; // 危险!往一个未知的内存地址写数据,程序可能崩溃
指针定义之后,一定要让它指向一个合法的地址,再通过它去操作数据。
int a; int *p = &a; // 正确做法:定义的时候就让它指向a *p = 100; // 安全
误解三:*p和p搞混。
-
p是指针本身,存的是地址。 -
*p是解引用,得到的是它指向的那个变量的值。
用咱们的酒店比方:
-
p就是那张写着“1808”的纸条。 -
*p就是“拿着纸条去1808房间,看里面是谁”。
一个完整的例子,把今天学的串起来
#include <stdio.h> int main() { int a = 10; // 普通变量 int b = 20; int *p; // 声明一个指针变量 p = &a; // p指向a printf("p指向a,*p = %d\n", *p); // 输出10 p = &b; // p指向b(指针可以重新指向) printf("p指向b,*p = %d\n", *p); // 输出20 *p = 100; // 通过指针修改b的值 printf("修改后,b = %d\n", b); // 输出100 return 0; }
你看到没有?指针p一开始指向a,后来改成指向b了。指针变量可以重新指向别的变量,它不是死的。而且通过*p可以直接改它指向的那个变量的值。
为什么要有指针?
说了这么多,你可能会想:我直接用a改值不就行了吗?为什么还要绕个弯用指针?
好问题。现在你学的还只是最基础的指针概念,还没到真正体现指针价值的时候。但我提前给你透露一下——指针真正的威力,体现在这三个地方:
第一,函数可以通过指针修改外面变量的值。 还记得咱们前面讲参数传递吗?你写个交换函数,直接传变量进去是没用的,但传地址进去就能真的交换。这个“传地址”就是用指针。
第二,动态内存分配必须用指针。 你想在程序运行时动态申请内存,只能通过指针来管理。这个后面会详细讲。
第三,链表、树这些数据结构必须用指针。 没有指针,你根本没法把一个个节点串起来。
所以,今天只是一个开始。指针的基础概念——指针就是地址,指针变量就是存地址的变量,&取地址,*解引用——你把这几个核心点吃透了,后面学起来就不慌了。
最后强调三句话
第一,指针就是地址。 它没你想的那么神秘。
第二,&a是取a的地址,*p是顺着p的地址去取里面的值。 一取一解,配对着用。
第三,定义指针的时候类型要匹配。 指向int的指针写int *,指向float的写float *,别搞混。
好,这节课就到这里。回去之后自己动手,写几个变量,定义指针指向它们,用&和*打印地址和值,观察输出结果。再把指针从一个变量改指向另一个变量,看看会发生什么。指针这东西,看是看不会的,必须自己动手跑代码,跑着跑着就豁然开朗了。下节课咱们讲指针和数组的关系,那是指针里最好玩的部分之一。