C语言基础之:指针、结构体、链表
一、指针
指针的本质是存储其他变量地址的变量,就像一张写着 “黄金存放位置” 的纸条,通过它能快速找到目标数据。
1.1 指针的核心概念
指针也就是内存地址,指针变量是用来存放内存地址的变量。就像其他变量或常量一样,您必须在使用指针存储其他变量地址之前,对其进行声明。指针变量声明的一般形式为:
type *var_name;
int *ip; /* 一个整型的指针 */
double *dp; /* 一个 double 型的指针 */
float *fp; /* 一个浮点型的指针 */
char *ch; /* 一个字符型的指针 */
可以使用“小明存黄金” 的例子形象理解
- 小明在A 地点放了 1kg 黄金(对应变量
g=10
,A 是g
的内存地址); - 写一张纸条
Z1
,记录 A 地点的位置(对应指针int *z1=&g
,z1
存储g
的地址); - 再写一张纸条
Z2
,记录Z1
的存放位置 B(对应二级指针int **z2=&z1
,z2
存储z1
的地址)。
1.2 指针的定义格式与语法
基础格式:
数据类型 *指针变量名 = 目标变量地址;
- 数据类型:指针指向的变量的类型(如
int
、char
),决定了解引用时读取的字节数; *
:表示该变量是指针(不是普通变量);&
:取地址符,用于获取普通变量的内存地址。
代码示例:
#include<stdio.h>
int main() {int g = 10; // 普通变量:存储数据10int *z1 = &g; // 指针变量z1:存储g的地址printf("g的值:%d\n", g); // 输出:g的值:10printf("g的地址:%p\n", &g); // 输出:g的地址(如0x7ffeefbff4ac)printf("z1存储的地址:%p\n", z1); // 输出:与&g相同(z1存的是g的地址)return 0;
}
1.3指针的使用
使用指针时会频繁进行以下几个操作:定义一个指针变量、把变量地址赋值给指针、访问指针变量中可用地址的值。这些是通过使用一元运算符 * 来返回位于操作数所指定地址的变量的值。
#include <stdio.h>int main ()
{int var = 20; /* 实际变量的声明 */int *ip; /* 指针变量的声明 */ip = &var; /* 在指针变量中存储 var 的地址 */printf("var 变量的地址: %p\n", &var );/* 在指针变量中存储的地址 */printf("ip 变量存储的地址: %p\n", ip );/* 使用指针访问值 */printf("*ip 变量的值: %d\n", *ip );return 0;
}
因此,输出的结果分别为
var 变量的地址: 0x7ffeeef168d8
ip 变量存储的地址: 0x7ffeeef168d8
*ip 变量的值: 20
1.3 指针的 " & 与 * "
&
(取地址):获取变量的内存地址,“问地址”;*
(解引用):通过指针存储的地址,获取目标变量的值,“找数据”。
#include<stdio.h>
int main() {int g = 10;int *z1 = &g;// 1. 取地址:&g获取g的地址,赋值给z1printf("z1存储的地址:%p\n", z1); // 输出g的地址// 2. 解引用:*z1通过z1的地址找到g的值int n = *z1;printf("n的值(*z1):%d\n", n); // 输出:10(与g的值相同)// 3. 通过解引用修改目标变量的值*z1 = 20; printf("修改后g的值:%d\n", g); // 输出:20(g被修改)return 0;
}
如果以下代码运行输出
int n=10;
int *p=&n;
printf("%d",p);
printf("%d",*p);
printf("%d",&p);
则输出结果为
p存储东西的地址,即n的地址;
10;
p自己的地址
1.4 二级指针:指针的 “指针”
当指针本身也需要被存储时,就需要二级指针(数据类型 **
),它存储的是一级指针的地址。
int *z1=&g;
int **z2=&z1; //[Error] cannot convert 'int**' to 'int*' in initializationprintf("z2 = %d\n",z2);
代码中int **z2=&z1
报错,是因为若误将z2
定义为int *
(一级指针),会出现 “int**
无法转换为int*
” 的类型不匹配错误。正确定义必须是二级指针。
#include<stdio.h>
int main() {int g = 10;int *z1 = &g; // 一级指针:存g的地址int **z2 = &z1; // 二级指针:存z1的地址printf("z2存储的地址(z1的地址):%p\n", z2);printf("**z2的值(通过z2找z1,再找g):%d\n", **z2); // 输出:10// 通过二级指针修改g的值**z2 = 100;printf("修改后g的值:%d\n", g); // 输出:100return 0;
}
1.5 传值 vs 传址
C 语言函数参数默认是 “传值”(拷贝一份值给函数),无法修改实参;若要修改实参,必须用 “传址”(传递变量地址,通过指针操作)。
传递方式 | 原理 | 是否修改实参 | 适用场景 |
---|---|---|---|
传值 | 拷贝实参的值给形参 | 否 | 仅需使用实参的值(如求和) |
传址 | 传递实参的地址给指针 | 是 | 需要修改实参的值(如自增) |
1.6 动态内存分配:malloc 的使用
普通变量的内存是 “静态分配”(编译时确定大小),而malloc
能 “动态分配” 内存(运行时按需申请),返回的是分配内存的地址,必须用指针接收。
使用步骤:
- 调用
malloc(size)
:申请size
字节的内存(如malloc(4)
申请 4 字节,对应int
); - 强制类型转换:将
malloc
返回的void*
转换为目标指针类型(如(int*)malloc(4)
); - 判断是否分配成功:若
malloc
返回NULL
,表示内存不足,需处理; - 释放内存:用
free(指针)
释放申请的内存,避免内存泄漏。
代码示例:
#include<stdio.h>
#include<stdlib.h> // malloc和free的头文件
int main() {// 动态申请1个int大小的内存(4字节)int *p1 = (int*)malloc(sizeof(int)); // 判断内存是否分配成功if (p1 == NULL) {printf("内存分配失败!\n");return 1; // 退出程序}// 使用动态内存*p1 = 100;printf("动态内存存储的值:%d\n", *p1); // 输出:100// 释放内存free(p1);p1 = NULL; // 避免野指针(释放后指针指向无效地址)return 0;
}
1.7 指针的适用场景
- 需要修改函数实参的值(如
add2
函数); - 动态分配内存(如链表节点的创建);
- 实现复杂数据结构(链表、树、图等);
- 节省内存(传递地址比传递大结构体拷贝更高效)。
二、结构体:
数组只能存储 “相同类型” 的数据,而结构体可以存储 “不同类型” 的数据(如学生的年龄、分数、姓名),是 “数据封装” 的基础。
2.1 结构体的核心作用
将多个关联的、不同类型的数据打包成一个 “整体”,方便管理和传递。例如:用一个结构体存储一个学生的所有信息,而不用多个独立变量。
2.2 结构体的定义格式
用typedef
给结构体起 “别名”,简化后续使用(否则每次定义结构体变量都要写struct 结构体名
)。
基础格式:
typedef struct 结构体名 {数据类型 成员1; // 结构体的“字段”数据类型 成员2;// ... 更多成员
} 结构体别名; // 后续可直接用别名定义变量
代码示例:
#include<stdio.h>
// 定义学生结构体,别名是Student
typedef struct Student {int age; // 年龄(int类型)double score;// 分数(double类型)char name[20];// 姓名(字符数组)
} Student;
int main() {// 用别名Student定义结构体变量Student stu1;// 给成员赋值(用.访问普通结构体变量的成员)stu1.age = 20;stu1.score = 95.5;// 字符串赋值需用strcpy(不能直接用=)strcpy(stu1.name, "张三");// 打印结构体成员printf("姓名:%s, 年龄:%d, 分数:%.1f\n", stu1.name, stu1.age, stu1.score); // 输出:姓名:张三, 年龄:20, 分数:95.5return 0;
}
2.3 结构体的初始化
结构体初始化分 “静态初始化” 和 “动态初始化”(用malloc
分配内存,适合不确定大小的场景)。
初始化方式 | 特点 | 代码示例 |
---|---|---|
静态初始化 | 编译时分配内存 | Student stu1 = {20, 95.5, "张三"}; |
动态初始化 | 运行时分配内存(指针) | Student *stu2 = (Student*)malloc(sizeof(Student)); |
代码示例:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
typedef struct Student {int age;double score;char name[20];
} Student;
int main() {// 动态分配结构体内存(大小是整个结构体的字节数)Student *stu2 = (Student*)malloc(sizeof(Student));if (stu2 == NULL) {printf("内存分配失败!\n");return 1;}// 结构体指针用->访问成员(不能用.)stu2->age = 21;stu2->score = 92.0;strcpy(stu2->name, "李四");printf("姓名:%s, 年龄:%d, 分数:%.1f\n", stu2->name, stu2->age, stu2->score); // 输出:姓名:李四, 年龄:21, 分数:92.0// 释放动态内存free(stu2);stu2 = NULL;return 0;
}
2.4 结构体与指针的结合:-> 操作符
- 普通结构体变量:用
.
访问成员(如stu1.age
); - 结构体指针:用
->
访问成员(如stu2->age
),本质是(*stu2).age
的简化写法。
2.5 结构体的适用场景
- 封装复杂数据(如学生、员工、商品信息);
- 作为函数参数传递(传递指针可避免拷贝大结构体,提高效率);
- 定义链表、树等数据结构的 “节点”(存储节点数据)。
三、链表:基于指针与结构体的动态数据结构
链表是 “节点” 的链式集合,每个节点包含 “数据域”(存储数据)和 “指针域”(存储下一个节点的地址)。相比数组,链表的大小可动态调整,插入删除效率更高。
3.1 链表的核心概念
- 节点:链表的基本单元,由 “数据域” 和 “指针域” 组成(用结构体实现);
- 头节点:链表的第一个节点,是遍历链表的入口(不能丢失,否则链表 “失联”);
- 尾节点:链表的最后一个节点,指针域指向
NULL
(表示链表结束)。
链表与数组的对比:
特性 | 链表 | 数组 |
---|---|---|
内存分配 | 动态分配(不连续) | 静态分配(连续) |
大小调整 | 支持动态增减 | 固定大小,无法修改 |
插入删除效率 | 高(仅需修改指针) | 低(需移动大量元素) |
随机访问 | 不支持(需遍历) | 支持(通过下标直接访问) |
3.2 链表的完整实现
以 “存储整数的单链表” 为例,实现节点定义、创建节点、连接链表、遍历链表、插入节点、释放内存。
步骤 1:定义链表节点结构体
节点包含 “数据域data
” 和 “指针域next
”(指向同类型节点)。
#include<stdio.h>
#include<stdlib.h>
// 定义链表节点结构体,别名ListNode
typedef struct ListNode {int data; // 数据域:存储整数struct ListNode *next; // 指针域:指向 next 节点
} ListNode;
步骤 2:创建链表节点
用malloc
动态分配节点内存,避免重复代码。
// 创建一个新节点,返回节点指针
ListNode* createNode(int data) {ListNode *newNode = (ListNode*)malloc(sizeof(ListNode));if (newNode == NULL) {printf("节点内存分配失败!\n");return NULL;}newNode->data = data; // 给数据域赋值newNode->next = NULL; // 初始时指针域指向NULL(避免野指针)return newNode;
}
步骤 3:连接节点,形成链表
通过指针域next
将多个节点串联起来,确定头节点。
int main() {// 1. 创建5个节点(数据分别为100、200、300、400、500)ListNode *node1 = createNode(100);ListNode *node2 = createNode(200);ListNode *node3 = createNode(300);ListNode *node4 = createNode(400);ListNode *node5 = createNode(500);// 2. 连接节点(形成链表:node1 -> node2 -> node3 -> node4 -> node5 -> NULL)node1->next = node2;node2->next = node3;node3->next = node4;node4->next = node5;node5->next = NULL; // 尾节点指向NULL// 头节点是node1(后续操作都通过头节点进行)ListNode *head = node1;// 打印链表的值 printf("1:%d\n",node1->data); printf("2:%d\n",node1->next->data); printf("3:%d\n",node1->next->next->data); printf("4:%d\n",node1->next->next->next->data); printf("5:%d\n",node1->next->next->next->next->data);
步骤 4:遍历链表
// 3. 遍历链表(从 head 开始,直到 NULL)printf("链表遍历结果:");ListNode *temp = head; // 临时指针(避免修改头节点)while (temp != NULL) {printf("%d -> ", temp->data);temp = temp->next; // 移动到下一个节点}printf("NULL\n"); // 输出:链表遍历结果:100 -> 200 -> 300 -> 400 -> 500 -> NULL
步骤 5:链表尾部插入节点
在尾节点后添加新节点,需先遍历到尾节点,再修改尾节点的next
。
// 4. 尾部插入新节点(如插入600)int newData = 600;ListNode *newNode = createNode(newData);if (newNode == NULL) return 1;// 找到尾节点(next == NULL 的节点)temp = head; // 重置临时指针while (temp->next != NULL) {temp = temp->next;}// 插入新节点(尾节点的next指向新节点)temp->next = newNode;newNode->next = NULL; // 新节点成为尾节点// 重新遍历,验证插入结果printf("插入后遍历结果:");temp = head;while (temp != NULL) {printf("%d -> ", temp->data);temp = temp->next;}printf("NULL\n"); // 输出:插入后遍历结果:100 -> 200 -> 300 -> 400 -> 500 -> 600 -> NULL
步骤 6:释放链表内存(避免内存泄漏)
链表的内存是动态分配的,必须从 head 开始逐个释放每个节点。
// 5. 释放链表内存ListNode *freeTemp;temp = head;while (temp != NULL) {freeTemp = temp; // 保存当前节点temp = temp->next; // 先移动到下一个节点free(freeTemp); // 释放当前节点}head = NULL; // 头节点置空,避免野指针return 0;
}
3.3 链表的适用场景
- 数据量不固定(如日志系统、消息队列,需动态增减数据);
- 频繁插入 / 删除操作(如链表中间插入,仅需修改 2 个指针,效率 O (1));
- 内存资源有限(按需分配内存,不浪费空间)。
四、常见问题与解决方法
-
野指针问题:指针未初始化、或指向已释放的内存。解决:指针定义时置
NULL
(如int *p = NULL
),free
后再置NULL
。 -
内存泄漏问题:
malloc
分配的内存未用free
释放,程序结束前内存一直被占用。解决:动态内存使用后必须free
,链表需遍历逐个释放节点。 -
类型不匹配问题:如二级指针与一级指针混用(
int *z2 = &z1
,&z1
是int**
类型)。解决:严格匹配指针类型,一级指针存普通变量地址,二级指针存一级指针地址。 -
结构体指针访问成员错误:用
.
访问结构体指针的成员(如stu2.age
)。解决:结构体指针必须用->
(如stu2->age
),普通结构体变量用.
。
五、总结
指针、结构体与链表是 C 语言的核心,三者的关系可总结为:
- 指针是基础:提供 “地址访问” 能力,是动态内存分配和链表连接的关键;
- 结构体是封装:将 “数据 + 指针” 打包成节点,为链表提供数据载体;
- 链表是应用:结合指针与结构体,实现动态数据存储,解决数组的局限性。