news 2026/9/30 18:28:31

读者写者问题全解析:PV操作、信号量与读写公平

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
读者写者问题全解析:PV操作、信号量与读写公平

1. 读者写者问题到底在解决什么矛盾

操作系统、进程、PV操作、读者写者问题,这四个词放在一起,基本就是进程同步这一章的分水岭。前面生产者消费者还算好理解,一进到读者写者,很多人就开始迷糊:为什么读者之间不用互斥,写者却要和所有人互斥,那个readcount到底在保护什么。我第一次学这块的时候,把代码背下来了,可换个问法就答不上来,后来自己动手写了一遍带日志的模拟程序,看着线程输出一行行刷过去,才算真正搞明白。

先把场景说清楚。有一块共享数据,比如一个配置文件、一张缓存表、一段内存里的字典,有一批进程要读它,另一批进程要改它。读操作不会破坏数据,改操作会。如果两个人同时读,谁都不影响谁,没有任何问题;如果一个人在读、另一个人在写,读到的可能是写了一半的残缺数据;如果两个人同时写,那更是彻底乱套。所以约束只有三条:多个读者可以同时进行,写者必须独占,写者进行时读者不能进。整个读者写者问题的代码,说到底都是在用PV操作表达这三条约束。

它适合谁来啃?如果你正在学操作系统课程、准备期末或者考研复试,这是必考题型;如果你在做后台开发,手里有读多写少的共享资源,比如配置热更新、本地缓存的刷新,这套模型就是现成的思路参考。哪怕你平时写的是 Java、Go 或者 Python,底层的信号量模型是通用的,理解了它,再去看ReentrantReadWriteLock这类现成工具,会有一种“原来里面就是这么干的”的感觉。

我打算按从业者做项目的思路来拆:先把PV操作和临界区的底层逻辑捋顺,再讲三种策略为什么存在、各自取舍在哪,然后手写从伪代码到能跑起来的完整实现,接着真的把程序跑起来观察饥饿现象,最后把踩过的坑和常见追问整理成速查表。全程不绕弯,代码能给的我尽量给全。

2. 把PV操作和临界区的地基打牢

2.1 P操作和V操作到底做了什么

P操作和V操作,本质是对一个信号量做原子加减。P操作(很多教材写成 wait 或者 down)的含义是:把信号量的值减一,如果减完之后小于零,当前进程就阻塞,进到该信号量的等待队列里排队;V操作(signal 或者 up)的含义是:把信号量加一,如果加完之后小于等于零,说明等待队列里还有进程,就唤醒其中一个。关键在于“原子”两个字,这一减一判一阻塞在硬件层面是不可中断的,否则多进程同时进来就会数错。

为什么非得是原子的?想象一个公共厕所只有一个坑位,信号量初值为1。两个人同时判断“现在是1,我可以进”,都减成0然后都进去了,这就是经典的竞态。PV操作就是把“判断有没有空位”和“占用空位”这两步捏成了一个不可分割的动作,操作系统用关中断、原子指令或者自旋锁来实现它。理解了这一点,你就能理解为什么信号量能当互斥锁用:初值设为1的信号量,P一次就锁住,V一次就解锁。

我习惯用一个生活类比记住它:P就是“取号”,号发完了就站着等;V就是“叫号”,有人办完业务就喊下一个。饭店里的取号机就是个信号量,号池是共享变量。读者写者问题里,我们会有好几个这样的取号机,每个管一件事,下面会一个个说清楚。

2.2 临界区、互斥与同步的区别

临界区指的是进程里访问共享资源的那段代码,必须保证同一时刻只有一个(或者一类)进程在里面。互斥讲的是“你不让我、我不让你”,同一时刻只能有一个进程进临界区,比如两个写者之间。同步讲的是“我等你、你等我”,进程之间要按某个先后顺序执行,比如写者写完之后读者才能读。这两个词经常被混着用,但读者写者问题里两种关系都存在,区分的意义就出来了。

在读者写者问题里,写者和写者之间是互斥,写者和读者之间是互斥,读者和读者之间没有约束,是并行的。如果把“读者优先”策略也算进来,还有一层同步味道的东西:当第一个读者进场时,它要先确认没有写者,这个确认动作和写者的封锁之间是要协调的。这就是为什么我们需要一个额外的计数器——单靠一把互斥锁,没法表达“第一个读者锁门、最后一个读者开门”这种语义。

打个比方,图书馆自习室,很多读者可以同时进,但图书管理员要进来整理书架时必须清场。读者之间的规则是:第一个人进来时把门锁上不让管理员进,后来的人直接进;最后一个人走时把门打开。这个“第一个人锁门、最后一个人开门”的记账工作,就落在一个计数器加一把小锁上,也就是后面代码里的readcount和rmutex。

2.3 为什么需要一个计数器

这是新手最容易卡的地方。读者之间不需要互斥,那为什么要用rmutex去保护readcount?因为readcount本身也是个共享变量,多个读者同时去readcount++,同样会发生竞态,导致计数不准,进而导致门该锁的时候没锁、该开的时候没开。所以rmutex不是用来锁“读操作”的,它是专门保护readcount这个变量的,是“锁的锁”。把它理解成会计用的那把专用小锁就对了,读写数据的大锁是wmutex。

看下面这段最经典的结构,先建立肌肉记忆:

// 读者部分的记账逻辑(伪代码) P(rmutex); // 锁住计数器 if (readcount == 0) // 我是第一个读者 P(wmutex); // 那就把写者的门锁上 readcount++; // 计数加一 V(rmutex); // 放开计数器 // ... 这里执行真正的读操作,可以和其他读者并行 ... P(rmutex); // 再次锁住计数器 readcount--; // 计数减一 if (readcount == 0) // 我是最后一个读者 V(wmutex); // 那就把写者的门打开 V(rmutex); // 放开计数器

这段代码只有十几行,但信息密度很高。if (readcount == 0)判断的是“当前我是不是第一个”,只有第一个读者才有资格去抢wmutex,后面的读者看到readcount > 0,就安心进场。退场时同理,只有最后一个读者负责释放wmutex。写者那边就简单粗暴:进门P(wmutex),出门V(wmutex),中间独占。掌握了这个骨架,剩下的三种策略都是在它上面做加法。

3. 三种策略:读者优先、写者优先与读写公平

3.1 为什么同一个问题会有三种解法

原始约束只说了读者之间可并发、写者独占,并没有规定“读者和写者同时竞争时,谁先拿到资源”。这个空白就留出了策略空间。如果系统里读操作远多于写操作,倾向让读者优先,吞吐会更高;如果写操作很关键、必须尽快落地,比如配置文件更新要立刻生效,那就让写者优先,避免写者被源源不断的读者饿死;如果还要防止任何一方被无限期拖延,那就得做成按到达顺序排队的读写公平策略。三种解法的核心约束完全一致,差别只在于谁能插队。

这跟现实里的排班很像。会议室的预约规则可以规定“在读的人继续读,来写的人在外面等”,也可以规定“一旦有人要写,后来的人一律排队”,前者读者爽,后者写者不至于饿死。选哪个没有对不对,只有合不合场景。面试的时候如果只答一种,往往会被追问“那写者会不会饿死”,提前把三种都想清楚,回答的层次就出来了。

3.2 读者优先的取舍

读者优先的实现就是上一节那个骨架,不做任何额外动作。它的特点是:只要有读者在读,新来的读者可以不断加入,写者只能一直等。写者的P(wmutex)会一直阻塞,直到所有读者都退场。极端情况下,如果读者流源源不断,写者可能永远拿不到锁,这就是所谓的写者饥饿。

写者饥饿在真实系统里是要警惕的。举个我遇到过的例子,服务端有个内存缓存在被高频读取,同时有个定时任务要刷新它。如果刷新逻辑走的是写者角色,而读请求量一直很大,刷新可能被无限推迟,缓存就一直停留在旧数据上去了。所以读者优先适合那些“写很少发生,偶尔延迟也能接受”的场景,一旦写操作对时效有要求,就得换成下面两种。

3.3 写者优先与读写公平的实现思路

写者优先的核心是增加一道“拦截闸”。思路是再引入一个信号量,让第一个到来的写者把这道闸拉下来,之后来的读者都必须在这道闸后面排队,等写者走得差不多了再放行。具体做法是用一个writecount计数器统计正在等待或正在写的写者数量,第一个写者进来时锁住读者通道,最后一个写者离开时打开它。

读写公平就更进一步:用一个排队信号量让所有进程先按到达顺序取号,谁先到谁先过,读和写都不能插队。它的代价是牺牲了一点并发度——哪怕连续来了十个读者,只要中间夹了一个写者,后面的读者就得等写者做完再一起进。但在需要绝对公平、不想任何一方饿死的场景里,这个代价是划算的。

下面用一张表把三种策略放在一起对比,参数一目了然:

策略额外信号量读者并发写者饥饿读者饥饿适用场景
读者优先无完全并发可能发生不会读远多于写,写延迟可接受
写者优先r_block+wcount_mutex写者到来后受阻不会极端下可能写关键、必须尽快落地
读写公平queue按序并发不会不会双方都不能被无限拖延

看到这张表你会明白,加信号量不是为了炫技,每一个新增的闸门都对应用户提出的一个新要求。选型的时候先问清楚业务更怕哪一方被饿死,答案基本就出来了。

4. 手写完整实现:从伪代码到跑得起来的程序

4.1 读者优先版本的完整代码

先把最基础的一版写全,用的是 POSIX 信号量,方便在 Linux 上直接编译运行。我习惯把信号量全初始化为 1,因为我们要用它做互斥锁。这里创建了三个读者线程和两个写者线程,读者睡短一点,写者睡长一点,方便观察现象。

#include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <semaphore.h> #include <unistd.h> sem_t rmutex; // 保护 readcount 的计数器锁 sem_t wmutex; // 写者互斥锁,也是读者要争取的大锁 int readcount = 0; // 当前正在读的读者数量 void* reader(void* arg) { int id = *(int*)arg; while (1) { sem_wait(&rmutex); if (readcount == 0) sem_wait(&wmutex); // 第一个读者锁门 readcount++; sem_post(&rmutex); printf("[读者 %d] 开始读取, 在线读者=%d\n", id, readcount); usleep(200000); // 模拟读耗时 printf("[读者 %d] 读取结束, 在线读者=%d\n", id, readcount); sem_wait(&rmutex); readcount--; if (readcount == 0) sem_post(&wmutex); // 最后一个读者开门 sem_post(&rmutex); sleep(1); } return NULL; } void* writer(void* arg) { int id = *(int*)arg; while (1) { sem_wait(&wmutex); printf("[写者 %d] ===== 开始写入 =====\n", id); usleep(300000); printf("[写者 %d] ===== 写入完成 =====\n", id); sem_post(&wmutex); sleep(1); } return NULL; } int main() { sem_init(&rmutex, 0, 1); sem_init(&wmutex, 0, 1); pthread_t r[3], w[2]; int rid[3] = {1, 2, 3}; int wid[2] = {1, 2}; for (int i = 0; i < 3; i++) pthread_create(&r[i], NULL, reader, &rid[i]); for (int i = 0; i < 2; i++) pthread_create(&w[i], NULL, writer, &wid[i]); for (int i = 0; i < 3; i++) pthread_join(r[i], NULL); for (int i = 0; i < 2; i++) pthread_join(w[i], NULL); sem_destroy(&rmutex); sem_destroy(&wmutex); return 0; }

编译命令是gcc reader_first.c -o rf -lpthread,跑起来就能看到输出。这里有个细节要提醒:sem_wait对应 P 操作,sem_post对应 V 操作,千万别写反了。P 是“减并等待”,V 是“加并唤醒”,方向反了整个程序要么锁死要么形同虚设。我第一次写的时候把sem_wait和sem_post名字搞混,导致写者根本没被拦住,输出乱成一团,debug 了半小时才发现是名字记串了。

4.2 写者优先版本的代码与关键差异

写者优先要加两个信号量:r_block当作拦截读者的闸门,wcount_mutex保护writecount计数器。writecount记录当前有多少个写者想进来,第一个写者进来时把r_block用 P 操作拉下,后续读者就会卡在r_block上;最后一个写者离开时用 V 操作抬起闸门。代码在读者优先的基础上改写如下:

sem_t rmutex, wmutex, r_block, wcount_mutex; int readcount = 0, writecount = 0; void* reader(void* arg) { int id = *(int*)arg; while (1) { sem_wait(&r_block); // 若写者已拉开闸门,读者在此排队 sem_wait(&rmutex); if (readcount == 0) sem_wait(&wmutex); readcount++; sem_post(&rmutex); sem_post(&r_block); // 允许后续读者继续进闸 printf("[读者 %d] 开始读取, 在线读者=%d\n", id, readcount); usleep(200000); printf("[读者 %d] 读取结束, 在线读者=%d\n", id, readcount); sem_wait(&rmutex); readcount--; if (readcount == 0) sem_post(&wmutex); sem_post(&rmutex); sleep(1); } return NULL; } void* writer(void* arg) { int id = *(int*)arg; while (1) { sem_wait(&wcount_mutex); writecount++; if (writecount == 1) sem_wait(&r_block); // 第一个写者拉闸,拦住新读者 sem_post(&wcount_mutex); sem_wait(&wmutex); // 写者之间仍需互斥 printf("[写者 %d] ===== 开始写入 =====\n", id); usleep(300000); printf("[写者 %d] ===== 写入完成 =====\n", id); sem_post(&wmutex); sem_wait(&wcount_mutex); writecount--; if (writecount == 0) sem_post(&r_block); // 最后一个写者抬闸,放行读者 sem_post(&wcount_mutex); sleep(1); } return NULL; }

关键差异就在那两处writecount == 1和writecount == 0的判断上,它们把“第一个写者拉闸、最后一个写者抬闸”这件事表达出来了。运行之后你会发现,写者一出现,后面来的读者就得在r_block上等着,不会再无限制地插队,写者饥饿的问题基本解决了。要注意r_block初值必须为 1,否则读者一开始就全被拦在外面,程序直接卡死。

4.3 用 Python 快速验证读写公平策略

如果手边没有 C 环境,或者只是想快速看现象,用 Python 的threading写一版验证脚本更省事。这里实现的是读写公平版,多了一个queue信号量让所有进程按到达顺序取号。Python 的Semaphore就是现成的信号量,acquire对应 P,release对应 V,语义完全一致。

import threading import time queue = threading.Semaphore(1) # 排队信号量,保证先到先服务 rmutex = threading.Semaphore(1) # 保护 readcount wmutex = threading.Semaphore(1) # 写者互斥 readcount = 0 lock = threading.Lock() def reader(rid): global readcount while True: queue.acquire() # 先取号排队 with lock: if readcount == 0: wmutex.acquire() # 第一个读者锁门 readcount += 1 queue.release() # 取完号立刻放行,让后面的人能继续取 print(f"[读者 {rid}] 开始读取, 在线读者={readcount}", flush=True) time.sleep(0.2) print(f"[读者 {rid}] 读取结束", flush=True) with lock: readcount -= 1 if readcount == 0: wmutex.release() # 最后一个读者开门 time.sleep(1) def writer(wid): while True: queue.acquire() # 同样先取号 wmutex.acquire() # 独占 queue.release() print(f"[写者 {wid}] ===== 开始写入 =====", flush=True) time.sleep(0.3) print(f"[写者 {wid}] ===== 写入完成 =====", flush=True) wmutex.release() time.sleep(1) for i in range(1, 4): threading.Thread(target=reader, args=(i,), daemon=True).start() for i in range(1, 3): threading.Thread(target=writer, args=(i,), daemon=True).start() time.sleep(10)

这段脚本是我平时给学生演示用的,因为输出带时间顺序,一眼能看出谁在等谁。有个细节值得注意:queue.release()的位置很讲究。读者在持有queue的同时去判断和更新readcount,更新完立马释放queue,这样连续到来的读者才不会互相阻塞,同时又保留了“谁先取号谁先执行记账”的顺序。如果把queue.release()拖到读操作之前甚至之后,并发度会掉下来,效果就大打折扣。

5. 跑起来之后暴露的真实问题与排查技巧

5.1 用日志验证现象:写者饥饿不是理论

代码写对了,重点还在于验证。我的做法是在输出里加上时间戳和进程标识,让“谁在等、等了多久”看得见。跑读者优先版本的时候,会明显看到写者的“开始写入”被压到很后面,前面读者一行接一行不断刷,写者就是排不上。这套验证方式比任何纸上推导都有说服力,毕竟饥饿这种事,看一眼日志就懂了。

反直觉的是,饥饿现象有时只在高并发下才明显。如果你把读者线程数量设得很小、间隔又很长,写者很快就能抢到锁,反而看不出问题。所以测试的时候要刻意制造持续的读者流:多开几个读者线程、缩短读者的休眠时间,让读者几乎不间断地进入,这时写者的等待才会被拉长。验证一个同步程序,最忌讳的就是随便跑一下没报错就认为没问题——没报错不代表没饥饿,只是压力不够。

5.2 P操作顺序写反会出什么后果

很多 bug 都出在 PV 操作的顺序上。拿读者退场那段来说,一定是先readcount--,再判断是否为零,最后才V(wmutex),并且这些动作都要在P(rmutex)和V(rmutex)的保护范围内。如果把V(rmutex)提前到readcount--之前,计数器的更新就失去了保护,多个读者同时退场时可能都读到刚减完但还没判断的中间值,导致该开门的没开或者重复开门。

还有一个高频错误:写者忘记在P(wmutex)之后、V(wmutex)之前执行真正的写操作,直接把V挨着P写了,结果锁住又马上放开,形同虚设。我建议写完代码后对着“谁保护谁、谁先谁后”逐行念一遍,尤其检查每个共享变量是不是都在对应的锁里被访问。readcount和writecount这两个计数器是重灾区,因为大家容易只盯着数据本身,忘了计数器也是被共享的。

5.3 死锁和误唤醒的排查思路

写者优先版本里有一个容易翻车的点:r_block和wmutex的获取顺序。写者在持有wcount_mutex的时候去P(r_block),读者在持有r_block之后去P(rmutex)再P(wmutex),只要每条路径上的获取顺序保持一致,就不会形成环,也就不会死锁。一旦你在某处调换了顺序,两个线程各持一把锁互相等,程序就挂住了。排查死锁最简单的办法是gdb挂上去看每个线程卡在哪个sem_wait,栈里会写得明明白白。

另外要提防“惊群”式的误唤醒理解偏差。V操作唤醒的是等待队列里的一个进程,被唤醒的进程会重新去竞争信号量,并不保证它立刻就能拿到资源,可能又被别人抢走。这不叫 bug,是信号量的正常语义。理解这一点,你就不会因为看到日志里某个进程被唤醒后又等了一会而怀疑代码写错了。

下面把常见问题整理成一张速查表,方便对照定位:

现象可能原因排查与修正
写者长时间无法进入读者优先策略固有缺陷换写者优先或公平策略
程序直接卡死信号量初值设错,或P操作顺序成环检查初值是否为1,统一获取顺序
同时进入多个写者忘记用 wmutex 包裹写操作确保写操作在 P(wmutex)/V(wmutex) 之间
读者计数异常readcount 未受 rmutex 保护所有对计数器的读写都放进锁内
并发度极低queue 释放位置过早/过晚记账完成立即释放 queue

6. 面试与考试里绕不开的高频追问

6.1 为什么读者之间不需要互斥

这个问题几乎逢考必问。答案的核心是“读操作不改变共享数据”。既然读不会破坏数据,两个读者同时读,读到的都是完整的内容,结果等价于串行读取,就没有必要限制。反过来说,写操作会改变数据,如果和读并发,读者可能读到一个改到一半的中间状态,这种“脏读”破坏了正确性,所以必须互斥。把这条判断标准记住:判断要不要互斥,看这个操作会不会让别的操作观察到不一致的中间状态。

再深一层,这也是性能考量。如果强行让读者之间也互斥,那和直接把整块数据锁死没有区别,读多写少的场景下吞吐会惨不忍睹。读者写者问题的全部价值,就体现在“把可以并发的部分放开,把必须互斥的部分锁死”这个取舍上。能把这个取舍讲清楚,比背代码更能体现你真的理解了。

6.2 公平策略真的做到公平了吗

严格说,“公平”是相对的。上面用queue实现的版本保证了进程按到达顺序取号,但取号之后,多个读者仍然可以并发进场,所以实际的执行顺序并不是一个一个串行。真正的公平在读写场景里往往指“不会出现某一方被无限期拖延”,而不是“严格的先后次序”。如果业务要求绝对的先到先执行,那就退化成给整个数据加一把大锁,读写都串行,代价是并发度归零。

实际工程中还有更细的变体,比如给写者设置优先级阈值、限制读者连续进入的次数上限等等。这些花样的本质都是在“吞吐”和“公平”之间找平衡点。面试时如果能把“公平的定义本身就取决于业务目标”这句话说出来,通常会加分,因为它体现的是工程判断,而不是死记硬背。

6.3 从信号量到现成锁的迁移

真正写业务代码的时候,多数人不会手写信号量,而是用语言自带的读写锁。比如 Java 的ReentrantReadWriteLock默认是非公平的,构造时可以传入true开启公平模式,行为和上面讲的公平策略很接近;Go 的sync.RWMutex内部用了读计数和写优先的设计。理解了底层的 PV 操作模型,再看这些 API 的文档,你会发现它们讨论的“公平性”“写者优先”全都能对应上。

我在做配置中心缓存刷新的时候,就吃过默认非公平锁的亏——高并发读把写刷新一直往后拖,后来换成公平模式,刷新延迟才稳定下来。这个经历的启发是:底层模型看着离业务很远,但它决定了你在选锁时的默认参数该不该动。读者写者问题不是一道孤立的考题,它是你理解所有读写锁行为的一把钥匙。

7. 我个人在这道题上踩过的坑与一点经验

第一次真正动手实现,我把加法当成了验证。代码跑通、输出没乱,就以为万事大吉,结果没有制造压力,根本没看到写者饥饿。后来又因为sem_wait和sem_post写反,让整个互斥失效,输出交错得像没加锁一样。踩过几次之后我学乖了:验证同步程序,一要看日志顺序,二要放大压力,三要逐行核对每个共享变量是否都在锁内被访问,这三条缺一条都可能漏掉 bug。

我现在教别人的时候,会让他先把读者优先版本默写三遍,再自己推导写者优先该加哪几个信号量。默写是为了形成肌肉记忆,推导是为了理解“为什么加”。等你闭着眼睛能把readcount == 0那一对判断写出来,再往后看公平策略、读写锁源码,基本上就是一马平川了。这道题的价值从来不在于那几行代码,而在于它训练你用最小的工具去表达并发约束的能力,这种能力换个语言、换个业务场景都用得上。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/30 18:28:15

TensorFlow深度学习实战:环境配置、模型训练与部署避坑指南

在深度学习框架这块&#xff0c;TensorFlow 绝对是绕不开的名字。不管你是刚入门准备跑个图像分类&#xff0c;还是已经在搞大规模分布式训练&#xff0c;甚至是想把模型部署到手机端&#xff0c;你都会撞上它。这篇东西我打算换个角度来写——不给你念文档&#xff0c;而是结合…

作者头像 李华
网站建设 2026/9/30 18:27:13

OpenSpec:让配置文件成为可执行契约的规格驱动实践

1. OpenSpec 不是又一个 YAML 验证器&#xff0c;而是规格即契约的工程实践起点OpenSpec 这个名字最近在开发者社区里出现的频率明显高了——不是因为某家大厂突然开源&#xff0c;也不是某个明星项目背书&#xff0c;而是越来越多团队在重构 API 网关、设计微服务间通信协议、…

作者头像 李华
网站建设 2026/9/30 18:24:43

Java包与IDEA目录结构:从package声明到报错排查

刚接触Java那阵子&#xff0c;我最怕听到一句话&#xff1a;"你这类放错包了。"当时我脑子里的"包"就是一堆下载下来的jar文件&#xff0c;跟代码顶上那行package声明完全对不上号&#xff0c;可老师上课、同事沟通都用"包"这一个字&#xff0c;…

作者头像 李华
网站建设 2026/9/30 18:24:33

宝可梦精灵设计教科书:从剪影识别到进化叙事的完整方法论

1. 从“教科书级别”说起&#xff1a;宝可梦精灵设计到底强在哪 第一次看到“宝可梦教科书级别的设计&#xff0c;精灵学习的教科书”这个说法&#xff0c;我脑子里蹦出来的不是某一代作品&#xff0c;而是整整一套延续了二十多年的设计方法论。很多人聊宝可梦&#xff0c;聊的…

作者头像 李华
网站建设 2026/9/30 18:20:51

链表初始化用LinkList还是LinkList *L?C语言值传递与二级指针深度解析

链表初始化到底该用LinkList还是LinkList *L&#xff1f;这个问题困扰了我很长一段时间。说实话&#xff0c;当年我学C语言的时候&#xff0c;在链表初始化上踩过的坑&#xff0c;比后面工作中所有bug加起来都多。明明照着书上敲的代码&#xff0c;一运行就崩溃&#xff1b;加了…

作者头像 李华