作为一个正在啃 C 语言的大二学生,我曾经和绝大多数初学者一样,对指针充满了恐惧。老师说 "指针是 C 语言的灵魂",但我当时只觉得它是 "劝退 C 语言的灵魂"。什么*&傻傻分不清,什么数组名退化为指针,什么二级指针、函数指针,每一个概念都像天书一样。

直到我花了整整一周时间,把所有指针相关的代码都敲了一遍,对着 VS 的内存窗口一点点看变量的地址和值的变化,才突然有种 "开窍" 的感觉。原来指针一点都不神秘,它只是 C 语言给我们提供的一把直接操作内存的钥匙。

今天我就以一个初学者的视角,把我对指针的理解和学习过程整理出来,希望能帮到同样在被指针折磨的你。

一、指针知识框架思维导图

plaintext

C语言指针
├── 基础概念
│   ├── 内存与地址
│   ├── 指针的本质:存储地址的变量
│   ├── 指针的类型
│   └── 两个关键运算符:&(取地址) 和 *(解引用)
├── 基本操作
│   ├── 指针的定义与初始化
│   ├── 指针的赋值
│   ├── 指针的解引用
│   └── 指针的算术运算(+、-、++、--)
├── 指针与数组
│   ├── 数组名的本质:指向首元素的常量指针
│   ├── 用指针访问数组元素
│   ├── 数组作为函数参数的退化规则
│   ├── 指针数组 vs 数组指针
│   └── 二维数组与指针
├── 指针与函数
│   ├── 传值调用 vs 传址调用
│   ├── 函数参数为指针
│   ├── 函数返回指针
│   └── 函数指针
├── 进阶指针
│   ├── 二级指针(指向指针的指针)
│   ├── 空指针(NULL)
│   ├── 泛型指针(void*)
│   └── const修饰的指针
└── 常见陷阱与避坑指南
    ├── 野指针
    ├── 悬空指针
    ├── 数组越界访问
    ├── 返回局部变量的地址
    └── 指针类型不匹配

二、打破第一个认知误区:指针不是地址!

这是我刚学指针时犯的第一个,也是最致命的错误。我当时死记硬背 "指针就是地址",结果越学越乱。

正确的理解是:指针是一个变量,它的值是另一个变量的内存地址。

我给大家打一个非常形象的比喻:

  • 计算机的内存就像一个大酒店,每个房间都有一个唯一的房间号(这就是内存地址
  • 每个房间里住着一个客人(这就是变量的值
  • 指针,就是一张写着某个房间号的小纸条

你拿着这张小纸条,就能找到对应的房间,见到房间里的客人。这就是指针的全部意义。

我们用代码来验证一下:

c

运行

#include <stdio.h>

int main() {
    int a = 10; // 开了一个叫a的房间,里面住了客人10
    int* p = &a; // 开了一个叫p的房间,里面放了a的房间号

    printf("a的值:%d\n", a); // 直接去a房间看客人:10
    printf("a的地址:%p\n", &a); // 问a的房间号:比如0x7ffeefbff5ac
    printf("p的值:%p\n", p); // 去p房间看纸条上的内容:和a的地址一样
    printf("p自己的地址:%p\n", &p); // p自己也有房间号:比如0x7ffeefbff5a0
    printf("通过p找到a的值:%d\n", *p); // 按照p纸条上的房间号去找客人:10

    return 0;
}

我的感悟:当我第一次运行这段代码,看到p的值和&a一模一样的时候,我突然就懂了。原来指针真的只是一个普通的变量,只不过它存的不是整数、不是字符,而是另一个变量的地址。

三、为什么指针要有类型?

这是我当时第二个困惑的点。既然指针存的都是地址,为什么还要分int*char*double*?直接统一叫pointer不行吗?

答案是:指针的类型决定了两件事

  1. 当你解引用这个指针时,编译器会访问多少个字节的内存
  2. 当你对指针进行算术运算时,一次移动多少个字节

我们再用代码说话:

c

运行

#include <stdio.h>

int main() {
    int a = 0x12345678;
    int* p_int = &a;
    char* p_char = (char*)&a;

    printf("解引用int*:%x\n", *p_int); // 访问4个字节:12345678
    printf("解引用char*:%x\n", *p_char); // 只访问1个字节:78(小端模式)

    printf("p_int + 1:%p\n", p_int + 1); // 地址增加4个字节
    printf("p_char + 1:%p\n", p_char + 1); // 地址只增加1个字节

    return 0;
}

我的感悟:这个例子彻底让我明白了指针类型的重要性。如果没有类型,编译器根本不知道你要访问多少内存,也不知道指针移动一步该走多远。这就像你拿着一张写着房间号的纸条,但不知道这个房间是单人间、双人间还是套房,你根本没法正确地找到客人。

四、最容易混淆的 "双胞胎":数组与指针

这绝对是初学者的重灾区,也是考试最常考的知识点。很多人学完指针,还是分不清数组名和指针到底有什么区别。

4.1 数组名的本质

数组名是指向数组首元素的常量指针。注意两个关键词:

  • 指向首元素arr 等价于 &arr[0]
  • 常量指针:你不能修改数组名的值,也就是不能让数组名指向别的地方

4.2 数组名和指针的区别

它们最大的区别体现在sizeof运算符上:

  • sizeof(数组名):得到整个数组的总字节数
  • sizeof(指针):得到指针变量本身的字节数(32 位系统 4 字节,64 位系统 8 字节)

代码验证:

c

运行

#include <stdio.h>

int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    int* p = arr;

    printf("sizeof(arr) = %zu\n", sizeof(arr)); // 5*4=20字节
    printf("sizeof(p) = %zu\n", sizeof(p)); // 8字节(64位系统)

    return 0;
}

4.3 最重要的规则:数组作为函数参数时会退化为指针

这就是我们上一道题的考点!当你把数组名传给一个函数时,编译器不会把整个数组传过去,而是只传数组首元素的地址。

所以:

  • void test(int arr[5]) 会被编译器自动调整为 void test(int* arr)
  • void test(char* arr[5]) 会被编译器自动调整为 void test(char** arr)

这就是为什么上一道题中 B 和 D 都是正确的。

我的感悟:我之前一直不明白为什么函数参数里写数组长度没用,直到我知道了这个退化规则。原来编译器根本不关心你写的数组长度是多少,它只会把它当成指针处理。

五、初学者必踩的 5 个指针陷阱

我把我自己踩过的,以及身边同学踩过最多的指针陷阱整理出来,大家一定要避开:

陷阱 1:野指针

定义:未初始化的指针,它的值是随机的,指向一块未知的内存。

c

运行

// 错误写法
int* p; // p没有初始化,是野指针
*p = 10; // 向随机内存写入数据,可能导致程序崩溃

// 正确写法
int a = 10;
int* p = &a; // 初始化时就指向合法的内存

陷阱 2:空指针解引用

定义:指向NULL的指针,NULL表示指针不指向任何有效的内存。

c

运行

// 错误写法
int* p = NULL;
*p = 10; // 解引用空指针,程序一定会崩溃

// 正确做法:使用指针前先判断是否为NULL
if (p != NULL) {
    *p = 10;
}

陷阱 3:数组越界访问

c

运行

// 错误写法
int arr[5] = {1, 2, 3, 4, 5};
int* p = arr;
for (int i = 0; i <= 5; i++) { // i=5时越界
    printf("%d ", *(p + i));
}

数组越界是 C 语言中最常见的 bug 之一,它可能不会立即导致程序崩溃,但会破坏其他变量的值,造成非常诡异的错误。

陷阱 4:返回局部变量的地址

c

运行

// 错误写法
int* test() {
    int a = 10;
    return &a; // a是局部变量,函数结束后内存被释放
}

int main() {
    int* p = test();
    printf("%d\n", *p); // 访问已经被释放的内存,结果是未定义的
    return 0;
}

陷阱 5:指针类型不匹配

c

运行

// 错误写法
int a = 10;
char* p = &a; // int* 赋值给 char*,类型不匹配
*p = 20; // 只会修改a的第一个字节

六、我的学习感悟与建议

经过这一周的 "指针攻坚战",我总结出了几个对初学者非常有用的学习方法:

  1. 不要死记硬背语法,要理解内存模型指针的所有特性都源于内存的工作方式。如果你能在脑子里建立起内存的模型,把每个变量、每个指针都对应到内存中的一个位置,那么所有指针的问题都会迎刃而解。

  2. 多写代码,多调试这是最重要的一点!指针是一门实践的学问,光看书是永远学不会的。我建议大家把我上面写的所有代码都敲一遍,然后用 VS 或者 GDB 的调试功能,一步步看每个变量的值、地址的变化。特别是内存窗口,一定要打开看看,你会对指针有全新的认识。

  3. 遇到不懂的地方,画内存图当你搞不清指针指向哪里的时候,拿一张纸,画几个方框代表内存,写上地址和值,然后用箭头表示指针的指向。画完之后,你会发现原来这么简单。

  4. 不要害怕指针很多初学者一看到指针就头疼,直接跳过。但指针是 C 语言的核心,你绕不开它。而且当你真正掌握了指针之后,你会发现它是 C 语言最强大、最灵活的工具。很多其他语言做不到的事情,用指针都能轻松实现。

写在最后

现在回头看,我觉得指针之所以难,不是因为它本身有多复杂,而是因为它要求我们从一个全新的角度去思考程序 —— 从 "操作变量" 变成 "操作内存"。这是一个思维方式的转变,需要一点时间来适应。

但只要你迈过了这道坎,你就会发现 C 语言的大门真正向你打开了。你会理解为什么 C 语言能成为操作系统、嵌入式开发的首选语言,你会感受到那种直接操控计算机硬件的快感。

希望这篇博客能帮到正在学习指针的你。如果有什么不懂的地方,或者有不同的理解,欢迎在评论区和我交流。我们一起学习,一起进步!

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐