「收藏级」指针的前世今生:写给所有被C/C++折磨过的人

大家好,我是小康。今天聊聊让编程新手头疼的"指针"——这个 C 语言第一难点究竟是什么,为什么会被发明出来?

从直接操作内存到编程语言的"导航员"

你有没有过这样的经历:学习编程时,一切都还算顺利,直到遇见了"指针"这个概念,突然感觉像遇到了一道难以逾越的高坎?(我第一次接触指针时也是这样,一脸懵圈...

"指针是变量的地址?"

"指针是指向内存的变量?"

"为什么要用指针?没有指针不行吗?"

如果你也有这些疑问,那么今天这篇文章就是为你准备的。我们不打算用晦涩的技术语言解释指针,而是要讲一个故事:指针是怎样一步步被发明出来的

听完这个故事,你会发现,原来指针就像我们生活中的门牌号和导航,是那么简单自然的存在!

微信搜索 「跟着小康学编程」,关注我,后续还有更多硬核技术文章分享,带你玩转 Linux C/C++ 编程!😆

计算机内存:一条超长的街道

想象一下,计算机的内存就像一条超长的街道,街道上有成千上万的房子,每个房子都有自己的门牌号(地址)。

在计算机里,这些"房子"被称为内存单元,每个内存单元都可以存储一个数据。计算机通过门牌号(内存地址)来找到并操作这些数据。

现在,让我们回到计算机发展的早期,看看指针是如何逐步被发明出来的。

阶段一:最原始的数据存储方式

在计算机发展的早期阶段,程序员主要使用机器语言和汇编语言编程。在这些低级语言中,程序员需要直接操作内存地址。

比如,要存储数字 42 到特定内存位置,汇编语言可能会这样写:

MOV [1000], 42    ; 将数值42存入内存地址1000

要取出这个数字:

MOV AX, [1000]    ; 从地址1000读取数据到寄存器AX

这种直接操作内存地址的方式虽然给了程序员极大的控制权,但也极其麻烦。

想象一下,你的程序中有上百个数据,你需要记住每个数据分别存在哪个具体地址,这简直是噩梦!而且地址一旦写错,程序就会莫名其妙地崩溃。

看看这张简单的内存布局图:

内存地址    |    数据
-----------------------
1000       |    42      <-- 存放了数字42
1001       |    ?       <-- 其他数据
1002       |    ?
...        |    ...
2000       |    100     <-- 存放了数字100
...        |    ...

程序员需要记住:数字 42 在地址1000,数字 100 在地址 2000...太麻烦了!

阶段二:变量的诞生

为了解决上面的问题,聪明的程序员发明了"变量"。

变量就像是给内存地址贴上的标签。我们不再需要记住"地址1000",而是可以说:

int age = 42;

这里,age是一个变量名,编译器会自动为它分配一个内存地址(比如1000),并在那里存储数值 42。

当我们需要使用这个数据时,只需要写age,而不是"地址1000",编译器会自动帮我们找到正确的地址。

现在内存布局变成了这样:

内存地址    |    数据     |    变量名
----------------------------------------
1000       |    42      |    age      <-- 不用记地址,用变量名就行
1001       |    ?       |    
1002       |    ?       |
...        |    ...     |
2000       |    100     |    salary   <-- 同样用变量名引用
...        |    ...     |

这下舒服多了!但是,新的问题又来了。

阶段三:变量的局限性——共享数据的难题

变量确实解决了不少问题,但随着程序变得复杂,程序员们发现仅仅使用变量还不够。特别是当多个函数需要共享和修改同一份数据时,问题就来了。

看看下面这个简单的例子:

// 两个函数,都试图给一个数字加 1
void func1(int a) { a = a + 1; printf("在func1中,a = %d\n", a);  // 这里a等于3
}void func2(int b) { b = b + 1; printf("在func2中,b = %d\n", b);  // 这里b等于3
}int main() {int num = 2;func1(num);  // 调用func1,传入numfunc2(num);  // 调用func2,传入numprintf("最后num = %d\n", num);  // 奇怪,num还是2!return 0;
}

运行这段代码,你会发现一个奇怪的现象:虽然func1func2都把传入的值加了1,但最后num的值仍然是2,没有变化!

这是为什么呢?因为在C语言(和许多其他语言)中,当我们把变量传给函数时,传递的是变量的值的复制品,而不是变量本身。func1func2各自得到了num的一个副本,它们修改的是副本,而不是原始的num

下面是这个过程的图解:

+-------------+       +------------+       +-------------+
|    main     |       |   func1    |       |    main     |
|  函数内存    |  复制 |  函数内存   |       |   函数内存   |
|             | ----->|            |      |              |
| num = 2     |  值   | a = 2      |      | num = 2      |
|             |       |            |       |     ^        |
+-------------+       +------------+       +------|-------+|                    |a加1操作               没有变化↓+------------+|    func1   ||  函数内存   ||            || a = 3      ||            |+------------+

同理,当调用func2时,又创建了一个新的副本,对这个副本的修改也不会影响原始的num

+-------------+       +------------+       +-------------+
|    main     |       |   func2    |       |    main     |
|  函数内存   |  复制  |  函数内存   |       |   函数内存   |
|             | ----->|            |       |             |
| num = 2     |  值   | b = 2      |       | num = 2     |
|             |       |            |       |     ^       |
+-------------+       +------------+       +------|------+|                    |b加1操作               没有变化↓+------------+|    func2   ||  函数内存   ||            || b = 3      ||            |+------------+

这就带来了一个问题:如果多个函数需要共同操作同一个数据,该怎么办?

程序员们思考着:有没有一种方法,可以让函数直接访问和修改原始数据,而不是它的副本?

阶段四:指针的诞生 — 传递地址解决共享问题

为了解决上面的问题,聪明的程序员引入了一个革命性的概念:指针!

指针本质上就是一个存储内存地址的变量。它就像是一张写有门牌号的纸条,告诉你:"嘿,你要找的东西在这个地址!"

让我们用指针来改造前面的例子:

// 现在函数参数变成了指针(注意那个星号)
void func1(int *a) { *a = *a + 1;  // 通过指针修改原始数据printf("在func1中,*a = %d\n", *a);  // 现在是3
}void func2(int *b) { *b = *b + 1;  // 通过指针修改原始数据printf("在func2中,*b = %d\n", *b);  // 现在是4
}int main() {int num = 2;func1(&num);  // 传递num的地址func2(&num);  // 传递num的地址printf("最后num = %d\n", num);  // 现在num变成了4!return 0;
}

神奇的事情发生了!这次num的值真的改变了,从 2 变成了 4。为什么呢?

下面是使用指针时的图解:

+-------------+       +------------+       +-------------+
|    main     |       |   func1    |       |    main     |
|  函数内存    |  传递 |  函数内存   |       |   函数内存   |
|             | ----->|            |       |              |
| num = 2     |  地址 | a = &num   |------>| num = 3      |
|  ^          |       |            |  修改 |              |
+--|----------+       +------------+  原值  +-------------+|                      |    |             ^|                      |    |             ||                   *a 操作  |             ||                      |    |             |+----------------------+    +-------------+

当我们调用func2时,同样是传递地址,并通过这个地址修改原始的num值:

+-------------+       +------------+       +-------------+
|    main     |       |   func2    |       |    main     |
|  函数内存   |  传递  |  函数内存   |       |   函数内存   |
|             | -----> |            |       |             |
| num = 3     |  地址  | b = &num   |------>| num = 4     |
|  ^          |       |            |  修改  |             |
+--|----------+       +------------+  原值  +-------------+|                      |    |             ^|                      |    |             ||                   *b 操作  |             ||                      |    |             |+----------------------+    +-------------+

与传值不同,这次我们传递的是num的地址(&num)。函数接收到这个地址后,可以通过指针(*a*b)直接访问和修改原始的num变量。

这就好比:我不给你我家的钥匙副本,而是直接告诉你我家的地址,你可以直接来我家拿东西或放东西。

这就是指针的核心作用之一:让多个函数可以共享和修改同一个数据

当然,指针的用途远不止于此。它还能帮我们解决更多复杂的问题,比如处理大量数据时节省内存。想想看,与其复制一大堆数据,不如只传递一个小小的地址,这样效率高多了!

微信搜索 「跟着小康学编程」,关注我,后续还有更多硬核技术文章分享,带你玩转 Linux C/C++ 编程!😆

图解指针的实战演练

让我们通过一个简单的例子,来实际感受一下指针的使用:

#include <stdio.h>int main() {int number = 42;      // 一个普通变量int *pointer = &number; // 指针变量,存储number的地址printf("number的值: %d\n", number);     // 输出:42printf("number的地址: %p\n", &number);  // 输出类似:004FFD98printf("pointer存储的地址: %p\n", pointer); // 输出同上:004FFD98printf("pointer指向的值: %d\n", *pointer);  // 输出:42// 通过指针修改number的值*pointer = 100;printf("修改后number的值: %d\n", number); // 输出:100return 0;
}

这个例子展示了指针的基本操作:

  • &number:获取变量 number 的地址
  • int *pointer:声明一个指向 int 类型的指针
  • pointer = &number:让指针存储 number 的地址
  • *pointer:访问指针指向的值(这叫做"解引用")

通过*pointer = 100,我们改变了指针所指向地址上的值,因此 number 的值也变成了100。

为了更直观地理解指针,我们可以画一张简图:

内存地址    内容               变量名
---------------------------------------------------
0x004FFD98   | 42 (后来变成100) | number
---------------------------------------------------
0x114FFD98   | 0x004FFD98      | pointer (存储的是 number 的地址)
---------------------------------------------------

当我们写*pointer = 100时,计算机会:

  1. 查看 pointer 变量的值(0x004FFD98)
  2. 找到地址 0x004FFD98
  3. 把那里的值改为 100

这就是为什么 number 的值也会变成100!

生动实例:理解指针的本质

让我们用一个更生活化的例子来理解指针:

假设你和朋友约好去一家新开的餐厅吃饭。你可以有两种方式告诉朋友餐厅在哪:

  1. 详细描述餐厅的样子、菜单、服务员长相等(相当于复制数据本身)
  2. 直接发个定位或地址给他(相当于传递指针)

显然,第二种方式更简单高效!

在代码中也是如此:

// 方式 1:复制整个结构体
void sendRestaurantInfo1(Restaurant r) {// 这里 r 是原始餐厅数据的一个完整复制品
}// 方式 2:只传递"地址"(指针)
void sendRestaurantInfo2(Restaurant *r) {// 这里 r 只是一个指向原始餐厅数据的指针
}

方式 2 不仅传递的数据量更小,而且如果函数中修改了餐厅信息,原始数据也会被更新——因为我们操作的就是原始数据所在的地址!

阶段五:指针的进阶用法 — 动态内存分配

随着编程的发展,程序员发现了指针的另一个强大用途:动态内存分配。

我们都知道在 C 语言中,定义数组时必须指定一个固定的大小:

int numbers[100];  // 只能存储100个整数,多一个都不行!

但如果事先不知道需要多少空间怎么办?比如,用户输入一个数字 n,我们需要创建一个包含 n 个元素的数组?

这时,指针和动态内存分配就派上了用场:

int n;
printf("请输入需要的数组大小:");
scanf("%d", &n);// 动态分配n个int大小的内存空间
int *dynamicArray = (int*)malloc(n * sizeof(int));// 使用这个动态数组
for (int i = 0; i < n; i++) {dynamicArray[i] = i * 2;
}// 使用完毕后释放内存
free(dynamicArray);

让我们用图解来理解这个过程:

输入 n = 5 后:+-------------+          malloc           +-----------------+
|             |                           |                 |
| dynamicArray| ----------------------->  | [0][1][2][3][4] |
|  (指针变量)  |     分配5个整数大小的内存   |  (堆上的内存块)  |
|             |                           |                 |
+-------------+                           +-----------------+↑动态分配的内存(可以根据需要变化大小)

在这个例子中,我们通过malloc函数在堆上分配了一块内存,并得到了这块内存的起始地址,存储在指针dynamicArray中。

程序运行时才决定分配多少内存,用完后还可以释放它——这就是动态内存分配的魅力,而这一切都是通过指针实现的!

没有指针,我们就无法实现这种"按需分配"的内存管理方式,程序的灵活性会大大降低。

阶段六:复杂数据结构的实现 — 指针的终极应用

到目前为止,我们已经看到了指针如何帮助函数共享数据以及实现动态内存分配。但指针的故事还没有结束,它的最强大之处在于使各种复杂数据结构成为可能。

还记得我们小时候玩过的寻宝游戏吗?一条线索指向下一条,最终找到宝藏。在编程中,这种"一个指向另一个"的结构就是通过指针实现的!

链表 — 数组的灵活替代品

数组的一个大问题是:一旦创建,就无法轻松地插入或删除中间的元素。而链表解决了这个问题:

// 定义链表节点
struct Node {int data;           // 节点中存储的数据struct Node *next;  // 指向下一个节点的指针
};// 在链表头部插入新节点
struct Node* insertAtBeginning(struct Node *head, int value) {// 创建新节点struct Node *newNode = (struct Node*)malloc(sizeof(struct Node));newNode->data = value;// 将新节点链接到原链表头部newNode->next = head;// 返回新的链表头部return newNode;
}

图解一下链表的结构:

+--------+    +--------+    +--------+    +--------+
|  数据1  |   |  数据2  |    |  数据3  |    |  数据4  |
|        |    |        |    |        |    |        |
| 指针   |--->| 指针    |--->| 指针    |--->| 指针    |--->其他节点
+--------+    +--------+    +--------+    +--------+节点1         节点2         节点3         节点4

就像一列火车车厢,每个车厢(节点)不仅存放数据,还通过指针"勾住"下一个车厢。这种结构让我们可以在任意位置轻松地插入或删除节点,而不需要像数组那样移动大量数据。

树、图等更复杂的数据结构

除了链表,指针还使树、图等更复杂的数据结构成为可能。例如,二叉树的每个节点可以有左右两个"孩子"节点:

struct TreeNode {int data;struct TreeNode *left;   // 指向左子节点的指针struct TreeNode *right;  // 指向右子节点的指针
};
          +--------+|  根节点 ||        |+--+---+---+--+|             |↓             ↓+----+          +----+| 左 |          | 右 |+----+          +----+

这些复杂数据结构在现代编程中极其重要,它们构成了数据库、操作系统、游戏引擎等几乎所有复杂软件的基础。而这一切,都是因为有了指针这个简单却强大的概念!

指针到底是个啥?豁然开朗时刻

经过这一路的探索,我们终于可以给指针下一个通俗易懂的定义了:

指针就是存储内存地址的变量,它"指向"另一个数据的位置。

就像门牌号告诉你一栋房子在哪,指针告诉程序一个数据在哪。它不是数据本身,而是数据的"地址"。

指针的价值在于:

  1. 可以直接访问和修改指向的数据
  2. 传递大数据时只需传递一个小小的地址
  3. 可以实现动态内存分配
  4. 让复杂的数据结构(如链表、树)成为可能
  5. 提高程序的执行效率

历史小插曲:C语言与指针的故事

说到指针,就不得不提一下C语言。1972年,贝尔实验室的丹尼斯·里奇(Dennis Ritchie)在开发C语言时,将指针作为核心特性引入。

为什么?简单来说,计算机资源那时非常有限,而指针恰好能同时满足两个目标:

  • 提供像汇编语言一样直接操作内存的能力(高效)
  • 提供比汇编更好的抽象和可读性(易用)

C语言通过指针实现了高效和易用的完美平衡,这也是为什么几十年过去了,C语言仍然是操作系统、嵌入式系统等领域的主力军。

有趣的是,虽然 Java、Python 等现代语言隐藏了指针细节,但在它们的底层实现中,指针的概念依然无处不在!

结语:指针不再可怕

现在,你对指针有了更清晰的认识了吧?它不再是那个可怕的编程概念,而是一个解决实际问题的实用工具。

指针的本质很简单:就是存储地址的变量。它之所以被发明,是为了解决函数间数据共享、内存管理和复杂数据结构的实际需求。

从最初的直接操作内存地址,到变量的发明,再到指针的出现,我们看到了编程技术的不断进步。下次当你遇到指针时,不妨想象它就是一个写着门牌号的纸条,告诉你:"嘿,你要找的东西在这个地址!"

相信我,当你真正理解了指针的本质,你会发现它其实很简单,而且非常有用!


你还有哪些关于指针的疑问?欢迎在评论区留言,我们一起探讨!

嗨,我是小康,平时喜欢把复杂的技术问题讲得通俗易懂。看了这篇文章,希望指针不再是你学习路上的"拦路虎"!😊

如果你也对 计算机网络、操作系统、C/C++后端开发或大厂面试题 感兴趣,欢迎关注我的公众号「跟着小康学编程」。我会继续用生活化的例子和清晰的思路,帮你理解那些看似复杂的编程概念。

不管你是新手还是有经验的程序员,相信都能在这里有所收获!还有什么编程概念让你困惑?欢迎在评论区告诉我,说不定下一篇文章就是为你而写的!

如果觉得这篇文章对你有帮助,别忘了点赞、收藏、关注哦,或分享给你的程序员朋友们!你的每一次互动,都是我创作的最大动力!

怎么关注我的公众号?

扫下方公众号二维码即可关注。

另外,小康还建了一个技术交流群,专门聊技术、答疑解惑。如果你在读文章时碰到不懂的地方,随时欢迎来群里提问!我会尽力帮大家解答,群里还有不少技术大佬在线支援,咱们一起学习进步,互相成长!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/907500.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

VLM-3D空间理解

VLM-3D空间理解 CoT Thinking in spaceNotably, prevailing linguistic reasoning techniques (e.g., chain-of-thought, self-consistency, tree-of-thoughts) fail to improve performance, whereas explicitly generating cognitive maps during question-answering enhance…

PCR406-ASEMI智能家居专用PCR406

PCR406-ASEMI智能家居专用PCR406编辑:LL PCR406-ASEMI智能家居专用PCR406 型号:PCR406 品牌:ASEMI 封装:TO-92 阻断电压:600V 通态电流:0.6A 类型:单向可控硅 浪涌电流:10A 工作温度:-40℃~150℃ 灵敏触发与低功耗‌ 触发电流低至‌5~200μA‌(分段可选),门极触发电…

SvelteKit 最新中文文档教程(14)—— 错误处理

前言 Svelte,一个语法简洁、入门容易,面向未来的前端框架。 从 Svelte 诞生之初,就备受开发者的喜爱,根据统计,从 2019 年到 2024 年,连续 6 年一直是开发者最感兴趣的前端框架 No.1:Svelte 以其独特的编译时优化机制著称,具有轻量级、高性能、易上手等特性,非常适合构…

银行网点加钞间作业人数异常报警系统

银行网点加钞间作业人数异常报警系统检测银行网点加钞间区域内作业人数,对超过设定人数阈值,进行预警。加钞间少于2人进行报警,规范员工操作,落实银行制度。自助环境区域、防护舱内区域重点关注大于等于2人,预防案件发生。通过人脸识别比对系统,对加钞间非授权人员进行检…

RAGFlow部署

参照https://eogee.com/article/detail/17进行 其中主要就是放开ragflow/docker/.env文件中的RAGFLOW_IMAGE华为云镜像地址 修改ragflow/docker/docker-compose.yml文件中的端口 Q:访问网页注册登录无反应 打开控制台看是报了502的错误 A:确保mysql,redis,minio,es等先启动,然…

多模态AI核心技术:CLIP与SigLIP技术原理与应用进展

近年来,人工智能领域在多模态表示学习方面取得了显著进展,这类模型通过统一框架理解并整合不同数据类型间的语义信息,特别是图像与文本之间的关联性。在此领域具有里程碑意义的模型包括OpenAI提出的CLIP(Contrastive Language-Image Pre-training,对比语言-图像预训练)和…

在岗人数分析报警摄像机

在岗人数分析报警摄像机采用AI算法,通过大量真实的场景样本训练后,能够在各种应用场景下及时准确地对场景中人员数量的分析统计预警。在岗人数分析报警摄像机是可以实时分析一个指定值班域内的人员数量,当所监视区域的值班人员数量少于设定的阀值时摄像机输出开关量信号,可…

工厂车间在岗人数分析预警系统

工厂车间在岗人数分析预警系统基于AI人工智能分析技术,将车间生产区域员工在岗人数进行管理预警,可以实现对厂区的全面覆盖,全天候保障厂区生产安全,建立完善长效的安全生产运营机制。在岗人数分析预警系统嵌入AI人员离岗分析算法,可对车间生产区域内人员的数量进行精确分…

虚拟机vmware16 安装centos8.5 你全流程和详细配置

因为centos8.5 不能使用他自己的源,所以新装的系统默认不能安装软件,需要做一些配置 其二。部份虚拟机新装的不能连网,所以我将今天安装的流程记录以下,如果正好有别人需要,可以看一下我安装的流程第一步,如果不能上网配置。先装虚拟机配成桥接试试,可能是默认上nat 那…

FaceBook OAuth2登录配置

应用必须上架才能正式使用FaceBook developers 后台添加应用配置https://developers.facebook.com 创建应用,應用程式設定 -》 添加IOS包名,添加安卓包名, 應用程式編號,應用程式密鑰 对应服务端配置: FacebookLoginAppId FacebookLoginAppSecret 首次登录访问提示错误,错…

itdog-ping

地址 https://www.itdog.cn/ping/ 概览作者:mohistH 出处:https://www.cnblogs.com/pandamohist/ 本文版权归作者和博客园共有,谢绝一切形式的转载,否则将追究法律责任。

某客户RAID出现Multi-bit ECC error错误

某客户反馈,BMC带外有Major级别的告警,告警内容如下可以看出SEL指向RAID卡,产生告警时间为2024年11月14日。检查RAID卡日志,找2024年11月14号发生的事件。从RAID卡事件可以看出来RAID卡从2024年11月9号到2024年11月14号,5天内并没有记录任何日志(如下图)一直到2024年11月…