C语言指针从入门到实战

引言:为什么指针是 C 语言的灵魂

如果你问一个 C 语言开发者:"C 语言最难也最精华的部分是什么?",99% 的人会告诉你 ------指针

指针就像 C 语言的 "内功心法":

  • 不懂指针,你永远只是 C 语言的 "门外汉",写出来的代码总是 "隔靴搔痒"
  • 掌握了指针,你才能真正 "触摸" 到计算机的内存,理解程序运行的本质
  • 指针是实现高效算法、复杂数据结构、系统级编程的基础

很多初学者对指针望而生畏,觉得它太抽象、太容易出错。但请相信我:指针不是洪水猛兽,它只是一把打开计算机内存世界大门的钥匙。

本文将从最基础的内存模型讲起,配合 3 个企业开发中最常用的经典案例,带你从 "理解概念" 到 "实战应用",彻底吃透 C 语言指针!

一、基础概念:画图理解指针的本质

1.1 内存模型:计算机的 "储物柜"

在讲指针之前,我们必须先搞懂:程序在内存中是如何存储的?

我们可以把内存想象成一个巨大的储物柜:

  • 每个 "格子" 就是一个内存单元(1 字节)
  • 每个格子都有一个唯一的编号,这就是内存地址
  • 格子里放的东西就是数据

|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Plain Text 内存地址(十六进制) 内存单元(1字节) ------------------------------------ 0x00007FFD8B34F8C0 → [ 0x68 ] 'h' 0x00007FFD8B34F8C1 → [ 0x65 ] 'e' 0x00007FFD8B34F8C2 → [ 0x6C ] 'l' 0x00007FFD8B34F8C3 → [ 0x6C ] 'l' 0x00007FFD8B34F8C4 → [ 0x6F ] 'o' ... 0x00007FFD8B34F8D0 → [ 0x0A ] 10 |

|----------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 划重点 : * 内存地址是一个无符号整数,通常用十六进制表示 * 32 位系统地址范围:0x00000000 ~ 0xFFFFFFFF(4GB) * 64 位系统地址范围:0x0000000000000000 ~ 0xFFFFFFFFFFFFFFFF(理论值) |

1.2 什么是指针?

指针就是存储内存地址的变量!

就这么简单。普通变量存的是数据本身,而指针变量存的是 "某个数据在内存中的位置"。

让我们看一段最简单的代码:

|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| c #include <stdio.h> int main() { int a = 10; // 普通变量:存的是数据10 int *p = &a; // 指针变量:存的是a的地址 printf("a的值 = %d\n", a); printf("a的地址 = %p\n", &a); printf("p的值 = %p\n", p); printf("p指向的值 = %d\n", *p); return 0; } |

运行结果

|---------------------------------------------------------------------------|
| Plain Text a的值 = 10 a的地址 = 0x7ffd8b34f8c4 p的值 = 0x7ffd8b34f8c4 p指向的值 = 10 |

我们用图来理解这段代码的内存布局:

|------------------------------------------------------------------------------------------------------------------------------------------|
| Plain Text 变量名 内存地址 存储的值 ---------------------------------------- a → 0x7ffd8b34f8c4 → 10 p → 0x7ffd8b34f8c8 → 0x7ffd8b34f8c4 (这就是a的地址!) |

看到了吗?p 这个变量里存的就是 a 的地址!

1.3 两个关键符号:& 和 *

|----|------|---------------|-----------------|
| 符号 | 名称 | 作用 | 示例 |
| & | 取地址符 | 获取变量的内存地址 | &a 得到 a 的地址 |
| * | 解引用符 | 根据地址访问对应内存的数据 | *p 得到 p 指向地址的值 |

|---------------------------------------------------------------------------------------------------------------------|
| 注意 :* 符号有两个含义,千万别搞混了! * 定义指针时:int *p; 这里的*表示 "这是一个指针类型" * 使用指针时:*p = 20; 这里的*表示 "解引用,访问指向的内存" |

1.4 指针类型的意义

你可能会问:既然指针都是存地址,那为什么还要分 int*、char*、double* 这些类型?

答案是:决定了 "步长" 和 "解读方式"!

|---------------------------------------------------------------------------------------------------------------------------------|
| c int *p_int; // 指向int,每次移动4字节,按整数方式解读 char *p_char; // 指向char,每次移动1字节,按字符方式解读 double *p_double;// 指向double,每次移动8字节,按浮点数方式解读 |

|-------------------------------------------------------------------------------------------------------|
| 思考 :如果一个 int* 指针指向地址 0x100,那么 p+1 指向哪里? 答案是:0x104!因为 int 占 4 字节,指针 + 1 是 "移动一个单位",不是 + 1 字节! |

二、经典案例 1:函数传参深度解析

这是指针最基础也最常用的场景 ------让函数能够修改外部变量

2.1 面试必考题:为什么值传递交换失败?

几乎每个 C 语言面试都会考这个题:"下面这段代码能交换 a 和 b 的值吗?为什么?"

|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| c #include <stdio.h> // 尝试交换两个数(值传递版本) void swap_fail(int x, int y) { int temp = x; x = y; y = temp; printf("swap内部:x=%d, y=%d\n", x, y); } int main() { int a = 10, b = 20; printf("交换前:a=%d, b=%d\n", a, b); swap_fail(a, b); printf("交换后:a=%d, b=%d\n", a, b); return 0; } |

运行结果

|----------------------------------------------------------------------|
| Plain Text 交换前:a=10, b=20 swap内部:x=20, y=10 交换后:a=10, b=20 ← 没有交换成功! |

为什么失败?我们画图分析:

|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Plain Text 调用swap前:main函数栈帧 a的地址:0x100 → 值:10 b的地址:0x104 → 值:20 调用swap时:创建新的栈帧,拷贝值! x的地址:0x200 → 值:10(这是a的副本!) y的地址:0x204 → 值:20(这是b的副本!) swap内部交换的是x和y,也就是0x200和0x204这两个地址的值 但原来的0x100和0x104地址的值根本没动过! |

|-------------------------------------------------------|
| 划重点 :值传递的本质是 "拷贝"!函数操作的是原数据的 "副本",修改副本不会影响原件! |

2.2 正确方案:地址传递

既然值传递是拷贝数据,那我们不传数据本身,传 "数据的地址" 不就行了?

|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| c #include <stdio.h> // 交换两个数(地址传递版本) void swap_success(int *x, int *y) { // x存的是a的地址,y存的是b的地址 int temp = *x; // 取a地址的值 → temp=10 *x = *y; // 把b地址的值放到a地址中 *y = temp; // 把temp的值放到b地址中 } int main() { int a = 10, b = 20; printf("交换前:a=%d, b=%d\n", a, b); swap_success(&a, &b); // 传入地址! printf("交换后:a=%d, b=%d\n", a, b); return 0; } |

运行结果

|-------------------------------------------------|
| Plain Text 交换前:a=10, b=20 交换后:a=20, b=10 ✓ 成功了! |

内存分析

|-------------------------------------------------------------------------------------------------------------------------------------------------|
| Plain Text 调用swap时: x的值 = &a = 0x100 (x存的就是a的地址!) y的值 = &b = 0x104 (y存的就是b的地址!) *x 就是直接访问 0x100 这个地址(也就是a本身!) *y 就是直接访问 0x104 这个地址(也就是b本身!) |

|------------------------------------------------------------------------------|
| 思考 :为什么函数返回值只能有一个,但用指针可以 "返回" 多个值? 答案:指针让函数拥有了直接修改调用者内存的能力!这就是指针的威力。 |

三、经典案例 2:字符串指针实战

字符串是 C 语言中指针用得最多的场景。理解了字符串指针,你就看懂了一半的 C 标准库!

3.1 字符串的本质:以 '\0' 结尾的字符数组

在 C 语言中,字符串本质上就是一个字符数组,最后以一个值为 0 的字节('\0')作为结束标志。

|-------------------------------------------------------------------------------------------------------------------|
| c char str[] = "hello"; // 内存中实际是这样的: // 地址:0x100 0x101 0x102 0x103 0x104 0x105 // 值: 'h' 'e' 'l' 'l' 'o' '\0' |

而字符串的名字,就是这个数组首元素的地址!所以:

  • str 等价于 &str[0]
  • 类型是 char*(字符指针)

3.2 案例 1:自己实现 strlen(计算字符串长度)

标准库的strlen函数就是用指针实现的,我们自己写一个:

|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| c #include <stdio.h> // 计算字符串长度 int my_strlen(const char *str) { const char *p = str; // p指向字符串开头 // 只要没遇到'\0'就继续移动 while (*p != '\0') { p++; // 指针向后移动1字节 } // 结束地址 - 开始地址 = 字符个数 return p - str; } int main() { char str[] = "Hello, World!"; printf("字符串:%s\n", str); printf("长度:%d\n", my_strlen(str)); return 0; } |

运行结果

|------------------------------------|
| Plain Text 字符串:Hello, World! 长度:13 |

|--------------------------------------------------|
| 划重点 :两个相同类型的指针相减,得到的是它们之间 "元素的个数",不是字节数! |

3.3 案例 2:自己实现 strcpy(字符串拷贝)

这是笔试高频题,看看标准库是怎么实现的:

|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| c #include <stdio.h> // 字符串拷贝:把src拷贝到dest char* my_strcpy(char *dest, const char *src) { char *p = dest; // 保存目标起始地址 // 经典写法:赋值的同时判断是否结束 while ((*p++ = *src++) != '\0') { // 空循环体,所有逻辑都在条件里了 } return dest; // 返回目标起始地址,支持链式调用 } int main() { char src[] = "Hello, Pointer!"; char dest[100]; // 目标缓冲区 my_strcpy(dest, src); printf("源字符串:%s\n", src); printf("目标字符串:%s\n", dest); return 0; } |

运行结果

|-------------------------------------------------------|
| Plain Text 源字符串:Hello, Pointer! 目标字符串:Hello, Pointer! |

|------------------------------------------------------------------------------------------------------------------------------------------------------|
| 注意 :(*p++ = *src++) 这行代码是 C 语言的经典写法,拆解一下: 1. *src 取出源字符 1. 赋值给 *p 目标位置 1. 判断赋值结果是不是 '\0' 1. src 和 p 各自向后移动一位 |

3.4 char* 和 char [] 的区别

很多初学者搞不清这两个的区别,一句话讲清楚:

|-------------------------------------------------------------------------------------------|
| c char *p = "hello"; // p是指针,指向只读数据段的字符串 char arr[] = "hello"; // arr是数组,在栈上分配空间并拷贝字符串 |

关键区别

  • char *p = "hello":字符串存在只读数据区,不能修改!*p = 'H' 会崩溃
  • char arr[] = "hello":字符串在栈上,可以修改!arr[0] = 'H' 没问题

|---------------------------------------------|
| 避坑提醒 :千万不要修改字符串常量!90% 的初学者都在这里栽过跟头。 |

四、经典案例 3:单链表的指针操作

指针真正的威力体现在动态数据结构上。单链表就是指针 + 动态内存分配的经典应用。

4.1 为什么需要链表?

数组的缺点:

  • 大小固定,不能动态扩展
  • 插入删除元素需要移动大量数据

链表的优点:

  • 按需分配内存,用多少申请多少
  • 插入删除只需要修改指针,O (1) 时间复杂度

4.2 链表的结构定义

|----------------------------------------------------------------------------------------------------------------------------------------------------------|
| c #include <stdio.h> #include <stdlib.h> // malloc, free // 链表节点结构 typedef struct Node { int data; // 数据域 struct Node *next; // 指针域:指向下一个节点 } Node; |

每个节点就像一节火车车厢:装着数据,同时拉着下一节车厢。

|--------------------------------------------------------------------------------------|
| Plain Text 节点1 节点2 节点3 [data|*next] → [data|*next] → [data|*next] → NULL |

4.3 尾插法创建链表

|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| c // 创建新节点 Node* create_node(int data) { Node *new_node = (Node*)malloc(sizeof(Node)); if (new_node == NULL) { printf("内存分配失败!\n"); exit(1); } new_node->data = data; new_node->next = NULL; return new_node; } // 尾插法:在链表末尾添加节点 void append(Node **head, int data) { Node *new_node = create_node(data); // 如果链表为空,新节点就是头节点 if (*head == NULL) { *head = new_node; return; } // 找到最后一个节点 Node *p = *head; while (p->next != NULL) { p = p->next; } // 把新节点挂到最后 p->next = new_node; } |

|----------------------------------------------------------------------------------------------------------------------------|
| 划重点 :为什么参数是 Node **head 而不是 Node *head? 因为我们可能需要修改头指针本身(比如空链表插入第一个节点时)!如果传 Node *head,那又是值传递,修改的是副本,外面的头指针不会变。 |

4.4 遍历链表

|------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| c // 遍历打印链表 void print_list(Node *head) { Node *p = head; printf("链表:"); while (p != NULL) { printf("%d -> ", p->data); p = p->next; } printf("NULL\n"); } |

4.5 释放链表(防止内存泄漏)

|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| c // 释放整个链表 void free_list(Node **head) { Node *p = *head; while (p != NULL) { Node *temp = p; // 先保存当前节点地址 p = p->next; // 再移动到下一个 free(temp); // 最后释放当前节点 } *head = NULL; // 头指针置空 } |

4.6 完整测试

|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| c int main() { Node *head = NULL; // 空链表 // 添加节点 append(&head, 10); append(&head, 20); append(&head, 30); append(&head, 40); // 打印 print_list(head); // 释放 free_list(&head); printf("释放后:"); print_list(head); return 0; } |

运行结果

|------------------------------------------------------------|
| Plain Text 链表:10 -> 20 -> 30 -> 40 -> NULL 释放后:链表:NULL |

|----------------------------------------------------------------------------------------------------------------------|
| 思考 :如果释放链表时不先保存 temp,直接 free(p) 然后 p = p->next 会怎么样? 答案:会崩溃!因为 free(p) 之后,p 指向的内存已经被释放了,再访问 p->next 就是野指针! |

五、指针避坑指南:这些错误 90% 的人都犯过

指针很强大,但也很危险。下面这些坑,踩过一个就可能让你调试一整天!

5.1 野指针:指向 "垃圾内存" 的指针

什么是野指针?

  • 指针变量没有初始化
  • 指针指向的内存已经被释放了
  • 指针越界访问

错误示例

|------------------------------------------------------------------------------------------------------------------------------------------------------|
| c int *p; // 没有初始化!p的值是随机的垃圾值 *p = 10; // 往随机地址写数据!程序直接崩溃 // 或者 int *p = (int*)malloc(sizeof(int)); free(p); // p指向的内存被释放了 *p = 20; // p变成野指针了! |

如何避免

  1. 定义指针时要么初始化,要么置为NULL
  1. free之后立刻把指针置为NULL
  1. 使用指针前检查是否为NULL

5.2 空指针解引用:最常见的崩溃原因

NULL是一个特殊的地址(通常是 0x0),表示 "不指向任何有效内存"。

错误示例

|----------------------------------------------|
| c int *p = NULL; *p = 10; // 空指针解引用!100%崩溃 |

如何避免:使用指针前一定要判空!

|--------------------------------------|
| c if (p != NULL) { *p = 10; // 安全 } |

5.3 内存泄漏:malloc 和 free 不配对

只malloc不free,内存就会被白白占用,直到程序结束。

错误示例

|--------------------------------------------------------------------------------------|
| c void func() { int *p = (int*)malloc(100 * sizeof(int)); // 使用p... // 忘记free了! } |

每次调用func就泄漏 400 字节,调用 100 万次就泄漏 400MB!

如何避免

  • 谁申请谁释放,malloc 和 free 一定要成对出现
  • 使用工具检测:valgrind、AddressSanitizer

5.4 指针越界:缓冲区溢出的元凶

指针移动超出了合法范围,访问到不该访问的内存。

错误示例

|-----------------------------------------------------------------------------------------------------------------------|
| c int arr[5] = {1, 2, 3, 4, 5}; int *p = arr; for (int i = 0; i <= 5; i++) { // i=5时就越界了! printf("%d ", *p++); } |

后果:轻则数据错乱,重则程序崩溃,甚至被黑客利用进行缓冲区溢出攻击!

六、结语:指针学习方法论

看到这里,相信你对指针已经有了全新的认识。最后给大家几个学习建议:

1. 多画图,少空想

指针之所以难,是因为它操作的是看不见摸不着的内存。遇到问题先画图! 把每个变量的地址、值、指向关系都画出来,比盯着代码想半小时管用。

2. 多调试,看实际值

在调试器里观察指针的值变化:

  • 看 p 的值是多少(地址)
  • 看 * p 的值是多少(指向的数据)
  • 看 p+1 之后地址增加了多少(验证步长)

3. 多写代码,踩坑成长

指针这东西,看 10 遍不如写 1 遍。不要害怕报错,每个错误都是你理解指针的机会。

4. 进阶学习路线

掌握了基础指针后,可以继续学习:

  • 指针数组和数组指针
  • 函数指针和回调函数
  • 二级指针和多级指针
  • 指针和 const 的各种组合

最后想说:指针是 C 语言给程序员的最大礼物。它让我们拥有了直接操控内存的能力,这既是自由,也是责任。用好指针,你就能写出高效、优雅的 C 语言代码;用不好指针,它就是 bug 制造机。

但请记住:所有的 C 语言大神,都是从无数次 "段错误" 中爬出来的。加油,未来的 C 语言大神!

如果这篇文章对你有帮助,欢迎点赞收藏,有问题评论区交流!

相关推荐
WL_Aurora1 小时前
Python 算法基础篇之树和二叉树
python·算法
Cyan_RA91 小时前
SpringMVC 请求数据绑定与参数映射 详解
java·后端·spring·mvc·springmvc·映射请求数据
txzrxz1 小时前
关于前缀和
算法·动态规划·图论
杨连江1 小时前
载流子矩阵限域束缚实现常温常压超导的理论与结构设计
算法
逻辑驱动的ken1 小时前
Java高频面试考点场景题20
java·开发语言·深度学习·面试·职场和发展
bzmK1DTbd1 小时前
Java游戏服务器:Netty框架的高并发网络通信
java·服务器·游戏
longxibo1 小时前
【Flowable 7.2 源码深度解析与实战-前言】
java·后端·流程图
做cv的小昊1 小时前
【TJU】研究生应用统计学课程笔记(6)——第二章 参数估计(2.4 区间估计)
人工智能·笔记·线性代数·算法·机器学习·数学建模·概率论
普贤莲花2 小时前
【2026年第18周---写于20260501】---舍得
程序人生·算法·leetcode