用C语言实现贪吃蛇游戏过程中,定义蛇身节点是用到了结构体和指针。

1
2
3
4
5
6
// 蛇身节点结构体,用于存储每个蛇身段的位置坐标
struct Node {
int x; // x坐标
int y; // y坐标
struct Node* next; // 指向下一个节点的指针
};

在函数中定义蛇身(头部)节点时,我第一个想法是这样定义 struct Node *head,但实际上完整的写法是 struct Node *head = (struct Node*)malloc(sizeof(struct Node));。那么就要探究这两个写法的不同。

1 head = (struct Node*)malloc(sizeof(struct Node));

这段 C 语言代码的核心作用是在堆内存中为自定义结构体struct Node分配一块内存,并将这块内存的起始地址赋值给指针变量head。下面分模块拆解说明:

1.1 前置知识:struct Node(自定义结构体)

代码中隐含了一个提前定义的结构体类型struct Node(否则编译器会报错),例如链表节点的典型定义:

1
2
3
4
5
// 自定义结构体:表示链表节点
struct Node {
int data; // 数据域:存储节点数据
struct Node *next; // 指针域:指向下一个节点
};

struct Node是一种用户自定义的数据类型,占用的内存大小由其成员变量决定(比如上面的定义占 sizeof(int) + sizeof(struct Node*) 字节)。

1.2 核心函数:malloc(sizeof(struct Node))

(1)malloc 是什么?

malloc 是 C 标准库的动态内存分配函数(声明在 <stdlib.h>头文件中),功能是: 从堆内存(而非栈内存)中分配一块连续的、指定字节数的内存; 返回值:分配成功则返回内存块的起始地址(类型为 void*,无类型指针);分配失败(如内存不足)则返回 NULL。 #### (2)sizeof(struct Node):计算内存分配大小 sizeof 是 C 的运算符,用于计算某一类型 / 变量占用的字节数。sizeof(struct Node)会精准计算出一个struct Node结构体实例所需的字节数,确保malloc分配的内存刚好能容纳该结构体(避免内存不足或浪费)。

1.3 类型转换:(struct Node*)

malloc 的返回值是 void*(无类型指针),而headstruct Node*类型的指针(结构体指针)。(struct Node*) 是强制类型转换,将malloc返回的void*转换为struct Node*类型,目的是:

  • 让编译器明确这块内存的用途(存储struct Node类型的数据),避免类型不匹配的编译警告(尤其是严格编译模式下);
  • 注意:C 语言中void*可以隐式转换为其他指针类型(因此有人会省略强制转换,写成 head = malloc(sizeof(struct Node))),但显式转换更清晰,也兼容 C++(C++ 要求必须显式转换)。

1.4 赋值操作:head = ...

head 是一个提前声明的struct Node*类型指针变量(即 struct Node *head;),赋值后:

  • head 指向堆内存中刚分配的struct Node结构体实例;
  • 后续可通过head指针操作这块内存(比如 head->data = 10; 给结构体成员赋值)。

1.5 完整代码示例(带错误检查和内存释放)

这段代码单独写存在风险(未检查malloc返回值、未释放内存),完整的规范写法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <stdio.h>
#include <stdlib.h> // 必须包含malloc/free/exit的头文件

// 定义结构体
struct Node {
int data;
struct Node *next;
};

int main() {
struct Node *head; // 声明结构体指针

// 动态分配内存(核心代码)
head = (struct Node*)malloc(sizeof(struct Node));

// 关键:检查malloc是否分配成功(避免NULL指针解引用崩溃)
if (head == NULL) {
perror("malloc failed"); // 打印错误原因
exit(EXIT_FAILURE); // 退出程序
}

// 使用分配的内存:给结构体成员赋值
head->data = 10;
head->next = NULL;
printf("Node data: %d\n", head->data); // 输出:Node data: 10

// 释放内存:堆内存需手动释放,否则造成内存泄漏
free(head);
head = NULL; // 置空指针,避免野指针(free后原地址仍可访问,但内存已失效)

return 0;
}

1.6. 关键注意事项

  • 必须包含头文件:使用malloc必须包含 <stdlib.h>,否则编译器会报 “implicit declaration of function ‘malloc’” 错误;
  • 必须检查返回值:malloc可能分配失败(返回NULL),直接解引用NULL指针会导致程序崩溃(段错误);
  • 手动释放内存:堆内存不会像栈内存那样自动释放,使用完必须调用free(head),否则会造成内存泄漏;
  • 避免野指针:free(head)后,head仍指向原内存地址(但内存已被系统回收),需手动置为NULL。

2 struct Node* head;

首先要分清两个概念:指针变量本身 和 指针指向的内存(数据本体) ——struct Node* head; 只创建了 “指针变量”,但没有创建 “节点数据本体”;而 malloc 才是为 “节点数据本体” 分配内存,让指针有可操作的有效地址。

2.1 先看:只写 struct Node* head; 会发生什么?

这段代码的本质是声明了一个指针变量,但未给它绑定任何有效的内存空间:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>

struct Node {
int data;
struct Node *next;
};

int main() {
struct Node* head; // 仅声明指针变量
// 尝试操作指针指向的内存:直接崩溃!
head->data = 10; // 段错误(Segmentation fault)
return 0;
}

关键原因:

指针变量本身存在,但指向随机地址: head 作为局部变量,会在栈内存中分配一小块空间(比如 64 位系统占 8 字节),用于存储 “内存地址”;但未初始化时,这个地址是随机的垃圾值(即 “野指针”)。

解引用野指针 = 操作非法内存: head->data = 10 试图往这个随机地址写入数据,而这个地址大概率不属于当前程序的内存空间,操作系统会直接终止程序(段错误)—— 这是 C 语言中典型的 “未定义行为”。

2.2 如果想 “直接定义” 出可用的节点,有两种写法,但都有局限

你可能误以为 “直接定义” 是指 “不写 malloc”,其实真正能直接定义的是结构体实例(数据本体),而非仅定义指针:

写法 1:定义栈上的结构体实例,再让指针指向它

1
2
3
struct Node node; // 直接定义栈上的节点本体(数据存在)
struct Node* head = &node; // 指针指向栈上的本体
head->data = 10; // 此时可用,因为指针指向了有效内存

但这种写法的核心局限是:栈内存的生命周期仅限于当前作用域。比如跨函数使用时,问题就暴露了:

1
2
3
4
5
6
7
8
9
10
11
12
// 错误示例:返回栈上的指针
struct Node* createNode() {
struct Node node; // 栈上的节点,函数结束后立即销毁
node.data = 10;
return &node; // 返回栈地址,函数结束后该地址失效
}

int main() {
struct Node* head = createNode();
printf("%d", head->data); // 未定义行为:可能输出乱码/崩溃
return 0;
}

栈内存由系统自动管理,函数执行完毕后,栈上的node会被销毁,返回的指针就成了 “野指针”,后续操作完全不可靠。

写法 2:全局 / 静态结构体实例(更不推荐)

1
2
struct Node node; // 全局变量,内存在全局区(非栈/堆)
struct Node* head = &node;

全局变量的生命周期是整个程序,但缺点致命:

  • 内存一直占用,无法手动释放;
  • 全局变量会导致代码耦合度高、线程不安全,完全失去 “动态创建 / 销毁” 的灵活性。

2.3 malloc 的核心价值:为 “节点本体” 分配堆内存

malloc(sizeof(struct Node)) 的作用是在堆内存中创建 “节点数据本体”,再让指针指向这个本体:

1
struct Node* head = (struct Node*)malloc(sizeof(struct Node));

2.4 总结:为什么不能 “只定义指针”?

  1. struct Node* head; 只是 “空指针壳”:指针本身是个 “地址容器”,但容器里装的是随机地址,没有对应的 “节点数据本体”,根本无法操作;
  2. 直接定义栈上的节点(struct Node node;)有生命周期限制:仅适合局部临时使用,无法作为动态数据结构(如链表)的节点(链表需要节点长期存在、跨函数访问);
  3. malloc才是动态数据结构的刚需:链表、树等结构的节点数量是运行时动态变化的(比如随时增删节点),堆内存的 “手动管理生命周期” 特性,是实现这类结构的核心基础。
⬆︎TOP