CSCI2520 虽然不要求你只能用 C 作答,但课堂确实经常用 C 去解释资料结构。原 因很直接:pointer、heap allocation 与 data layout 可以把资料结构在内存 里的行为直接展示出来。
这一节不是一般程序语言教程,而是为了让你后面读 stack、queue、hash table 实现时,不会因为看不懂 C 而失去整个概念。
为什么资料结构课要先复习这些工具
本地 tutorial 材料强调:考试与书面作业并不限制你一定使用某种语言。但 lecture 与 tutorial 仍然用 C 去说明:
- pointer 实际保存什么;
- 节点怎样用
malloc建立; struct怎样把字段组织成一个 record;typedef怎样协助 ADT 接口隐藏底层表示。
如果这几样读错,问题不只是 syntax,而是你会看不见资料结构究竟怎样在内存 里运作。
Pointer 保存的是地址,不是值
定义
Pointer
Pointer 变量保存的是某个对象的地址,而不是该对象本身的值。
Tutorial 1 特别区分两个符号:
&x表示“x的地址”;- 表示“pointer
p所指向地址中的值”。
这两个概念一定不能混淆。Pointer 不是 object 本身,而是 object 所在位置 的记录。
例题
逐行读一个 pointer trace
考虑 tutorial 里的例子:
int firstvalue = 5, secondvalue = 15;
int *p1, *p2;
p1 = &firstvalue;
p2 = &secondvalue;
*p1 = 10;
*p2 = *p1;
p1 = p2;
*p1 = 20;
应该逐行理解:
p1先指向firstvalue,p2指向secondvalue;- 把
firstvalue改成10; - 把
10复制到secondvalue; - 并不是复制整数,而是把
p1重新指向secondvalue; - 现在就会改变
secondvalue。
最后得到:
边读边试
追踪一条 pointer 状态序列
这个 tracer 让你改动初始整数,再逐步重播 pointer tutorial 的状态变化。
步骤 1
int firstvalue = ...; int secondvalue = ...; int *p1, *p2;firstvalue = 5
secondvalue = 15
p1 指向 unassigned
p2 指向 unassigned
两个整数已存在,但两个 pointer 仍未持有合法地址。
读 pointer trace 时,最好一直把两件事分开:
- 每个 pointer 现在指向哪里?
- 那个地址里的值现在是多少?
Dereference 是经地址去读或写
当 pointer 已经持有合法地址之后,dereference 才有意义。
int x = 7;
int *p = &x;
*p = 12;
最后 x 会变成 12。 不是建立一个新整数,而是经 p 所记录
的地址,直接写入原来的对象。
常见错误
把 pointer 赋值与经 pointer 赋值混淆
会改变 p 保存的地址。
会把 q 指向位置中的值复制到 p 指向的位置。
两句看起来很像,但作用完全不同。
malloc 在 heap 上分配空间
资料结构实现经常需要动态建立节点,而不是预先知道总共要几格。这个时候
就要靠 malloc。
定义
用 malloc 做 heap allocation
malloc(n) 会向 runtime 请求 n bytes 的空间,并返回该区块起始位置的
pointer。
典型写法是:
struct node *x;
x = malloc(sizeof(struct node));
现在 x 就指向一段足以容纳一个 struct node 的新空间。
但要记住两件事:
malloc只会给你空间,不会自动填好字段;- 真实程序最终仍然要处理
free与 ownership 问题。
例题
为什么 linked structure 离不开 malloc
假设 stack 用 linked list 表示。每次 push,都可能要建立一个新节点。因
为节点数量事前未知,固定 local variable 根本不够,必须靠 malloc 逐个
向 heap 申请新节点。
typedef 让接口更易读
Tutorial 也复习 typedef,因为 ADT 接口经常要用简短名称去遮蔽又长又底层
的 pointer type。
typedef struct node *nodePtr;
typedef int stackElementT;
typedef 不会建立新的 runtime 对象;它只会建立新的类型名称。好处是:
- function prototype 更短;
- header file 更容易扫读;
- ADT 边界更清楚。
当课程写 stackADT 时,这个名字本身就是 abstraction 的一部分。它要求
client 把这个对象理解为“一个 stack”,而不是“某个具体 struct 的 pointer”。
struct 把相关字段组成一个 record
定义
Structure
struct 可以把多个字段组成同一个 record type。
例如:
struct node {
int data;
struct node *next;
};
这就是 linked-list node 的典型形状:一个字段放 payload,一个字段记录下 一个 node 的地址。
重点不只是语法上的分组,而是它让你能精确建模资料结构需要维持的状态。
例题
Queue node 怎样支持 queue 操作
若 queue 使用 linked node,常见写法是
struct cellT {
queueElementT element;
struct cellT *next;
};
而 queue 对象本身通常还会保存:
headpointertailpointer- 可能还有长度字段
所以 enqueue、dequeue 与 QueueLength() 等 ADT 操作,实际上就是在更
新少量 pointer 字段。
这些工具怎样直接连到 ADT 设计
课堂的 C 语言 review 与 ADT lecture 不是两条分开的线。
- Pointer 让一个 object 可以指向另一个 object;
malloc让节点可以按操作需要动态建立;struct让多个相关字段可以被当成一个整体维护;typedef让 ADT 接口可以把实现细节藏起来。
所以当课程说 ADT 要暴露 “what” 而隐藏 “how” 时,上面几样工具正是 C 里实现这种分离的方法。
常见错误
常见错误
未初始化的 pointer 不是合法对象
写 只表示建立一个 pointer 变量,并不表示它已经指向安全空间。 在赋值之前就去 dereference,属于未定义行为。
常见错误
malloc 不会替你建好节点内容
malloc 只提供 raw storage。节点字段仍然要由你自己逐一初始化。
常见错误
两个 pointer 可以看到同一个 object
若两个 pointer 指向同一块内存,经其中一个 pointer 写入数据,另一个 pointer 之后读到的也会是更新后的结果。
快速检查
快速检查
与 有什么分别?
用地址与值来回答。
解答
答案
一个完整的 memory 流程
lecture 里的 rationalADT 例子和 tutorial 里的 student 例子,其实都在
讲同一件事:如果你在 heap 上开了空间,就要知道 object 之后怎样被使用,
以及什么时候要收回来。
可以把整个流程记成四步:
- 先
malloc一块空间; - 再用 field assignment 或
fscanf初始化; - 使用期间保持 pointer 有效;
- 用完之后
free,如果还有 file,也要fclose。
这个流程不只是程序风格,而是资料结构程序最重要的安全习惯。当你以后看到 stack node、queue node、或者 hashtable cell,都应该马上问:这个 object 从 哪里来?又要去哪里?
快速检查
为什么 malloc(sizeof(struct node)) 比手写 byte 数更可靠?
想想结构字段如果之后变化会发生什么。
解答
答案
一个更完整的 struct trace
student 例子值得再慢读一次,因为它同时牵涉 pointer、struct、file input 和 ownership。
先看这几行:
Sdata *p;
p = (Sdata *)malloc(sizeof(Sdata));
FILE *fp = fopen("example.txt", "r");
fscanf(fp, "%s %d %s", p->name, &p->age, p->address);
可以把 trace 拆成四个问题:
p指向哪里?- 和 是写入哪个 buffer?
&p->age为什么要加 address?- 读完之后谁负责清理?
当你可以清楚答出这四条时,说明你已经开始把 pointer、field access 和 resource ownership 放进同一个框架里理解,而不是逐句死记。
练习
快速检查
追踪这段 code 的最后结果:int x=1,y=2; int *p=&x; int *q=&y; *p=*q; q=p; *q=9;
先分清楚值复制与地址重定向。
解答
引导解答
快速检查
解释为什么 linked-list node 几乎一定要有指向下一个 node 的 pointer。
把答案连到 traversal 与动态增长。
解答