用C语言实现贪吃蛇游戏过程中,定义蛇身节点是用到了结构体和指针。
// 蛇身节点结构体,用于存储每个蛇身段的位置坐标
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(否则编译器会报错),例如链表节点的典型定义:
// 自定义结构体:表示链表节点
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*(无类型指针),而head是struct 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返回值、未释放内存),完整的规范写法如下:
#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; 会发生什么?
这段代码的本质是声明了一个指针变量,但未给它绑定任何有效的内存空间:
#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:定义栈上的结构体实例,再让指针指向它
struct Node node; // 直接定义栈上的节点本体(数据存在)
struct Node* head = &node; // 指针指向栈上的本体
head->data = 10; // 此时可用,因为指针指向了有效内存
但这种写法的核心局限是:栈内存的生命周期仅限于当前作用域。比如跨函数使用时,问题就暴露了:
// 错误示例:返回栈上的指针
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:全局 / 静态结构体实例(更不推荐)
struct Node node; // 全局变量,内存在全局区(非栈/堆)
struct Node* head = &node;
全局变量的生命周期是整个程序,但缺点致命:
- 内存一直占用,无法手动释放;
- 全局变量会导致代码耦合度高、线程不安全,完全失去 “动态创建 / 销毁” 的灵活性。
2.3 malloc 的核心价值:为 “节点本体” 分配堆内存
malloc(sizeof(struct Node)) 的作用是在堆内存中创建 “节点数据本体”,再让指针指向这个本体:
struct Node* head = (struct Node*)malloc(sizeof(struct Node));
2.4 总结:为什么不能 “只定义指针”?
struct Node* head;只是 “空指针壳”:指针本身是个 “地址容器”,但容器里装的是随机地址,没有对应的 “节点数据本体”,根本无法操作;- 直接定义栈上的节点(
struct Node node;)有生命周期限制:仅适合局部临时使用,无法作为动态数据结构(如链表)的节点(链表需要节点长期存在、跨函数访问); malloc才是动态数据结构的刚需:链表、树等结构的节点数量是运行时动态变化的(比如随时增删节点),堆内存的 “手动管理生命周期” 特性,是实现这类结构的核心基础。