线性表 ADT 里 *L / L / *e / e 到底怎么记?——一次把 C 语言“值传递/指针传递”讲透(学习日志)
最近在学《数据结构》线性表(SqList)时,我被教材里 ADT 操作的写法搞得很乱:
有时候写 ListInsert(*L, i, e),有时候又是 ListLength(L)
有时候 GetElem(L, i, *e),有时候 LocateElem(L, e)
在 union(并集)示例代码里又出现 ListLength(*La)、GetElem(Lb, i, &e)、ListInsert(La, ++La_len, e)
这导致我一直在纠结:
什么时候要加 *?什么时候不加?调用时到底要传 L 还是 &L?e 到底是本体还是指针?
这篇日志就是把这些疑惑一次性梳理干净。
1. 我最开始的核心困惑:是不是“要改动就传指针,不改动就传值”?
我一开始的理解是:
ListLength(*La) 用 *La 是因为 La 会改变
Lb_len = ListLength(Lb) 不用指针是因为 Lb 不变
GetElem(Lb, i, &e) 用 &e 是因为要给 e 赋值
所以“容易混淆的地方”就是:要被改动就传指针,不改动就传参数?
这个结论方向是对的,但我后来发现还缺了一个关键点:
有时候出现 *La / La 并不是因为“改不改”,而是因为“函数形参类型需要什么”,你手里拿的变量类型又是什么。
换句话说:
你到底该写 * 还是 &,最终要看“函数原型”和“你当前变量的类型”。
2. ADT 里写的 *L,并不是让你调用时写 *L
教材 ADT 操作经常写:
ListInsert(*L, i, e)
ListDelete(*L, i, *e)
ListLength(L)
GetElem(L, i, *e)
我以为:既然 ADT 写了 *L,那我调用 ListInsert 时也应该写 *La。
但这是误解。
正确理解:ADT 里的 *L 是一种“标记”
它在告诉你:
这个操作会修改线性表 L,所以实现时要用“指针参数”来传表。
也就是实际 C 代码的函数原型更像这样:
C
Status ListInsert(SqList *L, int i, ElemType e);
Status ListDelete(SqList *L, int i, ElemType *e);
所以:
ADT 的 *L ≈ “会修改 L”(应当传地址)
不是在告诉你“调用时要写星号”
3. 为什么示例代码里 ListInsert(La, …) 前面没有 *?
示例代码里的并集函数:
C
void unionL(SqList *La, SqList Lb)
{
…
ListInsert(La, ++La_len, e);
}
我疑惑:书上不是写 ListInsert(*L, i, e) 吗?这里为什么是 ListInsert(La, …) 而不是 ListInsert(*La, …)?
原因:La 本身已经是指针了
unionL 的形参写的是:
C
SqList *La
所以 La 的类型就是 SqList*(地址)。
而 ListInsert 需要的也是 SqList*:
C
Status ListInsert(SqList *L, int i, ElemType e);
因此调用时应当直接传:
C
ListInsert(La, …);
如果你写成 ListInsert(*La, …),那传进去的是 SqList 本体(结构体值),类型反而不匹配(除非函数原型不同)。
4. 我又问:L 是线性表,为什么不能直接代表地址?
我曾经觉得:
“L 既然是一个线性表,那我写 L 也能代表它的地址吧?为什么还要 &L?”
关键点:结构体变量不会“自动退化”为指针
在 C 里:
SqList L; 是一个结构体变量(本体)
L 不是地址
&L 才是地址
我把“数组名能当地址”的特例套到了结构体上。
数组的特例(容易误导人)
C
int a[10];
很多场景下 a 会退化为 &a[0],所以你会感觉 a 就像地址。
但结构体不这样:
复制代码
C
SqList L;
L 绝不会自动变成 &L。
5. 于是我开始想要一个“记忆法”:为什么 L 有时带星号,有时不带?e 有时带星号,有时不带?
这是我最后最想解决的问题。
终极判断原则(强烈推荐背下来)
函数里要改动谁,就传谁的地址(指针)。
只读谁,就按值传。
但要更精确一点:
*L(ADT 里)表示:操作会修改线性表
L 表示:只读线性表
*e(ADT 里)表示:e 是输出参数,要把结果写回给调用者
e 表示:e 只是输入值
6. 把所有 operation 按“是否修改/是否输出”分类(最不容易混)
✅ 修改线性表 L(所以 ADT 写 *L,实现用 SqList *L)
InitList(*L):初始化会改 L
ClearList(*L):清空会改 L
ListInsert(*L, i, e):插入会改长度/内容
ListDelete(*L, i, *e):删除会改表(还会输出被删元素)
✅ 不修改线性表 L(所以 ADT 写 L)
ListEmpty(L):只判断不改
ListLength(L):只求长度不改
LocateElem(L, e):只查找不改
GetElem(L, i, *e):表不改,但要输出元素值
7. e 到底什么时候要 *e?
这是我最容易搞混的一点。最终我用一句话就能判断:
如果这个函数要“把一个 ElemType 的值带回给你”,它就需要 *e(输出参数)。
因此:
输入参数:e
LocateElem(L, e)(拿 e 去比较)
ListInsert(*L, i, e)(把 e 插进去)
输出参数:*e
GetElem(L, i, *e)(把元素写到 e 里)
ListDelete(*L, i, *e)(把被删元素写到 e 里)
8. 调用时到底写不写 & / *?——看你手里变量的类型
情况 A:你手里是本体 SqList L;
如果操作需要“修改表”(需要指针),你就传 &L:
C
SqList L;
ElemType x;
ListInsert(&L, 1, x);
GetElem(L, 1, &x);
情况 B:你手里已经是指针 SqList *La;
那就直接传指针本身:
C
SqList *La;
ElemType x;
ListInsert(La, 1, x);
GetElem(*La, 1, &x); // 注意:如果 GetElem 形参是 SqList L(本体),就需要 *La
这里顺带解释了示例代码里的一个细节:
La_len = ListLength(*La);
因为 ListLength(L) 可能定义成接收 本体,而 La 是 指针,所以要 *La 来匹配类型。
这再次说明:
出现 *La 有时是“类型匹配”而不是“改不改”。
9. 我还问过:ElemType e 那句“声明与 La 和 Lb 相同的数据元素 e”到底是什么意思?本体还是指针?
在 union 示例里常见:
C
ElemType e;
GetElem(Lb, i, &e);
我当时疑惑:“e 既然要传 &e,那 e 是不是指针?”
结论:ElemType e; 的 e 是本体(值变量)
判断方法极简单:看声明。
ElemType e; ✅ e 是本体(一个值)
ElemType *e; ✅ e 才是指针
GetElem(…, &e) 只是把 e 的地址传进去让函数写值而已。
也就是:
e:装元素值的“盒子”
&e:告诉函数“盒子在哪里”,让它把值放进去
教材里那句话“与 La 和 Lb 相同的数据元素 e”,说的是:
e 的类型要和线性表里存放的元素类型一致(即 ElemType),这样才能接住从表里取出的元素。
它不是说 e 和 La/Lb 的类型相同,更不是说 e 是 La/Lb 的指针。
10. 最终记忆口诀(我现在就靠这个不再混)
我给自己总结了四句口诀:
改表传地址:ADT 写 *L(实现用 SqList *L)
取值回传地址:ADT 写 *e(实现用 ElemType *e)
只读就不带星:L
只是输入值就不带星:e
再配一个“调用规则”:
我手里是本体 L → 要指针就写 &L
我手里已是指针 La → 直接传 La
输出参数一律传 &变量
11. 一个极简小抄(贴在笔记本第一页)
假设函数原型风格是常见教材版:
C
Status GetElem(SqList L, int i, ElemType *e);
int LocateElem(SqList L, ElemType e);
Status ListInsert(SqList *L, int i, ElemType e);
Status ListDelete(SqList *L, int i, ElemType *e);
int ListLength(SqList L);
那么调用时就按:
插入:ListInsert(&L, i, e)
删除:ListDelete(&L, i, &e)
取值:GetElem(L, i, &e)
查找:LocateElem(L, e)
长度:ListLength(L)
如果你在函数里拿到的是 SqList *La,那把 &L 换成 La 即可。
总结
我最开始纠结的所有“星号问题”,归根结底都来自两件事:
ADT 写法里的 *L / *e 是“会被修改/输出”的标记,不等于调用时直接写星号。
C 里到底写 * 还是 &,最终要看:函数形参类型需要什么 + 我手里变量类型是什么。
现在我用“改谁就传谁地址 + 看原型匹配”这两条,基本不会再混了。

