
先把话说清楚这篇小结讲的是C/C代码里的指针不是鼠标指针更不是系统设置里那个“提高指针精确度”的选项。我在学习和答疑过程中经常遇到有人把“指针”理解成玄学也经常看到有人栽在指针常量、数组指针、函数指针这些名词上。这篇文章把指针学习里最核心、最容易混、也最实用的一整套内容梳理成一份可收藏的笔记适合正在学C语言的同学、刚接触C的初学者以及那些写代码时遇到空指针、内存问题就头疼的人。指针并不神秘它就是一门“用地址操作内存”的手艺。难点不在于概念本身而在于一上来要接受“间接”这个思维模式你手里握着一张写着门牌号的小纸条通过纸条去找房子。搞明白这一层后面所有指针语法、指针数组、智能指针都能串起来。1. 先搞清楚指针到底解决什么问题1.1 从“变量就是一块内存”说起很多初学者把“变量”当成一个数学符号x等于5y等于6。但在计算机里变量本质上是内存中的一块区域有起始地址、有长度、有内容。普通变量直接给你内容指针变量给你的不是内容而是内容存放在哪里的地址。举个例子定义一个int a 10编译器会在内存中划出4个字节把那4个字节存成10。变量名a是给人看的抽象CPU不认这个名字它只认地址。你可以用a把这个地址取出来然后定义一个int *p a把这个地址保存到p里。这就是指针变量最基础的操作存地址、取地址、按地址访问。我见过最快的理解方式是把它类比成快递柜。普通变量是柜子里的包裹指针是取件码纸条。取件码不是包裹本身但通过它能打开对应柜子拿到包裹。你现在手里有张纸条写着“三号柜第三格”这就相当于指针变量顺着这个位置去取东西就是解引用操作*p。指针到底解决了什么问题最核心的是三点第一函数之间共享和修改数据C语言函数传参默认是值传递想改外部变量只能靠指针传地址第二动态内存管理malloc、new出来的一块内存只能通过指针去访问第三实现灵活的数据结构和算法链表、树、图本质上都是靠指针把不连续的内存节点串起来。1.2 指针类型为什么重要有一个问题我几乎每次都会被问到指针变量不就是存一个地址吗地址不都是一个数字吗为什么要分int *、char *、double *这么多类型答案藏在两层里。第一层解引用的时候编译器要知道按几个字节去读。int *p指向一个int你写下*p时编译器从这个地址开始读4个字节并按int解释char *p就只读1个字节。第二层指针加减运算的步长不一样。p 1不是地址加1而是地址加sizeof(指向类型)个字节也就是跨过一个完整元素。我用一个很经典的例子说明#include stdio.h int main(void) { int x 0x12345678; int *ip x; char *cp (char *)x; printf(ip %p, ip1 %p\n, (void *)ip, (void *)(ip 1)); printf(cp %p, cp1 %p\n, (void *)cp, (void *)(cp 1)); return 0; }你跑一下就会看到ip和cp本身可能指向同一个地址但ip 1比ip大4个字节cp 1只大1个字节。这就是为什么指针必须带类型信息。我经常看到有人把不同类型的指针互相强转然后步长计算就乱了这种 bug 特别隐蔽排查起来也很费劲。所以别嫌类型麻烦指针类型其实就是告诉编译器“这个位置存的是什么规格的数据”。这里额外提一个很多人忽略的点数组名在很多场景下会“退化”成指向首元素的指针。arr和arr[0]的值一样类型却不一样。arr能参与指针运算但它不是变量不能做arr这样的赋值。这就是后面要展开的“指针是地址但不等于什么都随便改”。2. 那些年我们总是搞混的概念2.1 指针常量与常量指针这两个词长得像双胞胎意思却相反。我当年也背了好几次最后发现还是从“const修饰谁”这个角度去拆最靠谱。const int *p读作“pointer to const int”也就是指向常量的指针。p本身是可以变的你可以让它指向别的地址但你不能通过*p去修改它当前指向的那个int值。int *const p读作“const pointer to int”也就是常量指针。p本身是只读的定义时必须初始化之后不能再指向别的地方但它指向的那个int值是可以通过*p修改的。const int *const p两个都不可变即“指针本身是常量指向的内容也是常量”。int a 10, b 20; const int *p1 a; p1 b; // 允许p1本身可变 // *p1 30; // 编译错误不能通过p1修改指向的值 int *const p2 a; // p2 b; // 编译错误p2本身不可变 *p2 30; // 允许a变成了30怎么记看const离谁近。const int *p中const离int近修饰的是int所以是“指向const int的指针”int *const p中const离p近修饰的是p本身所以是“const指针”。墙上的便签可以换、便签上标记的内容不能改这就是const int *p。把一张便签用胶水死死粘在一个位置、但上面的内容可以擦写这就是int *const p。这样类比下来就不容易混了。2.2 数组指针和指针数组这又是C语言里的“左右使”陷阱。判断方法是先找到变量名看它先跟谁结合。[]下标操作符的优先级高于*解引用操作符所以char *words[3]; // words先和[3]结合它是一个数组 // 数组里每个元素是char*这是指针数组 int (*row)[4]; // 因为加了括号row先和*结合 // row是一个指针指向int[4]这种数组这是数组指针“指针数组”是数组里面存的是指针。“数组指针”是指针它指向一个数组。举两个实际场景。指针数组最常见的用法就是管理一组字符串也就是搜热词里那个“指针数组存放字符串”。C语言里没有像Python那样的字符串列表最朴素的做法就是用char *数组char *fruits[] {apple, banana, cherry, date}; for (int i 0; i 4; i) { printf(%s\n, fruits[i]); }这里的fruits数组每个元素是一个char *每个指针指向一块存着字符串的内存。这样管理字符串的好处是灵活字符串长短不一也不浪费空间。数组指针则多用于二维数组的操作。比如int matrix[3][4] {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}}; int (*p)[4] matrix; // p指向一个长度为4的int数组注意matrix在这里的类型是int (*)[4]不是int *。p 1跳过一整行也就是4个int。当你需要写一个函数去处理任意行数、但列数固定的二维数组时数组指针就很好用void print_matrix(int (*p)[4], int rows) { for (int i 0; i rows; i) { for (int j 0; j 4; j) { printf(%d , p[i][j]); } printf(\n); } }2.3 函数指针与指针函数这俩也是老冤家。先说结论指针函数是返回值为指针的函数比如int *getArray()函数指针是指向函数的指针比如int (*handler)(int, int)。函数指针才是真正值得深入理解的东西。它为什么存在因为函数本身是一段机器指令存放在内存的代码段也有起始地址。我们把这个地址存进一个指针变量就能在运行时动态决定“调用哪一个函数”。这种机制是回调函数、接口抽象的基础。#include stdio.h int add(int a, int b) { return a b; } int sub(int a, int b) { return a - b; } int main(void) { int (*funcPtr)(int, int); funcPtr add; printf(add: %d\n, funcPtr(10, 3)); funcPtr sub; printf(sub: %d\n, funcPtr(10, 3)); return 0; }函数指针最常见的应用是给排序函数写比较器。C标准库的qsort就用函数指针来接收“怎么比较两个元素”的规则你传一个比较函数进去qsort在内部不断调用它来做决定。这就是把“策略”作为参数传递。顺带提醒一句函数指针的语法阅读顺序要从内往外看int (*fn)(int)是fn是一个指针指向一个参数为int、返回int的函数。丢掉括号写成int *fn(int)就完全变味了。2.4 指针的指针和结构体指针int **pp这种“指针的指针”第一次看到时很多人都懵。其实无非就是pp里存的是一个指针变量的地址而那个指针变量里存的是普通变量的地址。理解起来并不难难的是知道它有什么用。最常见的用途有两个。第一个是函数内部要修改指针本身。比如你写一个函数想要把传入的指针指向新分配的内存如果只传int *p函数拿到的是p的副本改副本不影响外面。这时就得传int **p让函数能修改外层的指针变量。void allocate(int **p) { *p (int *)malloc(sizeof(int)); **p 100; } int main(void) { int *ptr NULL; allocate(ptr); printf(%d\n, *ptr); free(ptr); return 0; }链表插入节点时经常需要这种手法。当头指针可能为空、也要被更新的时候如果不传二级指针只靠返回值或者特殊处理代码会绕很多。第二个用途是遍历指针数组。比如char **argv就是指向指针数组首元素的一个指针遍历它就可以拿到所有命令行参数。很多框架里也大量使用二级指针来管理一组字符串。结构体指针相对好理解结构体是一组变量的集合结构体指针就是指向这个集合的指针。访问成员用-本质上是(*p).member的语法糖。struct Student { char name[32]; int score; }; void print_student(const struct Student *s) { printf(%s %d\n, s-name, s-score); }写链表、树这类数据结构时结构体指针是绝对主力。一个节点里存着指向下一个节点的指针靠这种“指针接力”不连续的内存也能被串成逻辑上的线性表。这也是指针最值钱的地方之一。3. 指针与数组、参数、算法结合3.1 数组名是“会退化的指针”很多C语言初学者会把数组和指针画上等号。严格说数组是一块连续内存数组名是一个代表这块内存起始地址的“常量地址值”。它不能在赋值语句里被改变所以不能arr。但数组名在绝大多数表达式中会退化成指向首元素的指针。比如int arr[5]直接写arr被当成int *使用。这时就能做arr i、*(arr i)这些操作。arr[i]本质上就是*(arr i)的语法糖。这里有一个细节特别值得记录arr和arr的值相同类型不同。arr的类型是int *指向首元素arr的类型是int (*)[5]指向整个数组。它们值一样但步长不一样。arr 1往后跳4个字节arr 1往后跳20个字节。这个坑在写二维数组和多维数组时最容易踩。多维数组访问也可以统一成指针视角。比如int a[2][3]a的类型是int (*)[3]a[0]是第一个“行”的数组名退化后是int *指向第一行第一个元素。a[i][j]就是*(*(a i) j)。理解这一点你看到“C多维数组指针”相关的代码就不会晕。所以记忆口诀是数组名是指针常量不是指针变量数组下标是移动指针后解引用。用指针访问数组没问题但别忘了数组本身还有“长度”这一层信息它不在指针类型里。3.2 指针数组存放字符串的两种思路用指针数组来管理字符串是C语言里的经典操作。但实际写的时候还要面临一个选择到底用char *strs[]还是用char strs[][N]// 方式一指针数组 char *messages[] { ok, error, unknown command }; // 方式二二维字符数组 char messages2[][16] { ok, error, unknown command };两种方式都能存一组字符串行为却有差别。方式一里每个指针指向一个字符串常量字符串本身散落在静态区指针数组只占指针大小的连续内存非常省空间适合字符串长短不一的场景。方式二里所有字符串被拷贝到连续的二维数组里每行固定16字节访问时局部性好适合字符串长度接近、且需要修改内容的场景。面试里经常看到这样的题让你把一组字符串排序。用指针数组实现排序时只需要交换指针不需要移动字符串内容效率高很多。这也是“指针跳跃”这个词的一种体现指针从一个字符串跳到另一个字符串代价极小。实际使用要注意一点用char *指向字符串常量时内容不能修改否则是未定义行为。如果你需要修改要么用char messages[][N]要么用malloc给每个指针分配可写内存。3.3 值传递、指针传递与C引用C语言里函数传参默认是“值传递”。这句话很多书上都写但真正理解它的人不多。值传递的意思是实参的值被复制一份给形参函数内操作的是一份副本无论如何修改形参都不会影响外面的实参。有人会问那为什么之前写数组函数时arr作为参数可以修改外部数组因为数组名退化成指针传进去的不是整个数组的副本而是数组首元素的地址。地址也是值但这个值的含义是“外部数据的位置”。通过地址去读写就是在操作外部数据。所以正确说法是C语言必须通过指针传递才能让被调函数修改调用方的变量。这也是“C两数交换指针”这类题目反复出现的原因。C里多了一个选择引用。引用是变量的别名底层本质上还是指针但语法上不需要取地址、解引用。它比指针安全也比指针难绕。一个典型的交换函数可以写成三种版本// C版本传指针 void swap_ptr(int *a, int *b) { int tmp *a; *a *b; *b tmp; } // C版本传引用 void swap_ref(int a, int b) { int tmp a; a b; b tmp; }调用方式上swap_ref(x, y)比swap_ptr(x, y)可读性好不少。引用还有一个好处它可以绑定到临时对象上做实现细节处理而且不会出现“空引用”因为引用必须初始化且绑定后不可改绑。相比之下指针可以为空、可以随处改灵活但危险。现代C推荐在不需要“可空”和“可重指向”的场景下优先用引用指针只留给确实需要“没有对象”这种状态的场合。3.4 两数交换的经典写法每个C/C学习者都会写一遍两数交换。别小看这个小程序它能过滤掉一大部分“以为懂了指针”的人。#include stdio.h void swap_bad(int a, int b) { int tmp a; a b; b tmp; } void swap_good(int *a, int *b) { int tmp *a; *a *b; *b tmp; } int main(void) { int x 3, y 5; swap_bad(x, y); printf(after swap_bad: x%d y%d\n, x, y); swap_good(x, y); printf(after swap_good: x%d y%d\n, x, y); return 0; }运行结果一目了然swap_bad什么都没改变因为x和y的值被复制了一份放进形参a和b函数内部交换的是副本swap_good传的是x和y的地址函数通过指针找到原始变量直接修改内存所以x和y真的互换了。我经常在学生代码里看到两种错。第一种是写了swap_good(int *a, int *b)函数体却写成int *tmp a; a b; b tmp;。这是在交换指针变量本身外面实参的地址没有变值也没变。第二种是只声明了指针没初始化就使用比如int *p; *p x;这种写法极其危险等于往一个未知的内存地址写值。写指针代码先画图再下手绝对能减少这种错误。3.5 双指针法不止算法题里用双指针在算法题里是老熟人了比如“双指针法”经常出现在有序数组、链表、滑动窗口相关的题目中。但在学习指针本身时它是一个很好的思维训练一个指向头、一个指向尾或者一个快一个慢通过控制两个指针的相对运动完成原本需要多层循环的工作。最经典的是有序数组两数之和给一个升序数组和一个目标值找出数组中两个数等于目标值。暴力法是两层循环O(n²)。双指针做法是左指针指向开头右指针指向结尾和为sum。如果sum小于目标左指针右移如果大于目标右指针左移等于目标就找到了。因为数组有序这一收一放不会错过答案复杂度降到O(n)。int two_sum(int *nums, int n, int target, int *idx1, int *idx2) { int left 0, right n - 1; while (left right) { int sum nums[left] nums[right]; if (sum target) { *idx1 left; *idx2 right; return 1; } else if (sum target) { left; } else { right--; } } return 0; }再看链表判环的快慢指针。慢指针每次走一步快指针每次走两步。如果链表有环快慢指针一定会在环里相遇如果没有环快指针会先走到末尾。这个问题用双指针能稳定解决不需要额外开哈希表。双指针的本质是“利用有序性或者位置关系减少重复遍历”。它不只是一个考试技巧在很多实际场景里也能派上用场。比如合并两个有序链表、字符串去重、滑动窗口最大值都用得上。所以我在学指针时会刻意写一写这类题目不光是练算法更是练“通过指针操纵位置”的肌肉记忆。4. 从裸指针到智能指针C的进化4.1 裸指针的心累必须先承认C语言的指针很强大但它把内存管理的责任完全交给了程序员。malloc了要freenew了要delete漏一次就内存泄漏多一次就重复释放。更要命的是如果程序在分配内存和释放内存之间抛了异常delete语句可能根本执行不到。我自己踩过的坑是这样的写一个资源管理函数里面先new一个对象然后调用某个可能抛异常的函数结果异常把控制流带走了资源就永远漏在那里。用裸指针写业务代码心智负担非常重你要时刻在脑中维护一张“谁拥有这块内存、什么时候释放”的表。所以C提出了RAII思想用对象来管理资源把资源的生命周期绑定到对象的生命周期。对象构造时获取资源对象析构时释放资源。这样不管正常返回还是异常跳转析构函数都会被执行资源一定能释放。智能指针就是RAII思想在内存管理上的具体实现。4.2 三种智能指针的取舍C11开始标准库提供了三种智能指针unique_ptr、shared_ptr、weak_ptr。它们解决的问题不一样选型也不一样。智能指针所有权语义是否可以拷贝典型场景unique_ptr独占所有权不能拷贝可以移动一个对象只有一个持有者比如工厂函数返回值shared_ptr共享所有权可以拷贝引用计数多个对象需要同时持有同一个资源weak_ptr不拥有所有权可以拷贝不增加计数打破shared_ptr循环引用观察资源是否还活着#include memory std::unique_ptrint p std::make_uniqueint(42); // std::unique_ptrint q p; // 错误不能拷贝 std::unique_ptrint q std::move(p); // 正确转移所有权 std::shared_ptrint s1 std::make_sharedint(10); std::shared_ptrint s2 s1; // 拷贝引用计数变成2 std::weak_ptrint w s1; // 不增加引用计数 if (auto sp w.lock()) { // 资源还活着sp是shared_ptr } else { // 资源已被释放 }unique_ptr是默认首选。它开销极小语义也最清晰独占地拥有资源没有拷贝只能转移。shared_ptr用引用计数管理共享所有权但要注意循环引用两个对象互相持有对方的shared_ptr计数永远不为0内存永远不释放。打破循环要靠weak_ptr它只观察资源、不持有资源真正需要的时候用lock()提升为shared_ptr。关于“智能指针实现”的底层原理很多人面试时也会被问到。核心其实就是引用计数每个shared_ptr内部保存一个指向堆上计数器use_count的指针。构造时计数器加一析构时减一减到0就把管理的对象和计数器一起释放。这个逻辑看着简单实现时却要仔细处理好线程安全、自增自减的原子性等问题。有些大学OJ上也会让手写一个智能指针练手测的就是有没有理解RAII和引用计数的本质而不是背代码。4.3 一个简易智能指针的设计思路既然聊到“智能指针实现”我在学习阶段也手写过一个迷你版本。虽然不能用于生产环境但能帮助理解原理。最简单的版本是模拟unique_ptrtemplate typename T class MyUniquePtr { public: explicit MyUniquePtr(T *p nullptr) : ptr(p) {} ~MyUniquePtr() { delete ptr; } MyUniquePtr(const MyUniquePtr ) delete; MyUniquePtr operator(const MyUniquePtr ) delete; MyUniquePtr(MyUniquePtr other) noexcept : ptr(other.ptr) { other.ptr nullptr; } MyUniquePtr operator(MyUniquePtr other) noexcept { if (this ! other) { delete ptr; ptr other.ptr; other.ptr nullptr; } return *this; } T operator*() const { return *ptr; } T *operator-() const { return ptr; } T *get() const { return ptr; } private: T *ptr; };几个关键点析构函数负责释放内存这是RAII的核心拷贝构造和拷贝赋值被delete因为独占所有权不允许复制移动构造和移动赋值把指针所有转移过去并把源指针置空避免重复释放operator*和operator-提供类似于裸指针的使用体验。更进一步的shared_ptr还要增加一个引用计数指针。核心思路是所有shared_ptr共享同一个计数对象拷贝时count析构时--count为0时同时释放数据和计数。写这个小demo最大的收获是理解了“智能”二字不是魔法而是靠“对象的生命周期管理内存的生命周期”这个朴素机制。5. 实战中常见的坑和排查实录5.1 空指针、野指针、悬空指针的区分写指针出问题大概率落在这三类上。我建议先分清它们才能对症下药。空指针值为NULL或nullptr的指针没有指向任何有效对象。解引用空指针会崩溃通常表现为段错误。别害怕空指针它是可以判断的if (ptr ! nullptr)就是防这种崩溃。野指针定义后没有初始化里面存的是一个随机地址。这比空指针更危险因为它看起来像是有效地址解引用时可能没有立刻崩溃但写的值不知道覆盖了哪里导致诡异的后台数据错乱。悬空指针原本指向一块合法内存但那块内存已经被释放了指针还保留着原来的地址。这就是典型的使用已释放内存。释放内存后最稳妥的习惯是把指针置空这样下次误用时还能被空指针检查拦下来。int *p (int *)malloc(sizeof(int)); *p 100; free(p); p NULL; // 释放后置空避免悬空指针为什么free之后要置空因为free只释放内存不会把指针变量本身清成NULL。指针里还残留着那个地址再被解引用时行为未定义有时碰巧读到垃圾数据有时直接崩溃。做项目时我要求自己做到“释放即置空”这是成本最低、收益最明显的防御习惯。5.2 文件指针与资源释放顺序C语言里文件操作用的FILE *也是一种指针。以我排查过的问题来看文件相关的指针问题主要集中在没有判断返回值、忘记关闭、关闭后继续使用。正确打开文件的习惯是先判断FILE *fp fopen(data.txt, r); if (fp NULL) { perror(open data.txt failed); return -1; } // 读文件... fclose(fp);这里有个顺序细节先处理业务最后关闭文件。不要在业务中间就fclose结果后面还想读又拿到一个无意义的指针。更常见的坑是提前return忘了关文件导致文件句柄泄漏。如果你在项目里写了很多这样提前返回的路径建议用一个统一的goto出口或者干脆用C的std::fstream、std::ifstream让析构函数自动关文件。同理的还有动态分配的内存。C语言中malloc/free、fopen/fclose都是成对出现的资源。每一对调用最好保持在同一个函数或层级里不要一个函数分配、另一个函数释放除非你能保证所有错误路径都会走到释放代码。C里对应的是构造与析构、new与delete但更推荐用智能指针和容器来管理减少手工资源操作。5.3 定时器执行查询时报空指针的排查实录这个话题在搜索引擎里很热timer执行查询时报空指针。我遇到过类似场景用Qt写定时器回调回调里有一个m_query对象指针结果在事件循环时崩溃日志报的正是访问空指针。这类问题的根源一般不在“定时器”本身而在于回调执行时它所依赖的对象已经被释放。比如定时器回调里抓了一个this指针但触发回调之前this所属对象已经被销毁或者用裸指针保存了一个查询对象对象被delete之后指针没置空。我当时的排查步骤可以整理成一套通用流程看崩溃栈找到空指针访问的那一行确认是哪个成员变量或局部指针为空。在访问前加一个空指针判断if (m_query) { m_query-execute(); }先确认崩溃是不是停在这里。打印日志记录回调执行的时间和对象地址配合对象构造析构的日志判断“对象是否已经被释放”。如果回调里需要持有外部对象优先用std::shared_ptr或者在回调捕获时用weak_ptr使用前lock()提升避免持有裸指针。// 不安全的写法 void MainWindow::onTimer() { m_query-execute(); // m_query 可能已经失效 } // 安全的写法 void MainWindow::onTimer() { if (!m_query) { return; } m_query-execute(); }实际上定时器回调和网络回调这类“延迟执行”的代码最容易踩生命周期问题。核心原则是谁拥有对象、能否确保回调执行时对象一定活着。如果无法保证就不要在回调里直接使用裸指针。工程上我一般用QPointer、weak_ptr这类可检测失效的机制从根上规避。5.4 调试指针问题的经验手艺指针出问题跟普通逻辑 bug 不太一样它经常是“偶发”“崩溃在别处”“数据被改坏”。排查能力没法速成但有一些老手艺特别管用。第一打印地址。printf(%p, (void*)p)能帮你确认指针是否指向预期的内存。不同的值暗示不同的问题打印出0x0说明是空指针打印出非常凌乱的地址大概率是野指针打印出已经被释放的内存地址可能是悬空指针。阶段性地打印地址配合程序逻辑能很快缩小范围。第二画内存图。我在纸上画格子每个格子代表一块内存然后用箭头表示指针。指针指向哪里、步长多少、是否越界画出来比盯着代码想一百遍都清楚。这个方法对新手来说效果堪比开挂。第三用好工具链。GCC/Clang 都支持-fsanitizeaddress编译时加上这个选项程序在访问越界、释放后使用、内存泄漏时都会直接报错并指出代码行号比裸奔调试省太多时间。Valgrind 在 Linux 下也是经典神器适合检测未初始化内存、内存泄漏等静态不容易发现的问题。gcc -g -fsanitizeaddress main.c -o app ./app第四assert防御。在关键操作前加上assert(ptr ! NULL)让问题早暴露。很多崩溃看似随机其实是因为某个前置条件在你没察觉的时候被破坏了assert能帮你在最早的时间点抓住它。真正要把指针学好光看文章没有用必须动手写。我自己的经验是单独写一些不依赖业务逻辑的小demo逐个验证各种指针的语法和语义然后故意制造空指针、野指针、越界观察程序怎么崩溃再想你该怎么避免。这种“有意的作死”比被动踩坑效率高得多。最后再聊几句指针学习的门槛其实是思维模式的门槛。从“变量是值”切换到“变量是地址”从“直接访问”切换到“间接访问”这个过程需要时间。我见过不少人一开始觉指针难但坚持画内存图、写小实验一两个月后就能很顺。说到底指针就是一个工具它不故意为难人只是要求你头脑里有一张清晰的“内存地图”。如果你要选一本参考书我推荐《C和指针》这本书。它把指针相关的概念讲得很细尤其是数组与指针的关系、函数指针、指针的高级应用比一般教材透彻得多。有条件的话配套做一些OJ练习把双指针、指针数组、智能指针都亲手敲一遍收获会比单纯看书大得多。最后分享一个小技巧每次分析一段指针代码都在纸上写出“变量类型、当前值、指向的地址、指向的类型”四栏再用箭头连起来。这个方法一开始费时间但坚持一段时间后你会发现指针代码在脑子里变成了一幅动态图很多原来觉得复杂的写法看起来都顺理成章了。