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

「收藏级」指针的前世今生:写给所有被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,传入num

func2(num); // 调用func2,传入num

printf("最后num = %d\n", num); // 奇怪,num还是2!

return 0;

}

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

这是为什么呢?因为在C语言(和许多其他语言)中,当我们把变量传给函数时,传递的是变量的值的复制品,而不是变量本身。func1和func2各自得到了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

int main() {

int number = 42; // 一个普通变量

int *pointer = &number; // 指针变量,存储number的地址

printf("number的值: %d\n", number); // 输出:42

printf("number的地址: %p\n", &number); // 输出类似:004FFD98

printf("pointer存储的地址: %p\n", pointer); // 输出同上:004FFD98

printf("pointer指向的值: %d\n", *pointer); // 输出:42

// 通过指针修改number的值

*pointer = 100;

printf("修改后number的值: %d\n", number); // 输出:100

return 0;

}

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

&number:获取变量 number 的地址

int *pointer:声明一个指向 int 类型的指针

pointer = &number:让指针存储 number 的地址

*pointer:访问指针指向的值(这叫做"解引用")

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

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

内存地址 内容 变量名

---------------------------------------------------

0x004FFD98 | 42 (后来变成100) | number

---------------------------------------------------

0x114FFD98 | 0x004FFD98 | pointer (存储的是 number 的地址)

---------------------------------------------------

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

查看 pointer 变量的值(0x004FFD98)

找到地址 0x004FFD98

把那里的值改为 100

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

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

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

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

详细描述餐厅的样子、菜单、服务员长相等(相当于复制数据本身)

直接发个定位或地址给他(相当于传递指针)

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

在代码中也是如此:

// 方式 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; // 指向右子节点的指针

};

+--------+

| 根节点 |

| |

+--+---+---+--+

| |

↓ ↓

+----+ +----+

| 左 | | 右 |

+----+ +----+

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

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

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

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

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

指针的价值在于:

可以直接访问和修改指向的数据

传递大数据时只需传递一个小小的地址

可以实现动态内存分配

让复杂的数据结构(如链表、树)成为可能

提高程序的执行效率

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

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

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

提供像汇编语言一样直接操作内存的能力(高效)

提供比汇编更好的抽象和可读性(易用)

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

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

结语:指针不再可怕

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

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

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

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

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

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

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

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

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

怎么关注我的公众号?

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

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

相关手记

菠菜365定位 德国黑森林 10 个值得参观的地方
beat365手机版客户端ios 王国纪元四级魔物多久能出
beat365手机版客户端ios 手抓饼酱配方

手抓饼酱配方

07-27 👁️ 6210
菠菜365定位 租车位在哪个平台好租一些
菠菜365定位 宝马遥控器电池怎么换,自己换,还是去4S
365APP 使用PE装载Windows7系统的详细教程(在PE环境下一步一步安装Windows7系统,让你轻松搞定)