C语言指针进阶:数组指针、指针数组与字符指针辨析
发布时间:2026/9/30 13:22:43来源:尧图网络
1. 为什么指针进阶这一关非过不可指针,数组指针,指针数组,字符指针——这四个词摆在一起几乎每个学C语言的人都在这一关卡过壳。我在带新人的时候发现一个规律语法会写、循环会敲、结构体也能用可一旦看到int (*p)[10]和int *p[10]同时出现人就懵了。这不是智力问题而是因为前三章讲指针时大家记住的都是指针就是地址这种过于粗糙的结论缺少一层对声明结构的解析能力。这篇文章解决的就是这件事让你看到任何一个指针相关的声明都能在几秒钟内拆出它到底是什么。数组指针的本质是一个指针它指向的是一个数组指针数组的本质是一个数组它里面装的全是指针字符指针则是一个指向字符的指针它因为字符串常量的存在而衍生出一堆坑。这三样东西在实际工程里出现的频率高得惊人——二维数组传参、字符串表、命令行参数、结构体里的字符串字段背后全是它们在撑场。适合谁看如果你能写出int a 10; int *p a;但对char *argv[]这种写法只能照抄不犯错、但说不清为什么那这篇文章就是给你写的。我会从声明解析讲到内存布局再到实际崩溃案例和排查手段尽量把每一步的为什么都摊开说清楚。下面先从最容易被混淆的辨识方法讲起这是后面所有内容的地基。1.1 从会用指针到读懂声明的分水岭很多人学指针停在操作层面取地址用解引用用*传参用指针避免拷贝。这套认知在单层指针里完全够用但一进到复合声明就崩了。原因是C语言的声明语法有一套自己的优先级规则你不按它的规则读就只能靠背。核心规则只有两条方括号[]的优先级高于星号*圆括号()可以强制改变结合顺序。就这两条能解释掉你现在遇到的所有困惑。int *p[10]因为[]优先级高所以p先和[]结合成为一个数组数组的元素类型是int *即装着10个整型指针的数组。而int (*p)[10]括号先把*和p绑在一起p先是一个指针指向的是int [10]即指向10个整型数组的指针。你可能会问为什么当初设计语言的人要搞这么绕因为C的声明设计目标是声明和使用一致。int *p[10]里p[10]表示取第10个元素一个指针那*p[10]自然就是解引用得到int声明的书写顺序和使用时的书写顺序是对应的。这个设计哲学叫声明模仿使用理解了它你甚至能自己推导出没见过的复杂声明而不是去查资料。顺带提一句函数指针热词里函数指针和指针函数也是同一个坑的两面。int (*f)(int)是函数指针f是指针指向一个接收int、返回int的函数。int *f(int)是指针函数f是函数接收int、返回int *。判断方法和数组那组完全一样看名字先和谁结合。1.2 三个概念的一句话辨析法在往下深入之前我先把三个主角各用一个生活化的类比锁死后面再看细节时你就有锚点了。指针数组像一排储物柜每个柜子上贴着一个门牌号地址柜子本身是固定数量的一排里面装的不是东西是东西放在哪的线索。它强调的是数组这个容器只不过容器里全是地址。数组指针像一把可以整体搬走整排柜子的搬运车。它不是指向某一个格子而是指向整排柜子这个整体你让它往前走一步它跨的是整排柜子的长度而不是一个格子的长度。字符指针像一张纸条纸条上写着一个地址顺着地址能找到一段字符。危险在于有些地址指向的是别人的展品只读的字符串常量你顺过去想修改它就会被系统拦下来。这三句话你先记住。接下来每一节我都会用代码和内存图把这三句话落到实处并且告诉你实际项目里它们各自出现在什么场景。1.3 从声明到内存一张表先建立全局观在动手写代码前先给你一张速查表把最容易混的几种声明一次性对齐。这张表我建议你存下来以后每次犯迷糊就回来看一眼。声明写法中文叫法名字先和谁结合本质描述步长1跨多少int *p[10]指针数组[]含10个int *的数组跨一个int *通常8字节int (*p)[10]数组指针()内的*指向int[10]的指针跨一整个int[10]40字节char *p字符指针*指向字符的指针跨一个字符char *p[5]字符串数组[]含5个char *的数组跨一个char *char (*p)[5]指向字符数组的指针()内的*指向char[5]的指针跨5个字符int (*f)(int)函数指针()内的*指向函数的指针无步长概念int *f(int)指针函数()返回int *的函数无步长概念表格里步长那一列特别关键也是后面排查越界、错位读写问题的核心依据。很多诡异的bug本质就是你以为指针走一步跨一个元素实际它跨了一整个数组。提示判断任何复杂声明先找变量名然后从变量名出发先看右侧有没有[]或()有就先结合没有或者被括号挡住就看左侧的*。括号里优先于括号外。2. 数组指针指向整个数组的那根指针数组指针在实际代码里出现的频率比很多人以为的要高得多。你不写它但你在用二维数组传参时编译器内部用的就是它。这一节我们把它的声明、步长特性和典型场景全部拆开。2.1 数组指针的声明与类型推导先从最直观的例子开始。假设我们要一个能指向5个int的数组的指针int arr[5] {1, 2, 3, 4, 5}; int (*p)[5] arr; // 注意是 arr不是 arr这里有个细节很容易踩arr和arr的值地址数值是一样的但类型完全不同。arr在多数表达式里会退化为int *指向首元素而arr的类型是int (*)[5]指向整个数组。这就像一栋楼的地址和这栋楼里第一个房间的门牌号数值碰巧相同但指向这栋楼和指向第一个房间是两种视角。怎么验证类型确实不同用sizeof和指针加法printf(%zu\n, sizeof(arr)); // 20整个数组 printf(%zu\n, sizeof(*p)); // 20解引用后是 int[5] printf(%p\n, (void*)arr); // 首元素地址 printf(%p\n, (void*)arr); // 整个数组地址数值相同 printf(%p\n, (void*)(arr 1)); // 首元素地址 4 printf(%p\n, (void*)(arr 1)); // 整个数组地址 20实测下来最后两行的差值大概率是4和20具体取决于int宽度。这个差异就是数组指针存在的意义它让指针加法以数组为单位移动。你以后处理二维数组的行、处理固定长度记录块靠的就是这个特性。我经常用一句话跟新人解释数组指针是给整体用的指针。你要搬整张桌子不能拿搬一个杯子的工具数组指针就是那台搬桌子的车。类型系统在这里不是在为难你而是在帮你保证一次搬走一整个数组这件事的逻辑正确。2.2 步长差异带来的实际影响步长这个概念讲原理容易讲清楚它为什么导致bug难一些。我举一个我真实遇到的场景解析一个由固定长度记录组成的二进制缓冲区。假设每条记录是16字节的结构体缓冲区是一个大数组。如果用普通的char *去遍历你得手动每次加16稍不留神写成加15或17就会读出错位的垃圾数据而且这种错误不会崩溃只会让解析结果悄悄变错——这是最可怕的。换成数组指针就稳了typedef struct { int id; char name[12]; } Record; // 16字节 Record records[100]; Record (*pr)[100] records; // 指向整个数组 Record *one records; // 指向单个记录 // one 1 跨16字节pr 1 跨1600字节两次加法的语义完全不同编译器会根据类型给你正确的偏移量。这就是用类型表达意图的价值你不是在手动算字节数而是告诉编译器我要移动一个记录还是我要移动整块记录数组剩下的交给它。反过来说如果你本来想移动一个记录却声明成了数组指针p 1会直接越过整个数组很可能访问到未映射内存程序当场段错误。段错误虽然粗暴但它至少比悄悄读错数据友好至少你知道出问题了。我最怕的其实就是那些不报错、只出错值的指针bug。注意不要把int (*p)[5]和int *p[5]写成一样的意思它们的 sizeof 结果一个是8指针一个是40数组。如果你写sizeof(p)想得到数组总大小却拿到了8八成是声明写岔了。2.3 二维数组传参为什么必须用数组指针这是数组指针最实用主义的出场场景。看这段代码void print(int a[][4], int rows) { for (int i 0; i rows; i) { for (int j 0; j 4; j) printf(%d , a[i][j]); printf(\n); } }形参写的是int a[][4]编译器实际把它调整成int (*a)[4]——一个数组指针指向含4个int的数组。为什么不能写成int **a因为二维数组在内存里是连续的一整块a[i][j]的真实寻址是*(*(a i) j)而a i需要知道一行有多宽才能算对偏移。宽度信息就藏在[4]里也就是藏在数组指针的类型里。如果你把形参写成int **a编译器会认为a i是在移动一个指针的大小8字节而实际一行是16字节结果就是第二行开始全错。更糟的是int **期待的是指针的数组而你传进去的是连续的二维块两者的内存结构根本不是一回事这类错误在段错误和错误数据之间随机切换排查起来非常折磨。那正确的通用写法是什么两种思路。第一种是宽度已知形参写int a[][4]或者明确写int (*a)[4]把列数固定下来。第二种是宽度动态这时就要把二维数组降维成一维处理参数传int *a加上行数列数自己算a[i * cols j]。很多高性能库比如图像处理、矩阵运算都采用第二种因为它对任意尺寸都通用代价是你要自己管索引。我个人的选择习惯是列数编译期固定就用数组指针写法类型安全、可读性好列数运行时才知道就传一维指针加宽高别硬凑int **。硬凑int **用得最多的其实是真正的指针数组场景比如字符串表char *argv[]那才是它的主场下一节就讲。3. 指针数组一个装着地址的数组指针数组是这三者里接受度最高的一个因为它读起来顺int *p[10]就是十个int指针。但接受度高不等于用得好实际项目里它的用法和坑一样多。特别是当它和字符串、结构体结合起来指针数组存放字符串这个组合几乎处处可见。3.1 声明形式的辨识技巧再强调一次辨识方法int *p[10]里p先和[10]结合成为数组int *是元素类型。所以它是数组长度10每个元素是int *。这里有个反直觉的点sizeof(p)得到的是整个数组的大小64位下是10 * 8 80而不是8。很多人写sizeof(p)/sizeof(p[0])来求元素个数这个技巧只对真正的数组有效对形参里的指针无效——因为形参里它已经退化成指针了。这就引出一个经典陷阱数组作为函数参数时会退化成指针。看这个void f(int *p[10]) { // 形参实际是 int **p printf(%zu\n, sizeof(p)); // 8不是80 }形参写int *p[10]编译器把它调整成int **p因为函数参数里的数组名等价于指向首元素的指针而这里首元素是int *所以指向首元素的指针就是int **。你如果指望在函数里用sizeof算出元素个数永远会得到指针大小。所以元素个数要么用单独的参数传要么在能看见数组定义的地方用宏计算别在函数体内指望它。我有一次review代码看到同事在一个工具函数里用sizeof(arr)/sizeof(arr[0])计算传进来的数组长度结果恒为28除以4因为int是4字节循环只跑了两次。这个bug很隐蔽因为小数组测试时可能是对的——测试数组恰好两个元素。所以这类问题必须靠理解退化规则来提前避免而不是靠测试碰运气。3.2 指针数组存放字符串的经典用法指针数组存放字符串是热词里出现频率极高的组合也是指针数组最漂亮的用法。看这个const char *names[] {Alice, Bob, Charlie, David};names是一个数组每个元素是const char *指向一个字符串常量。names[0]是指向Alice的指针names[1]指向Bob。注意这里字符串常量本身存在别处通常是只读数据段数组里存的只是它们的地址。这种结构相比二维字符数组有什么优势最直接的是内存效率和灵活性。如果用char names[4][20]你必须给每个名字分配20字节哪怕Bob只用4字节剩下16字节全是浪费。而指针数组里每个元素只是一个指针8字节字符串本体按实际长度存储不浪费。名字长度差异越大这个优势越明显。第二个优势是排序和交换的成本。如果你想按字典序给名字排序用指针数组只需要交换指针各8字节而不需要整块拷贝字符串每个20字节。指针交换是常数开销字符串交换是长度相关的开销。数据量一大差距就出来了。这也是为什么很多配置表、命令表、字典内部都用指针数组而不是二维字符数组。第三个优势是变长和动态。指针数组的元素可以指向不同长度的字符串甚至指向动态分配的内存。二维字符数组则被列宽锁死。当然用动态分配就要配套管理释放这是代价后面排错章节会讲这类内存问题的排查。// 字符串排序示例只动指针不动字符串本体 int cmp(const void *a, const void *b) { return strcmp(*(const char **)a, *(const char **)b); } qsort(names, 4, sizeof(names[0]), cmp);qsort的第四个参数比较函数里那个*(const char **)a是初学者另一个坎因为传入的是指向数组元素的指针而数组元素本身是const char *所以参数是指向const char *的指针即const char **。这个指针的指针层级关系理清楚了一次就通了。3.3 指针数组与二维字符数组的内存布局对比为了把上面说的优势落到实处我们把两种写法的内存布局画出来用文字描述。二维字符数组char names[3][8] {Tom, Jerry, Spike};内存里是一整块连续的24字节排成3行8列。names[0]就是第0行这块8字节的首地址names[1]是第1行首地址它们之间固定相差8字节。字符串本体和行结构是绑死的任何一行不够长都会被截断写超了会覆盖下一行——这是经典的静默数据破坏。指针数组const char *names[] {Tom, Jerry, Spike};内存里是两块一块是3个指针组成的数组24字节64位下另一块是三段字符串常量。names[0]指向第一段字符串这些字符串不要求连续、不要求等长。数组元素之间固定相差8字节指针大小但字符串之间没有固定间距。这个区别带来一个实践结论如果你需要频繁修改字符串内容用二维字符数组如果你主要是读取、查找、排序用指针数组。因为字符串常量通常是只读的写进指针数组的字符串如果被修改行为未定义。如果你需要可修改的字符串集合可以用指针数组指向各个独立的malloc出来的缓冲区或者干脆用二维数组。注意char *p hello; p[0] H;这种代码在某些平台上看起来能跑是因为字符串常量恰好落在了可写内存里但这属于未定义行为换编译器或换优化等级就可能崩溃。写字符串字面量时一律给指针加const让编译器帮你拦住这类修改。4. 字符指针从字面量到字符串操作的细节坑字符指针看起来最简单char *p指向一个字符。但它坑最多因为字符串常量这个特殊存在让指针和数组的边界变得模糊。很多让人一脸问号的崩溃都出在这一块。4.1 指向字符串常量的只读陷阱最经典的对比char a[] hello; // 字符数组本体可写 char *p hello; // 字符指针指向只读常量C中类型是 const char*第一行分配了6字节含结尾的\0在栈上内容是hello的副本你可以a[0] H随便改。第二行里p是一个指针变量指向的hello存放在静态存储区通常只读。你写p[0] H行为未定义在多数现代系统上会直接段错误。为什么设计成这样因为字符串常量需要在程序生命周期内一直存在而且如果同一个字面量在代码里出现多次编译器可以合并它们存一份节省空间。如果允许随便改合并就会出问题——你改了p[0]另一个指向同一份常量的地方也莫名其妙变了。所以标准规定它不可写。还有个更隐蔽的差异sizeof(a)是6sizeof(p)是8。因为数组知道自己是数组指针只是个指针。这点在需要传递字符串大小时特别重要。另外a作为数组名在多数表达式里会退化成指针但sizeof和取地址a是例外。我个人建议的习惯是凡是写字符串字面量的字符指针一律声明为const char *。这样编译器会在你试图修改时直接报错把运行时的不确定崩溃提前成编译期的确定错误。这是花零成本换稳定性非常划算。C里更严格字符串字面量的类型本来就是const char[N]不加const会直接编译不过。4.2 字符数组与字符指针的等价与不等价很多时候我们听到数组名就是指针这句话在特定上下文里成立但一旦当成普遍真理就会摔跤。下面这几组对比能帮你把边界划清。对比项char a[] abcchar *p abc存储位置栈局部时指针在栈内容在静态区内容可写可以不可以未定义sizeof4数组总长8指针大小可否重新赋值不可以a ...非法可以p ...合法作为函数参数退化为char *本身就是char *数组名不可重新赋值这一条也常被忽略。a xyz是编译错误因为数组名不是左值它代表的是一块固定的内存块不能改成指向别处。而指针变量p本身有地址、可以改指向p xyz换个地址指过去完全合法。这一点在很多需要遍历不同字符串的循环里区别巨大。再看一个实际场景函数接收字符串并想在其内部修改传入的内容。如果调用方传的是字符串常量函数内部试图修改就崩如果传的是字符数组就没问题。所以谁拥有、是否可写这两个信息必须由接口约定清楚。C标准库里那些带const char *的参数设计就是在传达我只读、不修改的契约。void to_upper(char *s) { // 声明为可写 for (; *s; s) if (*s a *s z) *s - 32; } char buf[] hello; to_upper(buf); // 可以 // to_upper(hello); // 危险传常量给可写接口上面这种接口设计如果参数能写真写const char *就应该写上让类型系统帮你挡住误传。只有确实需要修改时才用非const。这个习惯能帮你在编译期就发现大量潜在问题。4.3 字符串遍历与指针运算的实用写法字符指针在遍历字符串时非常顺手因为很多字符串操作本质上就是边走边看。核心惯用法是const char *s hello world; while (*s) { // 遇到结尾的 \0 自动停 putchar(*s); s; }while (*s)这个写法比while (*s ! \0)更常见因为\0的值就是0*s作为条件时非零即真简洁且效率不差。但要注意分清*s和(*s)前者是先取*s的值、再让s前进一格后者是让s指向的那个字符自增。这两个写法在字符串处理里含义完全不同混用是经典bug源。char buf[] abc; char *q buf; char c *q; // c aq 现在指向 b // 若写 (*q)则 buf[0] 变成 bq 位置不变再分享一个实用技巧统计字符串长度、查找子串、分割全都可以用指针运算手写理解了指针的移动标准库函数的实现原理就透明了。比如手写strlensize_t my_strlen(const char *s) { const char *start s; while (*s) s; return (size_t)(s - start); }最后一行s - start是指针相减得到的是相隔多少个元素这里是字符个数而不是字节数差值除以1这么简单——因为char正好1字节看起来一样但换成int *相减结果就是元素个数而不是字节数。指针减法永远以元素为单位这是标准规定的。提示指针相减的两个指针必须指向同一个数组或数组末尾后一个位置否则行为未定义。跨数组相减得到的数字没有意义可能因为内存对齐等原因看起来对但那只是巧合。5. 高频混淆点与实战排错手册前面讲了原理和写法这一节集中处理实际跑起来会出什么事。我个人踩过的坑、帮同事排查过的case整理成表格和套路你以后遇到类似症状可以按图索骥。5.1 声明辨识与常见误写速查先把最容易写错的几组整理出来。判断方法还是那句先找名字先看右边再看左边括号优先。你想表达的正确声明常见误写误写的后果10个int指针的数组int *a[10]int (*a)[10]变成只装一个数组指针指向int[10]数组的指针int (*p)[10]int *p[10]变成10个指针的数组5个字符串的数组char *s[5]char (*s)[5]变成长度5的字符数组指针指向函数的指针int (*f)(int)int *f(int)变成返回指针的函数返回指针的函数int *f(int)int (*f)(int)变成函数指针判断技巧再固化一遍看到*和[]相邻谁先结合看括号。没有括号就是[]赢。把int *a[10]读成a是一个数组元素是int指针把int (*p)[10]读成p是一个指针指向int[10]。出声念一遍错误率会下降很多这是我验证过的笨办法但有效。另外提醒一句热词里那个error (orcap-5004): error initializing com property pages: 无效指针类的报错。这类无效指针提示通常是IDE或工具的GUI组件初始化失败跟你的C指针写法没关系本质是软件环境或注册表问题。看到无效指针这种中文报错先分清它是编译错误、运行崩溃还是第三方工具的环境报错别一股脑往语法上找原因方向错了会浪费大量时间。5.2 崩溃与错值的排查套路指针类问题分两类一类立刻崩段错误一类不崩但结果错。后一类更麻烦。我把常见症状和排查方向列成表症状可能原因排查手段段错误访问地址极低如0x0附近空指针解引用检查指针是否初始化、函数是否返回NULL段错误地址很大且随机野指针、数组越界、栈被破坏打印指针值加边界检查开编译告警数据悄悄变错不崩指针步长不对、类型不匹配检查声明类型打印sizeof和偏移量字符串被截断缓冲区太小、缺\0用带长度限制的函数检查分配大小释放时报错重复释放、释放非堆地址用调试分配器打印每步分配地址排查指针问题的核心手法我总结成三步。第一步打印。在可疑位置把指针的值、sizeof、以及和基址的差值打出来很多问题看一眼数字就明白了。第二步开满编译器告警。-Wall -Wextra能抓出大量类型不匹配和可疑写法我见过太多人用默认告警等级白白放过了编译器能帮他找的错。第三步上工具。地址消毒剂AddressSanitizer能在越界访问和释放错误时立刻给出精确位置比漫无目的地读代码快一个数量级。# 用地址消毒剂编译运行时自动检测越界和释放错误 gcc -fsanitizeaddress -g -Wall main.c -o main ./main上面这个编译方式我强烈建议养成习惯尤其是处理指针和内存的代码。开了它之后越界、使用悬空指针、重复释放都会在首次触发时给出文件行号而不是等到稍后才崩溃。代价是运行慢一点、内存占用高一点调试阶段完全值得。5.3 内存管理上的经验与纪律指针本身不可怕可怕的是所有权不明。一段代码里一个指针指向的内存到底谁负责释放如果没人负责就是泄漏如果两个人负责就是重复释放。我的做法是给每块动态内存定一个明确的归属谁分配谁负责需要转移所有权时接口名字里体现出来。字符指针数组特别容易出这类问题。比如你有一组配置项名字每个名字都malloc分配那么释放时必须逐个释放再释放数组本身顺序和层级不能错。漏一层就泄漏重复一层就崩溃。用指针的指针char **管理这类结构时最好配套一个专门释放的函数把层级和顺序固化成一处而不是散落在各处。char **make_list(int n) { char **list malloc(n * sizeof(char *)); if (!list) return NULL; for (int i 0; i n; i) { list[i] malloc(16); if (!list[i]) { /* 出错时要倒序释放已分配的部分 */ } } return list; } void free_list(char **list, int n) { for (int i 0; i n; i) free(list[i]); free(list); }注意上面出错路径里倒序释放已分配部分这个细节。很多新手在分配中途失败时直接返回NULL把前面已经分配的内存全泄漏了。分配循环里任何一步都可能失败所以部分失败的回滚是必须的。这类代码写起来啰嗦但这是C语言里负责任的做法。提示智能指针是C里解决所有权问题的机制它把谁负责释放交给类型系统常见的unique_ptr表达独占所有权、shared_ptr表达共享所有权。C语言里没有语言级的智能指针靠的是人为的纪律和成对出现的分配/释放函数。如果你在写C优先用智能指针而不是裸指针管理堆内存。5.4 面试与刷题里必须一眼看穿的几道题指针相关的面试题翻来覆去就那几个考点看穿套路比硬背答案有用。第一类sizeof和strlen的组合。char s[] hellosizeof(s)是6strlen(s)是5差的那1就是结尾的\0。如果是指针char *p hellosizeof(p)是指针大小平台相关通常4或8strlen(p)仍是5。考的就是数组名和指针在 sizeof 下的区别。第二类指针加减与数组的偏移。给定int a[5]问a 1和a 1差多少。答案前者跨过整个数组20字节后者跨一个int4字节。这类题不考别的就是考数组指针和元素指针的步长区别。第三类字符串常量的可写性。给一段char *p abc; *p x;问运行时行为。答案是未定义行为可能崩可能不崩不能依赖。正确做法是改成数组或者加const。第四类多级指针的解引用层级。int a 1; int *p a; int **pp p;问**pp、*pp、pp各是什么。**pp是1*pp是p的值即app是p的地址。层级数就是解引用的次数多一层少一层都会错。这些题背后其实是同一句话指针的类型决定了它的行为。类型里有宽度信息、有步长信息、有可写性信息读懂了类型题就少了大半。最后提一句热词里的快慢指针在有序数组原地去重。那类问题虽然常见于某些脚本语言的算法题但双指针的思想是跨语言的一个指针走得快、一个走得慢或者一个从头、一个从尾利用有序性把嵌套循环降成单层扫描。理解指针的移动语义之后这类算法题会变得非常直观——本质就是用两个位置标记代替额外的存储空间。6. 从裸指针到工程化几条我真心建议的习惯写到这里三大主角的原理和坑基本铺开了。最后这部分不讲新语法讲我认为最值钱的几个习惯这些是文档里不会写、但对写代码的人影响最大的东西。第一条让类型替你说话。能用const就加const能写明确的数组指针类型就别写成int **硬凑。类型越精确编译器能帮你拦住的错误越多运行时你排查的就越少。我见过太多能跑就行的写法最后都在深夜的线上故障里还回来了。第二条声明和定义保持一致的命名习惯。给数组指针变量加后缀或前缀体现它是整体给元素指针体现它是单个。团队里统一一种读法比如统一念XX的数组指针review 时沟通成本能降一半。命名这东西看着小但在多人协作里价值极高。第三条动态内存必须有明确归属和回收路径。分配和释放成对出现最好集中在同一层跨层的所有权转移要有清晰的接口约定。谁申请谁释放、谁申请谁回收这个纪律松掉之后内存问题会像潮水一样涌来。第四条用工具而不是用眼睛。地址消毒剂、编译器满告警、静态检查工具这些都是把不确定的运行时风险提前变成确定的编译期或首次运行报错的手段。我个人的体会是开了这些工具之后指针相关bug的定位时间至少缩短一半因为你不用猜工具会直接告诉你哪一行越界了。指针进阶难难在它逼你同时理解语法怎么读内存怎么排类型怎么算三件事而这三件事在初学时是分开教的。把int (*p)[10]、int *p[10]、char *p这三个声明在脑子里各自对应一幅内存图再配上sizeof、步长、可写性这几个抓手你会发现后面那些看似花哨的声明拆开看都是同一套规则的不同组合。真正把这套规则摸熟之后回头看指针的指针函数指针结构体指针你会觉得它们只是老朋友换了身衣服。
网站建设高端定制企业官网