结构体和链表
1. 结构体
1.1 结构体概述
- 目前所学数据类型仅有基本数据类型 short int long,float double,char 。
- 以上类型无法用于描述较为复杂的数据情况。例如: Person 人,无法使用单一类型完成对于当前 Person 的信息描述,本身 Person 信息具备一定【复合程度】,包括 ID 姓名,性别,年龄,信息。。。。
通过一定的手段将数据进行整合用于描述一个完整的数据类型,在 C 语言中采用的方式就是【 struct 结构体方式】,结构体是面向对象的雏形。
1.2 C语言结构体定义格式
使用结构体定义关键字: struct
struct 结构体数据类型名称
{
结构体成员变量1【数据类型 变量名】;
结构体成员变量2【数据类型 变量名】;
结构体成员变量3【数据类型 变量名】;
结构体成员变量4【数据类型 变量名】;
};
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 自定义一个结构体类型,采用多种不同的基本数据类型
// 整合为一个结构体类型,整个结构体类型的完整名称 struct person
struct person
{
int id;
char name[32];
short age;
char info[64];
float salary;
};
/*
类型定义 typedef,可以给予数据类型重新定义名称
size_t ==> typedef unsigned long size_t
typedef struct student {..} Student;
Student 就是当前结构体的别名,可以直接使用,并且操作更为方便
*/
typedef struct student
{
int id;
char name[32];
char class_name[16];
int age;
float score;
} Student;
int main(int argc, char const *argv[])
{
/*
struct person 是完整的数据类型 p1 是结构体变量
*/
struct person p1;
/*
Student ==> typedef struct student 可以认为 Student 和 struct student 完全一致。
Student 是一个数据类型
*/
Student stu1;
return 0;
}
1.3 结构体变量定义和初始化操作
typedef struct student
{
int id;
char name[32];
short age;
float score;
} Student;
int main(int argc, char const *argv[])
{
/*
定义结构体变量
*/
Student stu;
/*
【结构体变量】操作结构体内部数据,需要通过 . 运算符
. ==> 的
*/
// 赋值操作
stu.id = 23;
// 使用 C 语言字符串拷贝函数
strcpy(stu.name, "James");
stu.age = 39;
stu.score = 25.8F;
// 取值操作
printf("ID : %d\n", stu.id);
printf("Name : %s\n", stu.name);
printf("Age : %d\n", stu.age);
printf("Score : %f\n", stu.score);
return 0;
}
1.4 结构体类型堆区定义
typedef struct student
{
int id;
char name[32];
short age;
float score;
char class_name[32];
} Student;
int main(int argc, char const *argv[])
{
/*
malloc 申请空间不会对空间进行擦除操作,为了防止野值问题,可以通过 memset 函数对申请的空间进行空间擦除。
memset(void * ptr, char ch, size_t size);
用户提供目标空间首地址,同时限制字节个数,将空间中的所有数据替换为目标字符 ch
memset(stu1, 0, sizeof(Student));
将 Student 对应的堆区空间进行擦除操作,使用的是【字符集编号为 0 的字符】该字符二进制数据为 0000 0000,相当于整个申请的内存空间都是 0
*/
Student *stu1 = (Student *)malloc(sizeof(Student));
memset(stu1, 0, sizeof(Student));
Student *stu2 = (Student *)calloc(1, sizeof(Student));
/*
【结构体指针】想要操作结构体的数据内容需要使用 -> 运算符
-> ===> 的
*/
stu1->id = 1;
strcpy(stu1->name, "Tom");
stu1->age = 5;
stu1->score = 100.0F;
strcpy(stu1->class_name, "IOT-2303");
stu2->id = 2;
strcpy(stu2->name, "Jerry");
stu2->age = 3;
stu2->score = 100000.0F;
strcpy(stu2->class_name, "IOT-2303");
// 取值操作
printf("ID : %d\n", stu1->id);
printf("Name : %s\n", stu1->name);
printf("Age : %d\n", stu1->age);
printf("Score : %f\n", stu1->score);
printf("ClassName : %s\n", stu1->class_name);
printf("\n");
printf("ID : %d\n", stu2->id);
printf("Name : %s\n", stu2->name);
printf("Age : %d\n", stu2->age);
printf("Score : %f\n", stu2->score);
printf("ClassName : %s\n", stu2->class_name);
free(stu2);
free(stu1);
stu1 = NULL;
stu2 = NULL;
}
1.5 结构体数组【重点】
typedef struct student
{
int id;
char name[32];
short age;
} Student;
int main(int argc, char const *argv[])
{
// 定义了一个数组,数组中存储的元素都是 Student * 结构体类型指针
// Student * student_array[10] = {NULL}
Student ** student_array = (Student **)calloc(10, sizeof(Student *));
for (int i = 0; i < 10; i++)
{
// 申请内存堆区空间,数据类型强转为 Student *
Student * stu = (Student *)malloc(sizeof(Student));
// Student * stu = (Student *)calloc(1, sizeof(Student));
memset(stu, 0, sizeof(Student));
// 赋值必要数据
stu->id = i + 1;
stu->age = i + 5;
strcpy(stu->name, "Tom");
// 数组中存储目标 Student 结构体指针
student_array[i] = stu;
}
// 循环展示数据
for (int i = 0; i < 10; i++)
{
printf("ID : %d, Name : %s, age : %d\n",
student_array[i]->id,
student_array[i]->name,
student_array[i]->age);
}
// 循环释放 malloc 申请的内存空间
for (int i = 0; i < 10; i++)
{
free(student_array[i]);
student_array[i] = NULL;
}
free(student_array);
student_array = NULL;
return 0;
}
1.6 结构体嵌套
结构体作为另一个结构体的组成部分
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct birthday
{
char year[8];
char month[4];
char day[4];
} Birthday;
typedef struct person
{
int id;
char name[32];
// Person 结构体中使用的自定义结构体类型 Birthday 作为成员变量
Birthday birthday;
} Person;
int main(int argc, char const *argv[])
{
Person * p = (Person *)calloc(1, sizeof(Person));
p->id = 1;
strcpy(p->name, "James");
// 通过 Person 赋值内部 Birthday 结构体数据
strcpy(p->birthday.year, "1985");
strcpy(p->birthday.month, "12");
strcpy(p->birthday.day, "30");
printf("ID : %d\n", p->id);
printf("Name : %s\n", p->name);
printf("Year : %s\n", p->birthday.year);
printf("Month : %s\n", p->birthday.month);
printf("Day : %s\n", p->birthday.day);
free(p);
p = NULL;
return 0;
}
2. 链表【重点】
2.1 概述
非连续存储空间的数据结构,常用的链表形式
- 有头双向链表【功能最完整】
- 无头双向链表
- 有头单向链表
- 无头单向链表
- 循环链表
3. 有头双向链表
3.1 有头双向链表结构格式
结点 Node:
存储目标数据,具备连接前一个和后一个结点的能力
头结点 Head:
存储当前链表中第一个结点地址和最后一个结点地址,同时存储当前链表中的有效元素个数
头结点:
数据类型名称 Linked_Head
// 头结点定义,数据类型重命名为 LinkedHead
typedef struct head
{
// 结点 Node 指针类型,存储链表中第一个 Node 结点地址
struct node * first;
// 结点 Node 指针类型,存储链表中最后一个 Node 结点地址
struct node * last;
// 当前双向链表中有效结点个数
int size;
} LinkedHead;
结点 :
数据存储节点,数据类型名称 Node,当前结点存储数据为 Student 类型
typedef struct node
{
// 结点 Node 指针类型,当前 Node 结点存储上一个结点的首地址
struct node * prev;
// 结点 Node 指针类型,当前 Node 结点存储下一个结点的首地址
struct node * next;
// Node 结点中存储的数据内容,当前 Node 存储 Student 类型,存储数据为指针类型
Student * stu;
} Node;
3.2 有头双向链表图例
3.3 有头双向链表相关代码实现代码实现
3.3.1 流程概述
链表结构体声明
创建新的链表头结点函数:
- 无参有返回值
- 函数返回值为 LinkedList 类型头结点的首地址
创建新的学生结构体:
- 有参有返回值
- 参数为学生相关信息。函数返回值为 Student 类型的学生结构体首地址
创建新结点函数:
- 有参有返回值
- 参数为 Student 类型的学生结构体首地址。函数返回值为 Node 类型的新结点首地址
尾插函数(添加数据到链表尾部):
有两种情况
当链表中没有结点时
head->first = new_node; head->last = new_node;
当链表中有结点时
new_node->prev = head->last; head->last->next = new_node; head->last = new_node;
有参有返回值
参数为头结点首地址和新结点首地址。函数返回值 int 类型, 添加成功,返回
EXIT_SUCCESS
告知函数代码成功执行添加操作, 否则返回EXIT_FAILURE
头插函数(添加数据到链表头部):
有两种情况
当链表中没有结点时
head->first = new_node; head->last = new_node;
当链表中有结点时
new_node->next = head->first; head->first->prev = new_node; head->first = new_node;
有参有返回值
参数为头结点首地址和新结点首地址。函数返回值 int 类型, 添加成功,返回
EXIT_SUCCESS
告知函数代码成功执行添加操作, 否则返回EXIT_FAILURE
辅助函数:
- 找指定下标函数
- 找用户指定的下标地址
- 有参有返回值
- 参数为头结点首地址和用户指定的 int 类型的下标。函数返回值为 Node 类型的目标结点地址,如果没有找到,返回 NULL
- 结点释放函数
- 释放删除操作中需要进行内存释放的结点,需要释放的是 Student 类型空间和被删除的结点 Node 类型的空间。
- 有参有返回值
- 参数为被删除的结点首地址。 函数返回值 int 类型, 释放成功,返回
EXIT_SUCCESS
告知函数代码成功执行释放操作, 否则返回EXIT_FAILURE
指定下标地址添加函数:
有三种情况
当指定插入位置
index == 0
调用头插函数当指定插入位置
index == head->size
调用尾插函数指定插入位置不是以上两种情况时,首先利用找指定下标函数,找到指定下标对应的结点首地址 target
new_node->prev = target->prev; new_node->next = target; target->prev->next = new_node; target->prev = new_node;
有参有返回值函数
参数为头结点首地址,指定下标和新结点首地址。函数返回值 int 类型, 添加成功,返回
EXIT_SUCCESS
告知函数代码成功执行添加操作, 否则返回EXIT_FAILURE
尾删函数:
有两种情况
当前链表里没有一个结点,直接终止函数运行
当前链表中有结点时,先用临时变量 temp 存储被删除结点地址
Node * temp = head->last; head->last = temp->prev; head->last->next = NULL; temp->prev = NULL;
调用结点释放函数释放 temp 存储的结点申请的堆区内存空间。
有参有返回值函数
参数为头结点首地址。函数返回值 int 类型, 删除成功,返回
EXIT_SUCCESS
告知函数代码成功执行删除操作, 否则返回EXIT_FAILURE
头删函数:
有两种情况
当前链表里没有一个结点,直接终止函数运行
当前链表中有结点时,先用临时变量 temp 存储被删除结点地址
Node * temp = head->last; head->first = temp->next; head->first->prev = NULL; temp->next = NULL;
调用结点释放函数释放 temp 存储的结点申请的堆区内存空间。
有参有返回值函数
参数为头结点首地址。函数返回值 int 类型, 删除成功,返回
EXIT_SUCCESS
告知函数代码成功执行删除操作, 否则返回EXIT_FAILURE
指定下标删除函数:
有三种情况
当指定下标
index == 0
时, 调用头删函数当指定下标
index == head->size - 1
, 调用尾删函数指定下标位置不是以上两种情况时,先调用查找指定下标函数,把找到的目标删除结点首地址存储到临时变量 temp
Node *temp = find_node(head, index); temp->prev->next = temp->next; temp->next->prev = temp->prev; temp->prev = NULL; temp->next = NULL;
调用结点释放函数释放 temp 存储的结点申请的堆区内存空间。
有参有返回值函数
参数为头结点首地址和指定下标。函数返回值 int 类型, 删除成功,返回
EXIT_SUCCESS
告知函数代码成功执行删除操作, 否则返回EXIT_FAILURE
展示链表包含学生信息函数:
- 有参无返回值
- 参数为链表头结点首地址
3.3.2 链表结构体声明
typedef struct student
{
int id;
char name[32];
int age;
} Student;
// 结点类型 Node
typedef struct node
{
// 结点 Node 指针类型,当前 Node 结点存储上一个结点的首地址
struct node * prev;
// 结点 Node 指针类型,当前 Node 结点存储下一个结点的首地址
struct node * next;
// Node 结点中存储的数据内容,当前 Node 存储 Student 类型,存储数据为指针类型
Student * stu;
} Node;
// 头结点定义,数据类型重命名为 LinkedHead
typedef struct head
{
// 结点 Node 指针类型,存储链表中第一个 Node 结点地址
Node * first;
// 结点 Node 指针类型,存储链表中最后一个 Node 结点地址
Node * last;
// 当前双向链表中有效结点个数
int size;
} LinkedHead;
3.3.3 创建新的链表头 LinkedList
/*
创建 LinkedHead 链表头,也可以认为在创建链表基本结构
@return LinkedHead 数据在内存【堆区】首地址
*/
LinkedHead * create_new_linked()
{
/*
通过 calloc 函数申请对应的内存空间,申请空间。calloc 会
内存空间进行擦除操作。
内存擦除操作,对应内存所有二进制为都是 0,会根据不同的数据
类型对外的数据情况不同
如果是指针类型 ==> NULL
如果是整数类型 ==> 0
如果是 float 类型 ==> 0.0F
如果是 double 类型 ==> 0.0
如果是 char 类型 ==> '\0' 编码中编号为 0 的字符
*/
return (LinkedHead *)calloc(1, sizeof(LinkedHead));
}
3.3.4 创建 Student 类型数据
/*
根据用户提供的必要参数创建一个 Student 结构体数据,返回值是
对应数据在内存【堆区】空间首地址
@param id 学员 ID 号
@param name 学员姓名
@param age 学员年龄
@return Student 类型在内存【堆区】的空间首地址
*/
Student * create_new_student(int id, char * name, int age)
{
// 申请内存堆区空间
Student * stu = (Student *)calloc(1, sizeof(Student));
// 给予当前创建的 Student 数据进行赋值操作
stu->id = id;
strcpy(stu->name, name);
stu->age = age;
// 返回对应空间的首地址。
return stu;
}
3.3.5 创建新结点 Node
/*
创建一个新的 Node 结点,需要提供的参数是 Student 指针,
指向 Student 数据空间
@param stu Student 类型数据空间首地址
@return 创建的新的 Node 结点空间首地址
*/
Node * create_new_node(Student * stu)
{
// 申请 Node 对应的数据空间
Node * node = (Node *)calloc(1, sizeof(Node));
// 当前新的 Node 结点中的 stu 赋值为当前参数 Student 地址
node->stu = stu;
return node;
}
3.3.6 添加数据到链表末尾(尾插函数)
/*
添加新 Node 到当前双向链表的末尾
@param head LinkedHead 链表头指针
@param new_node Node 结点指针
@return 添加成功,返回 EXIT_SUCCESS, 否则返回 EXIT_FAILURE
*/
int add_last(LinkedHead * head, Node * new_node)
{
// 用户提供的链表头和 Node 结点数据都是 NULL
// 无法进行添加操作
if (NULL == head || NULL == new_node)
{
printf("用户提供的参数有误!");
// 添加失败
return EXIT_FAILURE;
}
// 根据当前链表头中的 size 有效元素个数,判断当前链表中是否有其他元素。
if (0 == head->size)
{
// 情况一: 当前链表中没有任何一个有效元素
head->first = new_node;
head->last = new_node;
}
else
{
// 情况二: 当前链表中有其他元素
// new_node 的 prev 指向原本最后一个结点
new_node->prev = head->last;
// 原本最后一个结点的 next 指向新结点 new_node
head->last->next = new_node;
// head 中的 last 指向新结点 new_node
head->last = new_node;
}
// 链表有效元素个数 += 1
head->size += 1;
return EXIT_SUCCESS;
}
3.3.7 添加新结点到链表头部(头插函数)
/*
添加新 Node 到当前双向链表的开端
@param head LinkedHead 链表头指针
@param new_node Node 结点指针
@return 添加成功,返回 EXIT_SUCCESS, 否则返回 EXIT_FAILURE
*/
int add_first(LinkedHead *head, Node *new_node)
{
// 判断用户提供的链表头和结点数据是否为 NULL
if (NULL == head || NULL == new_node)
{
printf("用户提供的参数有误!%s:%d\n", __FILE__, __LINE__);
return EXIT_FAILURE;
}
if (head->size != 0)
{
// 当前链表中有其他结点
// 【重点】建立新结点和原第一个结点的连接关系
// 新结点中的 next 存储原本第一个结点地址
new_node->next = head->first;
// 原本第一个结点的 prev 保存新结点地址
head->first->prev = new_node;
// 头结点 head 中的 first 存储 new_node 结点地址
head->first = new_node;
}
else
{
// 当前链表中没有其他结点
// 链表头中的 first 存储当前 new_node 地址
head->first = new_node;
// 链表头中的 last 存储当前 new_node 地址
head->last = new_node;
}
head->size += 1;
return EXIT_SUCCESS;
}
3.3.8 辅助函数
- 结点释放函数
/*
【辅助函数】释放删除操作中需要进行内存释放的结点
注意:
需要释放 Student 类型空间
@param node 需要被释放的结点地址
@return 释放成功,返回 EXIT_SUCCESS, 否则返回 EXIT_FAILURE
*/
int free_node_memory(Node *node)
{
if (NULL == node)
{
printf("结点数据为 NULL %s:%d\n", __FILE__, __LINE__);
return EXIT_FAILURE;
}
// 1. 释放 Student 空间
free(node->stu);
// 2. node 中的 stu 赋值为 NULL
node->stu = NULL;
// 3. 释放 Node 空间
free(node);
return EXIT_SUCCESS;
}
- 指定下标获取函数
/*
【辅助函数】根据用户指定的下标位置,找到目标结点地址,如果没有找到,返回 NULL
@param head LinkedHead 链表头指针
@param index 指定下标
@return 搜索成功返回目标 Node 结点地址,否则返回 NULL
*/
Node *find_node(LinkedHead *head, int index)
{
if (NULL == head)
{
printf("提供的链表为 NULL, %s:%d\n", __FILE__, __LINE__);
return NULL;
}
if (index < 0 || index > head->size - 1)
{
printf("下标不合法!, %s:%d\n", __FILE__, __LINE__);
return NULL;
}
Node *target = NULL;
if (index < head->size / 2)
{
// 从链表中的第一个结点开始搜索目标 index 下标对应的结点地址
target = head->first->next;
int count = 1;
// count 作为下标计数器,如果 count == index 找到目标元素
while (count != index)
{
// 未找到目标下标元素,target 赋值为当前结点的下一个结点。
target = target->next;
count++;
}
}
else
{
// 从链表中的最后一个结点开始搜索目标 index 下标对应的结点地址
target = head->last;
int count = head->size - 1;
while (count != index)
{
target = target->prev;
count--;
}
}
return target;
}
3.3.9 指定下标地址添加操作
/*
在当前链表中,指定下标位置添加新的 Node 结点
@param head LinkedHead 链表头指针
@param index 指定添加数据的下标位置,链表下标参考数组形式,从 0 开始到 size - 1
@param new_node 添加的新结点
@return 添加成功,返回 EXIT_SUCCESS, 否则返回 EXIT_FAILURE
*/
int add(LinkedHead *head, int index, Node *new_node)
{
// 给定两个指针数据判断是否为 NULL
if (NULL == head || NULL == new_node)
{
printf("提供的链表或者结点为 NULL, %s:%d\n", __FILE__, __LINE__);
return EXIT_FAILURE;
}
// 判断提供的下标数据是否合法
if (index < 0 || index > head->size)
{
printf("提供的下标越界, %s:%d\n", __FILE__, __LINE__);
return EXIT_FAILURE;
}
// 第一个大模块: 判断 index 是否为 0 或者 index == size
if (0 == index)
{
// 下标为 0 采用的方式在添加数据到第一个结点
return add_first(head, new_node);
}
else if (index == head->size)
{
// 下标数据为 size 采用的添加方式是添加到整个链表末尾
return add_last(head, new_node);
}
// 第二个模块:找到目标下标位置对应的结点地址
Node *target = find_node(head, index);
// 第三个模式: 目标位置添加新结点
// 核心步骤: new_node 和当前 target 结点以及 target 上一个结点连接
new_node->next = target;
new_node->prev = target->prev;
// target 上一个结点和当前 new_node 连接
new_node->prev->next = new_node;
// 当前 new_node成为 target 的上一个结点
target->prev = new_node;
// 有效元素个数 + 1
head->size += 1;
return EXIT_SUCCESS;
}
3.3.10 删除最后一个结点函数
/*
删除当前链表中的最后一个结点
@param head LinkedHead 链表头指针
@return 删除成功,返回 EXIT_SUCCESS, 否则返回 EXIT_FAILURE
*/
int remove_last(LinkedHead *head)
{
// 如果用户提供的链表头为 NULL 无法进行任何的代码操作
if (NULL == head)
{
printf("用户提供的参数有误!%s:%d\n", __FILE__, __LINE__);
return EXIT_FAILURE;
}
// 情况一: 当前链表中没有任何一个元素 0 == size ,终止函数运行。
if (0 == head->size)
{
return EXIT_FAILURE;
}
Node *free_node = head->last;
if (head->size > 1)
{
// 链表中存在多个元素
// 首先利用临时变量,存储当前被删除元素的上一个元素
Node *temp = head->last->prev;
// 断开连接 原本最后一个结点的 prev 赋值为 NULL
head->last->prev = NULL;
// 断开连接 原本最后一个结点的上一个结点的 next 赋值为 NULL
temp->next = NULL;
// 将 head 中的 last 指向 temp
head->last = temp;
}
else
{
// 链表中有且只有一个元素
// 链表头中的 first 和 last 同时赋值为 NULL
head->first = NULL;
head->last = NULL;
}
// 删除操作执行完毕,需要对 size -= 1
head->size -= 1;
return free_node_memory(free_node);
}
3.3.11 删除第一个元素函数
/*
删除当前链表中的第一个结点
@param head LinkedHead 链表头指针
@return 删除成功,返回 EXIT_SUCCESS, 否则返回 EXIT_FAILURE
*/
int remove_first(LinkedHead *head)
{
if (NULL == head)
{
printf("用户提供的参数有误!%s:%d\n", __FILE__, __LINE__);
return EXIT_FAILURE;
}
// 情况一: 当前链表中没有元素
if (0 == head->size)
{
return EXIT_FAILURE;
}
// 被删除的节点 Node
Node *delete_node = head->first;
if (1 == head->size)
{
// 情况二: 当前链表中有且只有一个元素
head->first = NULL;
head->last = NULL;
}
else
{
// 情况三: 当前链表中有多个元素
// 保留第一个结点之后的结点地址
Node *temp = head->first->next;
// 断开原第一个结点和之后结点的联系
head->first->next = NULL;
temp->prev = NULL;
// head->first 指向 temp
head->first = temp;
}
// 有效元素个数 - 1
head->size -= 1;
return free_node_memory(delete_node);
}
3.3.12 指定下标地址删除操作
/*
在当前链表中,删除指定下标位置结点
@param head LinkedHead 链表头指针
@param index 指定删除数据的下标位置,链表下标参考数组形式,从 0 开始到 size - 1
@return 删除成功,返回 EXIT_SUCCESS, 否则返回 EXIT_FAILURE
*/
int remove(LinkedHead *head, int index)
{
if (NULL == head)
{
printf("提供的链表为 NULL, %s:%d\n", __FILE__, __LINE__);
return EXIT_FAILURE;
}
if (index < 0 || index > head->size - 1)
{
printf("下标不合法!, %s:%d\n", __FILE__, __LINE__);
return EXIT_FAILURE;
}
Node *target = find_node(head, index);
if (NULL == target)
{
return EXIT_FAILURE;
}
// 删除结点的下一个结点的 prev 赋值为当前删除结点的 prev
target->next->prev = target->prev;
// 删除结点的上一个结点的 next 赋值为当前删除结的 next
target->prev->next = target->next;
// target 数据进行清理和释放操作
target->next = NULL;
target->prev = NULL;
// 有效元素 - 1
head->size -= 1;
return free_node_memory(target);
}
3.3.13 展示函数
void show(LinkedHead * head)
{
Node * temp = head->first;
while (temp != NULL)
{
printf("ID : %d, Name : %s, Age : %d\n",
temp->stu->id,
temp->stu->name,
temp->stu->age
);
temp = temp->next;
}
}
3.3.14 main 函数测试代码
int main(int argc, char const *argv[])
{
LinkedHead * head = create_new_linked();
// 常规写法
add_last(head, create_new_node(create_new_student(1, "James", 39)));
add_last(head, create_new_node(create_new_student(2, "Tom", 18)));
add_last(head, create_new_node(create_new_student(3, "Jerry", 19)));
add_last(head, create_new_node(create_new_student(4, "Wade", 42)));
// 展开写法
Student * stu = create_new_student(5, "YAO", 45);
Node * node = create_new_node(stu);
add_last(head, node);
show(head);
return 0;
}