C 语言之所以强大,指针是其核心特性之一。理解指针对于掌握 C 语言,进而学习更高级的数据结构至关重要。很多初学者在学习数据结构时,往往会被指针搞得晕头转向。今天我们就来彻底搞懂 C 语言中的指针,为后续学习链表打下坚实的基础。
什么是指针?
简单来说,指针就是一个存储内存地址的变量。这个内存地址指向另一个变量,我们可以通过指针来访问和修改该变量的值。
int num = 10; // 定义一个整型变量 num,赋值为 10
int *ptr = # // 定义一个整型指针 ptr,指向 num 的地址
printf("num 的值:%d\n", num); // 输出 num 的值:10
printf("num 的地址:%p\n", &num); // 输出 num 的地址,例如:0x7ffee1a23b68
printf("ptr 的值:%p\n", ptr); // 输出 ptr 的值,即 num 的地址:0x7ffee1a23b68
printf("ptr 指向的值:%d\n", *ptr); // 输出 ptr 指向的值,即 num 的值:10
& 符号用于获取变量的地址,* 符号用于解引用指针,即访问指针指向的变量的值。理解这两个符号的含义是理解指针的关键。
指针的用途
指针主要用于以下几个方面:
- 动态内存分配: 使用
malloc和calloc等函数动态分配内存,返回的是指向分配内存的指针。 - 函数参数传递: 通过指针可以在函数内部修改函数外部的变量的值。
- 数据结构: 在链表、树等数据结构中,指针用于连接各个节点。
指针易错点
- 空指针: 未初始化的指针或者指向
NULL的指针。访问空指针会导致程序崩溃。 - 野指针: 指向已经被释放的内存的指针。访问野指针的结果是未定义的,可能导致程序崩溃或者数据错误。
- 内存泄漏: 动态分配的内存没有及时释放,导致内存占用越来越大。尤其是在长时间运行的程序中,内存泄漏会导致系统资源耗尽。
结构体:自定义数据类型
结构体允许我们将多个不同类型的数据组合成一个整体,从而方便我们表示复杂的数据结构。类似于 Java 中的 Class,但是功能相对弱一些,没有继承、多态等特性。
定义结构体
struct Student {
char name[20]; // 姓名
int age; // 年龄
float score; // 分数
};
// 使用 typedef 简化结构体的使用
typedef struct Student Student;
int main() {
Student stu1 = {"张三", 18, 90.5};
Student stu2 = {"李四", 19, 85.0};
printf("姓名:%s, 年龄:%d, 分数:%.2f\n", stu1.name, stu1.age, stu1.score);
printf("姓名:%s, 年龄:%d, 分数:%.2f\n", stu2.name, stu2.age, stu2.score);
return 0;
}
结构体指针
结构体指针是指向结构体变量的指针。通过结构体指针可以方便地访问和修改结构体成员。
Student *pstu = &stu1; // 定义一个结构体指针 pstu,指向 stu1
printf("姓名:%s, 年龄:%d, 分数:%.2f\n", pstu->name, pstu->age, pstu->score); // 使用 -> 访问结构体成员
使用 -> 运算符可以通过结构体指针访问结构体成员。
链表:动态数据结构
链表是一种动态数据结构,由一系列节点组成,每个节点包含数据和指向下一个节点的指针。链表可以动态地添加和删除节点,因此非常灵活。
单链表
单链表的每个节点只有一个指向下一个节点的指针。
// 定义链表节点结构体
typedef struct Node {
int data; // 数据
struct Node *next; // 指向下一个节点的指针
} Node;
// 创建链表
Node* createList() {
Node *head = NULL; // 初始化头指针为空
Node *tail = NULL; // 初始化尾指针为空
int data;
printf("请输入链表数据(输入-1结束):\n");
while (scanf("%d", &data) == 1 && data != -1) {
Node *newNode = (Node*)malloc(sizeof(Node)); // 创建新节点
if (newNode == NULL) {
printf("内存分配失败!\n");
exit(1);
}
newNode->data = data; // 设置节点数据
newNode->next = NULL; // 初始化 next 指针为空
if (head == NULL) {
head = newNode; // 如果链表为空,则新节点为头节点
tail = newNode; // 同时也是尾节点
} else {
tail->next = newNode; // 将新节点添加到链表尾部
tail = newNode; // 更新尾节点
}
}
return head; // 返回头指针
}
// 打印链表
void printList(Node *head) {
Node *current = head; // 从头节点开始遍历
printf("链表数据:\n");
while (current != NULL) {
printf("%d ", current->data); // 打印节点数据
current = current->next; // 移动到下一个节点
}
printf("\n");
}
// 释放链表内存
void freeList(Node *head) {
Node *current = head;
while (current != NULL) {
Node *temp = current; // 临时保存当前节点
current = current->next; // 移动到下一个节点
free(temp); // 释放当前节点
}
}
int main() {
Node *head = createList(); // 创建链表
printList(head); // 打印链表
freeList(head); // 释放链表内存
return 0;
}
链表的操作
常见的链表操作包括:
- 插入节点: 在链表的头部、尾部或者中间插入新节点。
- 删除节点: 删除链表中的指定节点。
- 查找节点: 查找链表中是否存在指定值的节点。
- 反转链表: 将链表中的节点顺序颠倒。
链表的应用
链表广泛应用于各种场景,例如:
- 动态数组: 链表可以实现动态数组,可以根据需要动态地添加和删除元素。
- 队列和栈: 链表可以实现队列和栈等数据结构。
- 哈希表: 链表可以解决哈希冲突。
掌握链表对于理解和实现更复杂的数据结构,例如树和图,至关重要。在实际项目中,我们常常需要使用链表来解决各种问题。例如在 Nginx 的 upstream 模块中,就使用了链表来管理 upstream server 的配置信息,需要考虑反向代理、负载均衡、并发连接数等问题。使用宝塔面板可以方便地管理 Nginx 的配置,但深入理解 Nginx 的底层原理,才能更好地优化性能。
冠军资讯
CoderPunk