ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

C语言指针入门:定义、指针变量、解引用与基础运算全解析

C语言指针入门:定义、指针变量、解引用与基础运算全解析 说真的每次看到新手在群里发指针到底是个啥、为什么我写成*p100会崩我就忍不住想起自己当年被指针折磨到怀疑人生的样子。指针这东西几乎是C语言学习路上公认的劝退之王但你要真把它搞明白了后面学数组、链表、函数指针、甚至C的智能指针那都是一马平川。今天这篇就把指针最基础的四板斧——定义、指针变量、解引用、基础运算掰开揉碎给你讲透。这篇笔记适合刚接触C语言的纯新手也适合学了两三个月还是一看到*和就头大的朋友它不整那些花里胡哨的骚操作专门帮你把地基打扎实看完你至少能搞懂为什么int *p a;之后*p就等于a了。1. 指针到底是什么从门牌号说起1.1 内存和变量的本质想理解指针先得理解程序运行时内存是长什么样的。你可以把计算机的内存想象成一个超级大的酒店走廊走廊两侧分布着无数个房间每个房间都有一个独一无二的编号比如101、102、103。当你写下一行代码int a 10;实际上就是你在酒店前台登记申请了一个房间然后往这个房间里放了一个货物数字10。房间的编号比如说是403这个403就是变量a在内存中的地址。而房间里的货物10就是变量a的值。以前你在代码里写a而且不会去想呀这个a到底在哪个房间因为编译器早替你安排好了。但如果你想让程序在运行时能灵活地找到这个房间你就得把房间的编号记下来。谁帮你记呢就是咱们今天的主角——指针。1.2 指针与指针变量的严格区分这里有个特别容易混淆的点也是很多教材害人不浅的地方指针和指针变量。严格来说指针是一个地址它像一个指向关系的描述。比如我指着你说那个人在哪这个哪就是一个指针。而int *p;这行代码声明出来的p它的学名叫做指针变量它是一个实实在在的变量里面存的是那个哪也就是说p这个变量里装着一个地址。很多教材喜欢混着说p是指针这导致我在大一的时候一度觉得指针是一种会飞的神秘符号。你只要记住指针变量是存地址的变量就像你用一个抽屉变量p来放写着房间号的便签纸地址。抽屉是容器便签纸是内容房间号是指针。1.3 为什么非要搞个指针出来可能你会在心里嘀咕我直接用变量a不行吗为什么非要弄个便签纸转来转去核心原因有三个跨函数修改数据在C语言里函数参数是值传递。你往函数里传一个int a其实只是把房间里的货物复制了一份给对方你在函数里怎么折腾原件都不受影响。但如果你把房间号地址传给对方对方就能拿着钥匙直接进你的房间改东西。这是指针最重要的应用场景。动态内存管理程序运行时需要多少数据是未知的你需要用malloc向系统申请房间。系统不会给你变量名只给你一个地址你必须用指针来接收这个地址否则申请的内存就找不到了直接内存泄漏。高效处理大数据如果你要传一个包含1万个元素的数组如果复制整个数组时间开销和内存开销都巨大。传首地址进去只传一张便签纸成本低得可怜。所以我们学指针不是为了显得自己牛而是因为它是C语言操作内存的唯一钥匙。2. 指针变量怎么声明和初始化2.1 声明格式的底层逻辑来看这句最基础的声明int *p;这行代码怎么读从左往右拆解int表示我指向的目标是一个int类型的变量*表示这是一个指针变量p是变量名。连起来的意思就是声明一个名叫p的变量它可以存放一个指向int类型数据的地址。在这里要特别强调一个常见误区*是跟在p前面的所以p的类型其实是int*指向int的指针而不是int。有人喜欢写成int* p;有人喜欢写成int *p;这两种写法在编译器看来完全等价。但我个人强烈建议新手写成int *p;。原因很简单如果你写成int* p, q;那么只有p是指针而q是普通的int变量。而写成int *p, q;语义明显清晰得多——p是带星号的q是好汉。别小看这个风格它能帮你少踩一个声明多个变量的坑。2.2 初始化是铁律不是可选项声明了指针变量紧接着做一件事初始化。int *p NULL; // 或者初始化为0为什么因为一个未初始化的局部指针变量它的值是垃圾值随机内存地址你根本不知道它指向哪里。拿着这张写着瞎编房间号的便签纸去敲门轻则程序崩溃Segmentation fault重则把别的房间里的数据改了产出诡异到没法查的bug。这就是传说中的野指针。你可以把NULL理解为空门牌号意思是我没有指向任何地方。在C语言中NULL宏定义就是((void*)0)。把指针初始化为NULL至少保证它是一张空白的便签纸你还能通过if (p NULL)来检查它是否有效。对比一下垃圾值指针连检查都没法检查因为垃圾值不等于任何一个你认识的NULL。2.3 指针变量的赋值操作声明归声明我们怎么把真实的地址存进去靠的是取地址运算符。int a 10; int *p a;a表示取出变量a的内存地址。这里有一个极其重要的概念辨析务必刻进DNA里变量a里存的是值10。指针变量p里存的是a的地址比如0x7ffc1234。它们俩的关系是a赋值给p也就是p指向了a。新手最爱犯的一个错就是直接写int *p a;。如果你写的a是int类型编译器会报错invalid conversion from int to int*。这就像你往抽屉里塞了一头大象货物的本体抽屉塞不下不说你还试图把大象说成是房间号。必须加上把房间号取出来才行。3. 解引用指针的灵魂操作3.1 星号的双重身份与解引用的本质如果你已经声明了int *p a;那么接下来最核心的操作来了——解引用。int a 10; int *p a; printf(%d\n, *p); // 输出 10在这里*p就相当于变量a本人。我把这个叫拿着房卡到402房间拿东西。因为p里存的是a的房间号你对*p做任何操作实际上就是对a这个房间里的货物做操作。你可以读也可以写*p 20; // 相当于 a 20; printf(%d\n, a); // 输出 20这就揭开了*符号的庐山真面目在声明语句里*只是用来标记我是一个指针变量的语法糖在语句其他位置*是解引用运算符它的意思是把我手上这个地址对应的那个房间的门打开。3.2 声明和使用场景的星号区分很多初学者被int *p;里的星号误导以为后面*p也是一个声明。大错特错记住这句口诀声明时别看星星用的时候星星是钥匙。代码位置星号含义解释int *p;声明标记告诉你p是一个指针变量printf(%d, *p);解引用操作取p指向的位置的值*p 100;解引用操作向p指向的位置写值如果你在用的过程中写成p 100;那是把100这个整数当成地址存进指针变量里去了然后如果你再*p基本就崩了。正确写法是*p 100;意思是要把100写进指针指向的那个内存单元。3.3 空指针与野指针的禁区当我们说初始化指针时重点来了int *p NULL; *p 10; // 非法对NULL解引用立即崩溃对NULL指针解引用就像拿着一张写着0号房间的便签纸去敲门——直接Segmentation fault。反过来野指针的危害更隐蔽它指向一个不存在的或者已经被释放的内存解引用它时程序可能没崩溃但结果完全随机或者炸得莫名其妙。所以在写代码的时候只要有除法运算就先检查除数不为0只要有指针操作就先检查指针不为NULL。这是一个合格C语言CV工程师的肌肉记忆。3.4 解引用与取地址的互逆关系搞懂了取地址和解引用你就掌握了两个互逆操作的钥匙。看这个恒等式int a 10; int *p a; // 下面这句话的逻辑 *p *(a) a是取房间号*是按房间号敲门。所以*a就等于a本身。同理*p就等于p因为*p就是aa当然还是p。如果你能把这个恒等式在心里转个弯指针的入门大关就算过了一半。4. 指针的基础运算跳跃的步长与心灵的比较4.1 指针加减法跳一个格子还是跳四个格子指针也可以做加减运算但它和普通的加减法有很大区别。这里引出一个极其重要的概念——步长。int a[3] {100, 200, 300}; int *p a[0]; // 指向数组的第一个元素 printf(%p\n, p); // 打印地址假设是0x1000 printf(%p\n, p1); // 打印结果是0x1004而不是0x1001为什么p1在内存里跳了4个字节因为p是int*类型步长就是sizeof(int)——在32位机上int占4个字节。你可以把指针加法理解成你往前走一步要根据你脚上穿的鞋的尺寸来算。穿char鞋跳1个字节穿int鞋跳4个字节穿double鞋跳8个字节。这个鞋码就是指针的类型。4.2 数组与指针的无缝配合因为数组在内存中是一段连续的房间所以指针运算和数组是天生的搭档。int a[3] {10, 20, 30}; int *p a; // 数组名a会退化为指向首元素的指针等价于 a[0] for (int i 0; i 3; i) { printf(%d , *(p i)); // 依次输出10,20,30 }这里有两个特别容易懵的点。第一a这个数组名在大多数表达式里会被编译器当作一个指向a[0]的指针常量。注意是常量你不能写a;因为数组名不是变量。第二*(p i)和p[i]是完全等价的。没错p[i]实际上就是*(p i)的语法糖。所以下标操作符[]本质上就是指针运算加解引用。这也是为什么a[i]写多了会让人忘记它俩基本是一回事。4.3 指针相减与比较运算除了加减指针还能做相减和比较。这里有一个限定条件两个指针必须指向同一个数组这个减法运算才有意义。int a[5] {1,2,3,4,5}; int *p1 a[0]; int *p2 a[3]; int count p2 - p1; // 结果是3表示中间隔了3个元素注意指针相减的结果不是字节数而是元素个数。这就好比问从3号房走到6号房中间隔了几个房间答3个。底层原理是两个地址差除以步长。这个操作在后面算数组长度、判断元素位置时非常有用。指针的比较运算、!、、等常用于判断两个指针的关系。例如判断指针是否指向同一个元素或者循环遍历数组时判断是否越界。比如for (int *p a[0]; p a[3]; p) { // 安全地访问 }4.4 越界与未定义行为在这里必须给所有新人泼一盆冷水C语言不会检查指针越界。如果你在数组边界之外去解引用比如p[-1]或者p100程序的行为是未定义的。它可能不报错但返回值是垃圾也可能直接崩溃。这个后果全凭运气。我做项目排查bug时见过最诡异的情况是一个人写越界把另一个变量的值给改了程序逻辑全乱但没有一个报错。所以指针运算的前提是你心里必须清楚所指向内存的边界。5. 新手必踩的五个坑常见问题与排查技巧实录5.1 编译报错invalid conversion from int to int*现象int *p 100;或者int *p a;a是int。原因你试图把一个整数当地址塞给指针变量。排查检查赋值右手边是不是忘了加或者是不是把int*和int搞混。指针变量只能装地址不能装值。5.2 运行时崩溃Segmentation fault现象程序运行到一半崩掉终端打印段错误。原因99%是空指针解引用或野指针解引用。比如int *p NULL; *p 1;排查思路推荐三步走用调试器gdb定位到崩溃行看是哪个指针出问题。看看这个指针初始化了没有没有被赋值有效地址在解引用前加一句if (p ! NULL)看崩溃是否消失。5.3 值不对但又不崩(*p)和*p的区别现象本想让指针指向的值加1结果发现值没变但指针地址却跳走了。原因运算符优先级。*p等价于*(p)它是先取出p指向的值不理会然后让p自增一个步长。而正确的做法是(*p)注意括号不能少。int a 10; int *p a; (*p); // 这个对a变成11 // *p; // 这个错指针偷偷跑了越界这是新手最爱犯的经典坑之一没有编译报错只有逻辑错乱。5.4 悬垂指针返回局部变量的地址现象写了一个函数返回a在main里调用乍一看能用过一会就崩了。int* func() { int a 10; return a; // 错a是局部变量函数结束时内存被回收 }原因a是栈上的局部变量函数返回后这个房间被系统分配给别人了但你手里还攥着这张房间号。这种指针叫悬垂指针。正确做法是传给函数外部变量的地址或者用malloc在堆上申请一块和函数同生死的空间。5.5 用gdb验证核心概念我一直强调学指针一定要用工具把底层看穿。在Linux下或者VS Code里配置好gdb用print命令打印地址和值效果非常直观。一次典型的调试过程(gdb) break main (gdb) run (gdb) print a # $1 (int *) 0x7ffffffde4c (gdb) print p # $2 (int *) 0x7ffffffde4c (gdb) print *p # $3 10亲眼看到a和p的值一样*p和a一样比看十遍博客都管用。6. 实操心法与扩展建议6.1 关于这条地基的边界今天这篇笔记标题叫指针1所以我只讲了定义、指针变量、解引用和基础运算这四大核心。像指针数组、数组指针、函数指针、二级指针指针的指针、以及C里的智能指针这些都是在这个地基上盖楼。如果你连今天的内容都还没彻底搞透先蹲在这儿反复敲代码别急着往后冲。地基打不牢后面盖楼越高塌得越惨。6.2 教你一个亲自复现的方法打开任意一位编辑器和编译器Windows下推荐用Dev-C或者VS Code配MinGWMac/Linux下直接gcc把下面这段代码敲进去编译运行看输出#include stdio.h int main() { int a 10; int *p a; p p 1; // 手动让指针跳跃一步 printf(变量a的值: %d\n, a); printf(变量a的地址(a): %p\n, a); printf(指针p里存的地址: %p\n, p); printf(解引用*p的值: %d\n, *p); return 0; }注意看a和p的输出。一个人名是内存地址一个是指针变量的内容它们地址一样就说明指向对了。然后你再试着修改int *p a;为int *p a;看编译器怎么报错再改回去加深印象。6.3 画图比背代码强一万倍最后分享一个实操心法。我带了这么多届学生学指针最快的那批人都有一个共同习惯在纸上画内存分布图。画一个大大的矩形框当作内存每个变量以方块表示里面写上值大便签纸箭头从指针变量指向变量。你解引用的时候就沿着箭头走过去把值拿出来。等你能把声明、赋值、解引用这三个步骤的图画顺了指针的本质就刻在脑子里了。别急着背语法先把门牌号、便签纸、钥匙这三个概念在脑子里转明白。脚踏实地画三天图、敲两天代码你会发现原来曾经让你怀疑智商的指针其实也就这么回事。
返回列表