一个写全栈技术、偏底层基建、爱研究 bug 的程序员博客。技术界的一名小工匠⊥⊤每天进步一点点。C语言指针万字超详细教程吃透指针才算真正学会C语言前言很多学习C语言的朋友都会卡在同一个难点——指针。有人学了几个月只会写int *pn分不清指针数组、数组指针有人随便解引用程序直接崩溃面试时被问到char *str和char str[]的区别、const修饰指针三类写法直接卡壳。指针之所以被称为C语言的灵魂是因为操作系统、底层驱动、数据结构、动态内存全部依赖指针实现。本篇文章从零起步不跳过任何基础结合内存图解、完整可运行代码、易错坑点、工程实战一次性讲透指针全部知识点看完本文彻底告别指针盲区。一、前置基础内存与地址看懂指针的前提1.内存的基础概念计算机内存是一块连续的存储空间最小单位是字节每一个字节都会分配唯一编号这个编号就是内存地址。我们在程序中定义的一系列变量操作系统将会在程序运行时动态在内存中一一开辟一块对应大小的空间存放数据想要找到这块空间就需要它的地址。举个例子int num 10;int类型占4字节系统会分配4个连续字节每个字节有专属地址变量 num 代表这整块空间地址是首字节的编号。2.两个核心运算符(1).取地址运算符作用于变量获取变量的内存首地址。(2).*解引用运算符作用于地址/指针根据地址取出内存中存储的值。3.指针的本质定义指针变量是专门用来存储内存地址的特殊变量。普通变量存数字、字符指针变量只存地址编号。二、指针基础语法与使用1.指针变量的定义标准格式数据类型 *指针名;示例#includestdio.hintmain(){inta10;// 定义int类型指针pnint*pn;// 将a的地址赋值给指针pnpna;printf(a的值%d\n,a);printf(a的地址%p\n,a);printf(指针pn存储的地址%p\n,pn);printf(通过指针取出a的值%d\n,*pn);return0;}运行结果a的值10 a的地址0000008ad53ff904 指针pn存储的地址0000008ad53ff904 通过指针取出a的值10 Process finished withexitcode0重点误区纠正int *pn; 中 * 是修饰 pn 代表 pn 是指针不是修饰int。如果同时定义多个指针每个变量前都要加 * 。int *pn1, *pn2; // 两个指针正确int *pn1, p2; // pn1是指针p2是普通int变量新手高频错误2.通过指针修改原变量的值指针保存了变量的地址解引用 *pn 等价于直接操作原变量内存。#includestdio.hintmain(){inta10;int*pna;*pn100;// 修改地址对应内存的值printf(a %d,a);// 输出100return0;}输出结果a100Process finished withexitcode03.指针变量的大小指针存储的是地址编号在同一操作系统下所有指针占用字节大小完全相同和指针指向的数据类型无关-32位系统地址32位所有指针占4字节-64位系统地址64位所有指针占8字节博文本地机器环境信息Microsoft Windows[版本10.0.19045.6466](c)Microsoft Corporation。保留所有权利。 C:\Users\Lenovosysteminfo 主机名: DESKTOP-SU2LUGI OS 名称: Microsoft Windows10专业版 OS 版本:10.0.19045 暂缺 Build19045OS 制造商: Microsoft Corporation OS 配置: 独立工作站 OS 构建类型: Multiprocessor Free 注册的所有人: Lenovo 注册的组织: 产品 ID: 00330-80000-00000-AA701 初始安装日期:2023/7/26,12:28:47 系统启动时间:2026/6/5,9:23:55 系统制造商: LENOVO 系统型号:20238系统类型: x64-based PC 处理器: 安装了1个处理器。[01]: Intel64 Family6Model60Stepping3GenuineIntel ~2501 Mhz BIOS 版本: LENOVO 79CN46WW(V3.05),2013/12/23 Windows 目录: C:\Windows 系统目录: C:\Windows\system32 启动设备:\Device\HarddiskVolume1 系统区域设置: zh-cn;中文(中国)输入法区域设置: zh-cn;中文(中国)时区:(UTC08:00)北京重庆香港特别行政区乌鲁木齐 物理内存总量:8,108MB 可用的物理内存:359MB 虚拟内存: 最大值:14,986MB 虚拟内存: 可用:3,075MB 虚拟内存: 使用中:11,911MB 页面文件位置: C:\pagefile.sys 域: WORKGROUP 登录服务器:\\DESKTOP-SU2LUGI 修补程序: 安装了13个修补程序。[01]: KB5066130[02]: KB5066135[03]: KB5003791[04]: KB5011048[05]: KB5011050[06]: KB5015684[07]: KB5033052[08]: KB5072653[09]: KB5071959[10]: KB5031539[11]: KB5066790[12]: KB5071982[13]: KB5005699 网卡: 安装了3个 NIC。[01]: Broadcom802.11n Network Adapter 连接名: WLAN 启用 DHCP: 是 DHCP 服务器:192.168.96.1 IP 地址[01]:192.168.100.221[02]: fe80::eb3e:dcec:f21c:6644[02]: Qualcomm Atheros AR8172/8176/8178 PCI-E Fast Ethernet Controller(NDIS6.30)连接名: 以太网 状态: 媒体连接已中断[03]: Sangfor SSL VPN CS Support System VNIC 连接名: 以太网2状态: 媒体连接已中断 Hyper-V 要求: 虚拟机监视器模式扩展: 是 固件中已启用虚拟化: 是 二级地址转换: 是 数据执行保护可用: 是 C:\Users\Lenovo验证代码#includestdio.hintmain(){int*pn1;char*pn2;double*pn3;printf(int*大小%lu\n,sizeof(pn1));printf(char*大小%lu\n,sizeof(pn2));printf(double*大小%lu\n,sizeof(pn3));return0;}输出结果int*大小8 char*大小8 double*大小8 Process finished withexitcode04.指针类型的真正作用既然所有指针大小一样为什么还要区分int *pn1、char *pn2、double *pn3答案指针类型决定解引用时读取几个字节、指针偏移时移动多少字节。(1).int *pn1解引用读取4字节 p 地址4(2).char *pn2解引用读取1字节 p 地址1(3).double *pn3解引用读取8字节 p 地址8三、指针与数组新手最难理解模块1.数组名的本质数组名是数组首元素的地址是一个常量地址不允许赋值修改。#includestdio.hintmain(){intarr[5]{1,2,3,4,5};// arr arr[0]printf(%p %p,arr,arr[0]);}输出结果000000bcce7ffa70000000bcce7ffa70 Process finished withexitcode02.数组下标与指针偏移完全等价arr[i] 编译器底层会翻译为 *(arr i)推导arr[0] *(arr 0)arr[1] *(arr 1)遍历数组两种写法#includestdio.hintmain(){intarr[5]{10,20,30,40,50};int*pnarr;// 方式1下标遍历for(inti0;i5;i){printf(%d ,arr[i]);}printf(\n);// 方式2指针偏移遍历for(inti0;i5;i){printf(%d ,*(pni));}return0;}输出结果10203040501020304050Process finished withexitcode03.核心区分指针数组 VS 数组指针面试必考(1)指针数组存放指针的数组int *arr[]数组里每一个元素都是int类型指针多用于存储字符串数组。char*strArr[]{java,c,python};(2)数组指针专门指向数组的指针int (*pn)[5]其中() 优先级高于 *必须加括号指向一整个长度为5的int数组。intarr[5]{1,2,3,4,5};int(*pn)[5]arr;区分记忆口诀括号跟着谁谁就是主体。四、指针与函数1.值传递的缺陷普通变量作为函数参数是值传递函数内部操作的是变量副本无法修改外部实参voidswap(inta,intb){inttempa;ab;btemp;}// 调用后外部数值不会交换2.指针传参地址传递传递变量地址函数内通过解引用直接操作原始内存可修改外部变量同时避免大数据拷贝节省内存。正确交换两个数字代码#includestdio.hvoidswap(int*a,int*b){inttemp*a;*a*b;*btemp;}intmain(){intx10,y20;swap(x,y);printf(x%d y%d,x,y);return0;}输出结果x20y10Process finished withexitcode03.指针函数返回值为指针的函数函数返回一个地址注意禁止返回局部栈变量的地址函数执行完毕栈内存释放会形成野指针。// 正确返回全局/堆内存地址char*getStr(){staticcharstr[]hello c;returnstr;}4.函数指针高阶用法回调函数底层函数存放在代码段同样拥有地址指向函数的指针就是函数指针。定义格式 返回值类型 (*指针名)(参数列表)#includestdio.hvoidprint(intnum){printf(数字%d\n,num);}intmain(){// 定义函数指针pnvoid(*pn)(int)print;// 两种调用方式等价pn(100);(*pn)(200);return0;}输出结果数字100 数字200 Process finished withexitcode0典型应用回调函数、排序库函数 qsort 。五、二级指针指针的指针1.定义与内存结构一级指针int *pa存储普通变量地址二级指针int **pb存储一级指针变量的地址。#includestdio.hintmain(){inta283;int*paa;int**pbpa;printf(a %d \n,a);// 输出283printf(*pa %d \n,*pa);// 输出283printf(**pb %d \n,**pb);// 输出283return0;}输出结果a283*pa283**pb283Process finished withexitcode0//*pa 取出一级指针pa的值变量a的地址//**pb 先取pa的地址再解引用得到a的值2.二级指针两大实用场景(1).函数内修改一级指针的指向(2).存储字符串数组 char **str 多用于命令行参数 main(int argc, char **argv) 。3.三级与多级指针一级指针 int *p 存普通变量地址二级指针 int **pl2 存一级指针地址三级指针 int ***pl3 存二级指针地址#includestdio.hintmain(){inta100;int*pa;// 一级int**pl2p;// 二级int***pl3pl2;// 三级printf(a %d\n,a);printf(*p %d\n,*p);printf(**pl2 %d\n,**pl2);printf(***pl3 %d\n,***pl3);return0;}输出a100*p100**pl2100***pl3100Process finished withexitcode0(1).什么时候会用到三级指针[1]函数内部修改二级指针本身的指向[2]三层字符串数组、动态二维数组扩容场景[3]底层复杂参数传递嵌入式、内核少量场景。(2).开发规范提醒工程里极少用到三级及以上指针多层解引用可读性极差容易产生野指针、逻辑混乱如果出现三级指针优先思考重构代码改用结构体封装简化。六、字符串与char*指针开发高频场景1.char *str和char str[]核心区别(1).char *str “abc”;字符串常量存储在只读常量区指针str仅保存首字符地址不允许修改字符串内容修改会触发段错误。(2).char str[] “abc”;字符串拷贝到栈内存数组内存可读写支持修改字符。示例报错代码char *s “test”;s[0] ‘a’; // 程序崩溃常量区不可写2.指针操作字符串利用指针偏移遍历、拷贝字符串无需下标#includestdio.hintmain(){charstr[]CLion is a cross-platform IDE for C and C.;char*pnstr;while(*pn!\0){printf(%c,*pn);pn;}return0;}输出CLion is a cross-platform IDEforC and C. Process finished withexitcode0七、万能指针 void*1.特性void* 无固定类型可以接收任意类型变量的地址不能直接解引用必须强制转换为对应类型指针后才能取值。2.典型用途(1).通用参数接收任意地址(2).malloc 动态内存分配返回值为 void* 适配所有数据类型。#includestdio.h#includestdlib.hintmain(){// malloc返回void*自动强转int*int*arr(int*)malloc(5*sizeof(int));while(*arr!\0){printf(%c,*arr);arr;}return0;}输出PUss〡P\reopaagEMeoome8Crai 6ongFs\geonlPPrFstiCn2.ii\;Wose;WoCiwymW;WoSe\dPrl1;WoSe\n\\reopacAoSpf-l:n4n\g ey\Qh\\\reopacMotnss\g eea\eJE01n\g eea\o01bFBSU_F_IDuPE.;EAC.;ESSW.;CiDWoSe\v\vaOiwTMEMEOUGLPrFstiCn2.iRS_IN0ongW2\g eonl CpCiwymcePraCratra4Crai EHs\oSeo:nsM:eLvpto\pOSITEn6ayMl pg uIlMICSRL:eLvM:eLvpto\pmPri()\g ex\mFsome:omlPI:ePiir\d BORSS: Process finished withexitcode0八、空指针、野指针90%程序崩溃的根源1.空指针 NULLNULL 本质是宏定义 (void*)0 代表地址0该地址操作系统禁止读写用于标记指针不指向有效内存。规范写法定义指针时初始化赋值为NULL使用前判断cint *pn NULL;if(pn ! NULL){*pn 10;}2.野指针指针存储随机、非法地址没有明确指向解引用会随机崩溃、破坏内存。产生原因(1).指针未初始化存放随机垃圾地址(2).动态内存free之后指针未置空(3).函数返回局部栈变量地址。3.规避野指针规范(1).所有指针定义立刻初始化为NULL(2).堆内存释放后手动赋值 p NULL (3).函数绝不返回局部栈变量地址(4).使用指针前先判断非NULL。九、const修饰指针面试高频考点分三类写法区分修饰对象1.const int *pn指针指向的数据不可修改指针本身可以改指向int a 10,b20;const int *pn a;pn b; // 合法*pn 100; // 报错数据只读2.int *const p指针本身固定不能更换指向数据可修改int a10,b20;int *const pn a;*pn 100; // 合法pn b; // 报错指针只读3.const int *const pn指向和数据全部只读完全不可修改十、指针常见十大易错坑点1.指针未初始化直接解引用触发野指针崩溃2.混淆int *pn1,p2误以为两个都是指针3.修改字符串常量区 char* 指向的内容4.函数返回局部变量地址生成野指针5.free内存后指针不置空悬挂指针6.分不清数组指针与指针数组7.void* 未强制转换直接解引用8.二级指针滥用多层解引用逻辑混乱9.指针越界访问数组内存10.混淆 * 在定义和代码中的不同作用。十一、指针实战小案例案例1指针实现字符串反转#includestdio.h#includestring.hvoidreverse(char*str){char*leftstr;char*rightstrstrlen(str)-1;while(leftright){chartemp*left;*left*right;*righttemp;left;right--;}}intmain(){charbuf[]CLion is a cross-platform IDE for C and C.;reverse(buf);printf(%s,buf);return0;}输出.C dna C rof EDI mroftalp-ssorc a si noiLC Process finished withexitcode0案例2函数指针实现简单回调#includestdio.hvoidcalc(inta,intb,void(*func)(int,int)){func(a,b);}voidadd(intx,inty){printf(和%d\n,xy);}voidsub(intx,inty){printf(差%d\n,x-y);}intmain(){calc(10,5,add);calc(10,5,sub);return0;}运行结果和15 差5 Process finished withexitcode0十二、全文总结1.指针本质存储内存地址的变量地址定位空间类型决定读写字节长度2.核心运算符取地址、*解引用3.数组名是首元素常量地址 arr[i] 等价(arri) 4.指针传参可修改外部变量大幅减少数据拷贝开销5.分清数组指针、指针数组、函数指针、指针函数四类易混概念6.野指针、只读字符串、局部地址返回是程序崩溃三大元凶7.const修饰指针分三种场景区分“指针只读”和“内容只读”8.void万能指针适配所有内存地址动态内存分配必备。掌握指针才能看懂链表、栈、队列、动态内存、操作系统底层逻辑这也是C语言区别于高级语言的核心能力。后续学习数据结构、嵌入式开发、逆向工程指针都是必不可少的基础。一句话总结指针指针就是存放内存虚拟地址的变量通过地址直接读写内存是C语言操控底层存储的核心工具。Okgoodbye。