在编程世界里,有一道公认的“劝退分水岭”:指针。无数人学C语言学到指针就停下,也有无数人在某个深夜突然开窍——而帮他们开窍的,往往就是mycodeschool频道的这条《Pointers in C》。这条十几年前的教学视频至今仍被各技术社区在“哪条视频让你开窍”的讨论里反复置顶。本文不只复述它的讲解,而是把它背后的完整脉络补齐:从1945年的存储程序思想,到C标准的条文细节,再到Tony Hoare那句著名的“十亿美元错误”,最后看现代语言如何重新回答指针提出的问题。没看过视频,你能看懂;看过视频,你会多懂一层。
| 项目 | 内容 |
|---|---|
| 视频标题 | Pointers in C(中文直译:C语言指针) |
| 频道 | mycodeschool(以数据结构与C语言教学著称的工程师频道) |
| 发布时间 | 2013年前后(据频道页面,约值) |
| 时长 | 十几分钟量级(约值) |
| 播放量 | 数百万次量级(据频道公开数据约数) |
需要说明:这是一条多年前的经典教学视频,本文撰写时未能联网复核精确数据,上表均为约值。选取它,是因为直到今天,它仍是中文外文社区里被引用最多的指针入门讲解之一——一条十几分钟的旧视频能在一个不断更新换代的领域里站住十几年,本身就说明它讲对了东西。
这条视频最聪明的地方,是它坚决不肯从语法开始。它开篇就让你想象:内存是一长条格子,每个格子有一个编号,这个编号就是地址。变量是什么?变量是“某个格子里放着的一个值”,外加一个方便人类使用的名字。名字是给人看的,地址是机器真正操作的。
在这个模型上,指针的两个运算符根本不需要死记:&是“这个格子编号是多少”,*是“去这个编号的格子里取东西”。一个问地址,一个按地址取货,两者天然互逆——*(&x)还是x本身。很多教材把这两个符号讲成需要背诵的“语法”,这条视频讲成两个动作,这是它能让人开窍的核心原因。
int *p视频没有回避C语言声明语法的别扭之处,而这恰恰是多数初学者卡住的地方。为什么星号有时黏着类型(int* p),有时黏着变量(int *p)?根源是C语言“声明跟随使用”的设计哲学:int *p读作“*p是一个int”——也就是说,星号跟着变量名写,是在声明“对p做解引用之后,得到的是一个整数”。p本身只是指针,*p这个表达式才是那个int。
理解了这条设计逻辑,著名的坑也就一目了然:int* a, b;里,只有a是指针,b是个普通的int。星号在语法上属于声明符,不属于类型名。这不是C语言的bug,是它的历史选择——下文的溯源部分会讲这个选择从哪来。
视频用一个几乎每个C课程都会用的例子把指针的价值立起来:swap函数。先看失败版本——写一个swap(int a, int b),函数内部交换得好好的,回到main里一看,两个变量纹丝不动。原因是C的传参永远是值拷贝:函数拿到的是两份复印件,你在复印件上怎么涂改,原件都不会变。
想要函数有能力改到调用者的变量,就不能传复印件,要传原件的地址。swap(int *a, int *b),调用时swap(&x, &y)——函数顺着地址找到原件动手。这就是“传值”与“传址”的本质区别:不是两种语法,是两种权力。传值给函数看,传址给函数改。
视频后半段把指针和数组接在一起,这是整条课的升华点:a[i]和*(a+i)是同一个东西。数组名在表达式里会退化为指向首元素的指针,所谓“取第i个元素”,就是“从首地址出发,往后跳i个位置再取货”。而指针算术的“加1”,加的不是1个字节,而是1个“该类型的大小”——int指针加1跳4字节,double指针加1跳8字节。类型信息决定了步长。
这个恒等式还藏着一个经常被拿来炫技的事实:既然a[i]就是*(a+i),加法又满足交换律,那么2[a]在C语言里是合法的,等价于a[2]。它毫无实用价值,但完美证明了下标语法底下没有任何神秘东西,只有地址运算。
视频在收尾处点到指针的另一面:空指针、野指针、越界解引用。你获得了直接操作内存的权力,也就获得了直接破坏内存的权力。解引用一个没初始化的指针,等于拿着一个随机编号去开格子——开出来的可能是别人的数据,而写进去,就是事故。视频在这里只是点到为止,本文的批判性审查部分会把这条线补全。
编址内存的起源:把内存看成一排编号格子,这个模型来自1945年冯·诺依曼起草的《First Draft of a Report on the EDVAC》。这份报告确立了“存储程序计算机”的基本结构:指令和数据放在同一种可编址的存储器里。“地址”从此成为计算机构造的一等概念,指针不过是这个概念在编程语言里的投影。你以为在学C语言的语法,其实是在摸1945年定下的机器骨架。
C语言的家族史:指针运算的这套语义,沿着CPL→BCPL→B→C的路线传承。Dennis Ritchie在1993年的《The Development of the C Language》一文中明确记载,“声明跟随使用”的声明语法是从B语言继承的设计——int *p读作“*p是int”正是这套哲学的直接后果。1978年Kernighan与Ritchie的《The C Programming Language》第一版出版,把这套语义固定成了一代人的共同语言。语法的别扭不是随意为之,是历史的沉积岩。
标准条文里的精确性:视频里“数组名就是指针”的说法,在C标准里其实有更精确的表述。C11标准草案N1570的6.3.2.1节规定:数组类型的表达式在大多数场合会退化为指向首元素的指针,但有两个例外——sizeof的 operand,和取地址&的operand。所以sizeof(arr)给出的是整个数组的字节数而不是一个指针的大小,&arr的类型是“指向整个数组的指针”而不是“指向指针的指针”。同理,6.5.3.2节规定取地址的操作数必须是左值。教学视频的简化没有错,但知道标准在哪两条上留了例外,你才解释得了实际工程里那些诡异的编译错误。
十亿美元的错误:指针体系里最著名的反思来自Tony Hoare——图灵奖得主、快速排序的发明人。他在2009年QCon的演讲《Null References: The Billion Dollar Mistake》里公开检讨:1965年他在ALGOL W里引入空引用,是“我犯下的十亿美元错误”,因为语言从此允许一个指针“什么也不指”,而这个可能性渗透进了此后几乎所有主流语言的类型系统,化身为无数运行时空指针崩溃。视频里那句轻描淡写的“指针可能为空”,背后是一场持续半个世纪的类型系统论战——Rust的Option、Swift的Optional、Kotlin的可空类型标注,都是对Hoare检讨的迟到回应。
教学法的谱系:用“格子+箭头”的图画讲指针,不是这条视频的独创。斯坦福的Nick Parlante在1990年代制作的动画短片《Pointer Fun with Binky》(收录于其CS Education Library,编号104),用一个小机器人演示指针、解引用和空指针崩溃,至今仍是全球许多课堂的开场教具。mycodeschool这条视频可以看作Binky路线的加强版:同样的画图法,配上了真实代码和调试器演示。好的指针教学法三十年没变过,因为内存模型三十年也没变过。
站得住的部分:视频对内存模型、&与*的互逆关系、传值传址的swap对照、指针算术按类型步长伸缩的讲解,全部与C标准语义一致,可以逐条上机验证。作为入门材料,它没有引入任何“先学错、以后再推翻”的概念——这在指针教学里难得。
要打折扣的部分:
第一,“指针就是地址”这个等式是教学简化。更准确的说法是:指针=一个地址+一份类型解释。同样的地址,经int指针解引用读4字节,经double指针读8字节;void*只有地址没有解释,所以不能直接解引用。把指针完全等价于裸地址,后面遇到对齐、严格别名这类问题就会失去坐标系。
第二,“数组名就是指针”同样不精确。数组名在表达式里退化成指针,但它本身不是指针对象——sizeof和取地址两条例外就是证据。这个简化在入门阶段无害,在做sizeof类计算时是经典事故来源。
第三,社区流传的“螺旋法则”“顺时针读声明法”都是民间口诀,标准条文里并不存在。口诀可以应急,不能当语义——视频没有教这些口诀,反而是它严谨的地方。
第四,时代局限。视频成于静态分析工具普及之前,今天用AddressSanitizer(-fsanitize=address)、Valgrind、各类linter,绝大多数悬垂指针和越界访问能在开发期被精确报出。“指针=危险”应该修正为“指针错误在过去难以定位,今天已有成熟的抓捕手段”——危险的从来不是指针,而是没被工具看管的内存操作。
第五,视野局限。视频讲的是C的指针,但没有提这场游戏在全球范围内的重新洗牌:Java干脆砍掉指针运算只留引用;Go保留指针但禁止算术;Rust用所有权系统把指针纪律变成编译期检查;连最新的C标准(C23)都补上了nullptr关键字。指针没有消失,它被各个语言用不同姿势重新关进了笼子。学完这条视频,值得把这张地图也铺开看一眼。
1. 遇到指针bug,先画图,再看代码。准备一张纸,横着一排格子写地址,箭头表示指向。99%的指针疑难——改不到值、段错误、莫名的数据覆盖——画完图就现形。动手层面:printf("%p", &x)打印地址,gdb里x/4xw 地址直接查看内存内容。把“地址”从一个抽象概念变成你亲眼看到的数字,指针就祛魅了。
2. 亲手制造一次案发现场。写一个函数返回局部变量的地址,接收它并打印——你已经制造了一个悬垂指针。然后加-fsanitize=address重新编译运行,看工具如何精确报出“栈上已释放内存被使用”。半小时的实验,能把“听说指针危险”变成“见过案发现场、知道刑侦手段”——后者才是工程能力。
3. 把指针思维迁移到所有语言。指针的核心问题其实只有两句:这是值还是引用?谁拥有这块内存?带着这两句去读任何语言的代码:Java的参数传递为什么改不动基本类型、Python的列表为什么“原地就变了”、Go的map为什么不用传指针,全部豁然开朗。做代码评审和API设计时,这两个问题就是可变性与拷贝成本的边界谈判——这正是指针教给你的、超越C语言的通用功课。
flowchart TD
A[内存是一排编号格子] --> B[变量等于值加名字]
B --> C[取地址与解引用互逆]
C --> D[声明跟随使用<br/>int指针的星号属于变量]
D --> E[函数参数永远是复印件]
E --> F[想改原件就传地址]
F --> G[数组名退化为首元素指针]
G --> H[下标只是地址加法]
H --> I[类型决定指针步长]
I --> J[权力另一面是事故<br/>空指针与野指针]
J --> K[现代工具抓捕悬垂引用]
J --> L[新语言重新立规矩<br/>引用与所有权]
本文由「51学通信」技术团队出品。51学通信由通信行业18年老兵爱卫生(微信ID: gprshome201101)维护,聚焦5G/6G核心网、3GPP标准解读、通信前沿技术深度分析。 出版作品有《5G核心网原理与实践》《IMS原理与实践》等4本通信专业书籍。知识星球「51学通信」提供深度技术答疑和行业洞察。
[!tip] 51学通信知识星球 星主:爱卫生(微信:gprshome201101)——公众号「51学通信」创办者,19 年通信行业运维与讲师经验,著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书(各大电商平台有售)。 星球定位:深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域,主打运维岗位与信令分析。 当前规模:5200 篇主题 / 990 个回答,198 元/年。 加入福利:加入星球后请加爱卫生微信,拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题,另有群内分享。 AI 加持:51学通信知识星球已作为 skill 接入 WorkBuddy,使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例,做总结、找文档和 PPT 等场景全覆盖。
