1. 基本知识

1.1 概念

结构体属于用户自定义的数据类型,允许用户存储不同的数据类型

由一批数据组合而成的结构型数据

1.2 定义和使用

定义方法:

1
2
3
4
5
6
struct 结构体名 
{
成员1;
成员2;
...
};

通过结构体创建变量的方式有三种:

  • struct 结构体名 变量名
  • struct 结构体名 变量名 = { 成员1值 , 成员2值…}
  • 定义结构体时顺便创建变量

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//结构体定义
struct student
{
//成员列表
string name; //姓名
int age; //年龄
int score; //分数
}stu3; //结构体变量创建方式3

//结构体变量创建方式1
struct student stu1;

//结构体变量创建方式2
struct student stu2 = { "李四",19,60 };

注意:结构体书写位置不同会有不同的生效域:书写在函数里面是局部位置,只能在本函数中使用;书写在函数外面是全局位置,在所有的函数中都能使用。

给结构体变量赋值:变量名.属性 = 值;

1
2
3
stu3.name = "王五";
stu3.age = 18;
stu3.score = 80;

2. 结构体数组

作用:将自定义的结构体放入到数组中方便维护

语法: struct 结构体名 数组名[元素个数] = { {} , {} , ... {} };

struct 结构体名 数组名[元素个数] = { 变量1 , 变量2, ... 变量n };

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct student
{
//成员列表
string name; //姓名
int age; //年龄
int score; //分数
}
struct student arr[3]=
{
{"张三",18,80 },
{"李四",19,60 },
{"王五",20,70 }
};

3. 起别名

作用:简化使用结构体名时的繁琐

语法:在定义前加typedef

1
2
3
4
5
6
7
8
9
typedef struct 结构体名
{
成员1;
成员2;
...
}别名;

//后续使用别名就可以省略struct
别名 变量名;

其中的结构体名可以省略

4.结构体作为函数的参数进行传递

作用:将结构体作为参数向函数中传递

示例:(修改学生名字)

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
33
//学生结构体定义
struct student
{
//成员列表
char name[100]; //姓名
int age; //年龄
};S

int main()
{
S stu;
strcpy(stu.name, "zhangsan"); //赋结构体中的字符串应该这样操作
stu.age = 0;
ChangeName(stu); //值传递
ChangeName2(&stu); //地址传递

return 0;
}

//值传递
void ChangeName(S st)
{
scanf("%s", st.name); //st.name是数组,名字即是首个元素的地址
scanf("%d", &(st.age)); //st.age是整型变量,要取&地址,用()括起来
}

//地址传递
void ChangeName2(S* p) //指针p记录的是main函数中stu的内存地址
{
printf("接收到main函数中学生的初始数据为:%s,%d", (*p).name, (*p).age);
scanf("%s", (*p).name);
scanf("%d", &((*p).age)); // 有点复杂仔细记住
}

5. 结构体嵌套

作用: 结构体中的成员可以是另一个结构体

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct Message{
char phone[12];
char mail[100];
};

struct Student{
char name[100];
int age;
char gender;
double height;
struct Message msg;
};

struct Student stu;
strcpy(stu.msg.phone, "12345678901");
strcpy(stu.msg.mail, "123@qq.com");

6.结构体内存对齐

确定变量位置:只能放在自己类型的整数倍的内存地址上

最后一个补位:结构体的总大小,是最大类型的整数倍

1
2
3
4
5
6
struct num{
double a; //8字节
char b; //1字节
int c; //4字节
char d; //1字节
} //总大小应该是24字节

占内存应该如下(空的地方会填充空字节直到24,因为24是8的整数倍):

image-20241014194459384
  • 内存对齐:不管是结构体还是普通变量都存在的规则,只能放在自己类型整数倍的内存地址上(内存地址/占用字节 = 结果可以整除);

总结:把小的数据类型写在最上面,大的数据类型写在最下面可以节约空间

7. 补充知识

7.1 结构体成员是指针

  1. 定义包含指针的结构体
1
2
3
4
struct Person {
char *name; // 指向字符的指针,保存名字
int age; // 整数类型的成员,保存年龄
};

在这个结构体中,name 是一个指针,它指向字符数组(字符串)的起始位置,而 age 是一个普通的整数类型。

  1. 动态分配内存给指针成员

因为 name 是一个指针,通常我们需要为它动态分配内存,这样才能存储名字。可以使用 malloc 函数来为指针分配内存。

1
2
3
4
5
6
7
8
struct Person p1;

// 动态分配内存给 name 指针,假设名字最多包含 50 个字符
p1.name = (char *)malloc(50 * sizeof(char));

// 给成员赋值
p1.age = 30;
snprintf(p1.name, 50, "Alice");
  1. 访问结构体中的指针成员
  • 如果你直接使用结构体变量访问指针成员,可以使用 . 操作符:
1
p1.name = "Alice"; // 直接给指针赋值一个字符串字面值
  • 如果你使用指向结构体的指针来访问结构体的成员,使用 -> 操作符:
1
2
3
struct Person *p_ptr = &p1;
p_ptr->age = 25;
p_ptr->name = "Bob"; // 这里 name 也是一个指针,所以赋值的是字符串字面值

7.2 链表

7.1 基本概念

链表是一种线性数据结构,由一系列节点组成。每个节点包含两部分:

  • 数据部分(Data):用于存储节点的数据。
  • 指针部分(Pointer):存储下一个节点的地址,用于链接下一个节点。

链表的首节点通常称为头节点(Head),而最后一个节点指向NULL,表示链表的结束。

7.2 分类

7.2.1 单向链表(Singly Linked List)

每个节点只有一个指向下一个节点的指针。链表的最后一个节点指向NULL

1
2
3
4
struct Node {
int value; //数据域
struct Node* next; //指针域
};

7.2.2 双向链表(Doubly Linked List)

每个节点有两个指针,一个指向前一个节点,另一个指向后一个节点。这样可以从链表的任意节点进行前向和后向遍历。

1
2
3
4
5
struct Node {
int data;
struct Node* prev;
struct Node* next;
};

7.2.3 循环链表(Circular Linked List)

循环链表的特点是链表的最后一个节点指向链表的头节点,这样链表可以像一个环一样循环遍历。可以是单向或双向的。

1
2
3
4
struct Node {
int data;
struct Node* next;
};

7.3 链表的操作

链表中的常见操作包括节点的插入、删除、遍历和查找。

插入操作

  • 在链表头部插入:将新节点插入到头节点之前,并将头指针更新为新节点。
  • 在链表尾部插入:遍历到链表末尾,将新节点插入到尾节点之后。
  • 在链表中间插入:找到目标位置的前一个节点,并将新节点插入该位置。

头部插入的代码示例

1
2
3
4
5
6
void insertAtHead(struct Node** head, int newData) {
struct Node* newNode = (struct Node*)malloc(sizeof(struct Node));
newNode->data = newData;
newNode->next = *head;
*head = newNode;
}

删除操作

  • 删除头节点:直接将头节点的指针指向下一个节点,然后释放原头节点的内存。
  • 删除尾节点:需要遍历到倒数第二个节点,将其指向NULL,并释放原尾节点。
  • 删除中间节点:找到要删除节点的前一个节点,调整指针并释放要删除的节点。

删除头节点的代码示例

1
2
3
4
5
6
void deleteAtHead(struct Node** head) {
if (*head == NULL) return;
struct Node* temp = *head;
*head = (*head)->next;
free(temp);
}

注意一定要释放掉被删除的节点,不然会产生野指针,野指针的出现会造成严重的内存泄漏

遍历链表

从头节点开始,逐个访问节点的数据,直到NULL节点为止。

遍历链表的代码示例

1
2
3
4
5
6
7
8
void printList(struct Node* head) {
struct Node* current = head;
while (current != NULL) {
printf("%d -> ", current->data);
current = current->next;
}
printf("NULL\n");
}

查找操作

遍历链表,查找与目标值匹配的节点。

查找操作的代码示例

1
2
3
4
5
6
7
8
struct Node* search(struct Node* head, int target) {
struct Node* current = head;
while (current != NULL) {
if (current->data == target) return current;
current = current->next;
}
return NULL;
}

7.4 链表的内存管理

在C语言中,链表的节点是动态分配的内存,因此需要合理使用malloc进行内存分配,并使用free释放节点不再使用时的内存,防止内存泄漏。

1
2
3
struct Node* newNode = (struct Node*)malloc(sizeof(struct Node));
newNode->data = 5;
newNode->next = NULL;

释放节点的内存:

1
free(node);