当前位置: 首页 > news >正文

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=&gz1存储g的地址);
  • 再写一张纸条Z2,记录Z1的存放位置 B(对应二级指针int **z2=&z1z2存储z1的地址)。

1.2 指针的定义格式与语法

基础格式

数据类型 *指针变量名 = 目标变量地址;

  • 数据类型:指针指向的变量的类型(如intchar),决定了解引用时读取的字节数;
  • *:表示该变量是指针(不是普通变量);
  • &:取地址符,用于获取普通变量的内存地址。

代码示例:

#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能 “动态分配” 内存(运行时按需申请),返回的是分配内存的地址,必须用指针接收。

使用步骤

  1. 调用malloc(size):申请size字节的内存(如malloc(4)申请 4 字节,对应int);
  2. 强制类型转换:将malloc返回的void*转换为目标指针类型(如(int*)malloc(4));
  3. 判断是否分配成功:若malloc返回NULL,表示内存不足,需处理;
  4. 释放内存:用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&z1int**类型)。解决:严格匹配指针类型,一级指针存普通变量地址,二级指针存一级指针地址。

  • 结构体指针访问成员错误:用.访问结构体指针的成员(如stu2.age)。解决:结构体指针必须用->(如stu2->age),普通结构体变量用.

五、总结

指针、结构体与链表是 C 语言的核心,三者的关系可总结为:

  • 指针是基础:提供 “地址访问” 能力,是动态内存分配和链表连接的关键;
  • 结构体是封装:将 “数据 + 指针” 打包成节点,为链表提供数据载体;
  • 链表是应用:结合指针与结构体,实现动态数据存储,解决数组的局限性。
http://www.dtcms.com/a/491800.html

相关文章:

  • 王国保卫战全集下载 1~5部全系列MOD DLC修版 安卓+ios+PC电脑版
  • wordpress邮件发验证码网站站内结构优化
  • 国内专业网站设计关于网站运营
  • 【Java序列化与反序列化详解】
  • JAiRouter v1.0.0 正式发布:企业级 AI 服务网关的开源解决方案
  • HDR Scattering and Tone Mapping
  • 做我的世界皮肤壁纸的网站工业设计软件上市公司
  • 网站备案模板自己做的网站可以发布吗
  • vue-easy-tree树状结构
  • 重庆网站设计公司小白测评做网站
  • 可以发布广告的网站农村网站建设补助
  • 哪个网站做的简历最好专业网站设计力荐亿企邦
  • 轻量云服务器Lighthouse × 1Panel Halo 开启创意新玩法,建站与服务部署全攻略
  • 亦庄附近的网站建设公司关键词排名怎么查
  • 做网站建设的利润南宁做企业网站
  • 群体稳定性指标PSI:机器学习模型稳定性评估的核心工具
  • **标题:发散创新:探索自愈系统的设计与实现**引言:随着计算机技术的飞速发展,软件系统的可靠性和稳定性问题愈发受到关注。本文将
  • 常驻服务问答
  • 龙岩建网站公司手机网站建设公司
  • Flink性能调优基石:资源配置与内存优化实践
  • h5页面制作流程抖音seo搜索优化
  • 自己动手做网站公司网页网站建设ppt模板
  • 图片渐变透明,图片透明渐变,图片怎么渐变透明,颜色渐变透明,颜色透明渐变,怎么让图片渐变透明,图片边缘渐变透明,图片渐变半透明
  • 数据产品(3)-数据中台
  • STM32理论 —— 存储、中断
  • 如何选择做网站软件制作
  • 营销型网站建设广州搭建 网站 模版
  • 在NumPy中合并两个一维数组的多种方法
  • 如何求「加减 value 任意次后的最大 MEX」同余类求解
  • 『 数据库 』MySQL复习 - MySQL表CRUD操作全解析