☰
C语言指针本质与用法:从内存地址到指针变量,一篇讲透
2026/10/10 7:53:01 网站建设 项目流程

很多学C语言的人,学到指针这一章就开始犯怵。网上一直流传着“指针很难”“指针劝退”的说法,甚至有人把指针和“灵异事件”联系在一起。但如果你真的动手写过几段代码,把指针的来龙去脉捋清楚,会发现它其实就是一个“存放地址的变量”,本质上和int、char没什么区别。今天这篇就把指针的概念、指针变量的定义、使用场景和日常踩坑一次性说透,希望能帮还在指针门口徘徊的朋友迈过这道坎。

1. 指针的本质:为什么C语言非要有指针

1.1 从内存地址说起

要理解指针,先得理解计算机的内存。内存就像一排编好号的小格子,每个格子都有一个唯一的编号,这个编号就是“地址”。程序中定义的变量,本质上就是向操作系统申请了若干个这样的格子,并且给这组格子起了个名字。比如你写:

int a = 10;

编译器就会在内存里找一块4字节(在32位平台下通常是4字节,64位下也是4字节)的连续空间,把这块空间的起始地址和变量名a绑定。以后你在代码里写a,编译器就知道要去那块起始地址对应的位置取值。

那问题来了:地址本身也是数据,能不能用一个变量把地址存起来?当然可以。这种“用来存放另一个变量地址”的变量,就叫指针变量,简称指针。所以说白了,指针就是地址的容器,跟int变量是整数的容器、char变量是字符的容器一样,没什么神秘的。

1.2 为什么需要指针这种间接访问方式

有人会问:我直接用变量名不就行了,干嘛非要绕一圈去通过地址访问?

我举一个现实中的例子。你去快递驿站取件,快递员只需要给你一个取件码,你就能找到对应的货架和包裹。取件码本身不是包裹,但它指向包裹所在的位置。如果你要帮别人代取,只需要把取件码发给他,他就能按码找到包裹,不需要你把包裹的每一件东西都复制一份搬过去。指针在这里就扮演了“取件码”的角色。

在C语言中使用指针,核心原因有三个:

  • 函数传参时,默认是值传递,形参只是实参的拷贝,函数内部改形参,外面实参不变。如果想在函数里修改外部变量的值,就必须把外部变量的地址传进去,也就是传指针。
  • 数组、字符串、结构体这类复合数据,要么体积大,要么语法上不允许直接整体赋值。传递指针只需要复制一个地址,代价极低,效率高得多。
  • 动态内存分配(malloc、calloc、free)返回的是一块内存的起始地址,你只能通过指针去接管和使用这块内存。

顺着这个思路往下走,你会发现指针不是C语言设计的“副产品”,而是C语言能够高效控制硬件、操作内存的基石。很多高级语言里所谓的“引用”“对象句柄”,底层原理跟指针是相通的,只是包装得更安全而已。

2. 指针变量的定义、赋值与两种运算符

2.1 定义一个指针变量

指针变量的定义格式统一为:

类型 *变量名;

这里的*告诉编译器:我声明的是一个指针变量,它存放的是“某种类型变量的地址”。比如:

int *p; // p 可以存放 int 类型变量的地址 char *cp; // cp 可以存放 char 类型变量的地址 double *dp; // dp 可以存放 double 类型变量的地址

需要注意,p、cp、dp本身也是有类型的,这个类型就是“指向int的指针”“指向char的指针”“指向double的指针”。之所以要分这么细,是因为不同数据类型在内存中占用的字节数不同。指针变量保存的是起始地址,但如果没有类型信息,当你用指针去读取数据时,编译器根本不知道应该读取几个字节、按什么规则解释这串字节。

用快递柜来类比:取件码都长一样,但你取小件包裹和大件包裹用的柜子格子大小不一样。如果你拿着“大件柜”的取件码去小件柜取物品,可能取出来的东西跟你预期完全不同。

还有一点容易被忽略:指针变量本身也是一个变量,它也有地址,也可以被别的指针指向,于是就有了多级指针的概念(比如int **pp)。这个后面会专门聊。

2.2 取地址运算符 & 和间接引用运算符 *

定义完指针变量后,怎么让它指向某个变量?答案是取地址运算符&:

int a = 10; int *p; p = &a; // 把变量 a 的地址赋给指针 p

这里&a得到的是a在内存中的起始地址。赋值之后,p就“指向”了a。读取指针指向的变量的值,需要用到间接引用运算符*:

printf("%d\n", *p); // 输出 10

*p的意思是:从p中取出地址,再到该地址对应内存处去取值。&和*互为逆运算,&是从变量到地址,*是从地址到变量。这个对偶关系是整个指针用法的核心。

来看一个完整的小例子:

#include <stdio.h> int main(void) { int a = 10; int *p = &a; printf("a 的值: %d\n", a); printf("&a 的地址: %p\n", (void*)&a); printf("p 的值: %p\n", (void*)p); printf("p 指向的值: %d\n", *p); *p = 20; // 通过指针修改 a printf("修改后 a 的值: %d\n", a); return 0; }

运行结果大致如下(地址每台机器每次运行都可能不同):

a 的值: 10 &a 的地址: 0x7ffc2a1b4b54 p 的值: 0x7ffc2a1b4b54 p 指向的值: 10 修改后 a 的值: 20

你可以看到,&a和p的值完全一样,说明p确实保存了a的地址。通过*p = 20;修改了p指向的内存,效果等同于a = 20;。这就是“间接访问”。

2.3 声明时的 * 与使用时的 * 不要混淆

新手最容易乱的地方,就是把“声明指针变量时的*”和“取指针指向内容时的*”混为一谈。这两者的语法位置不同,语义也不同:

  • 声明时:int *p;,这里的*是类型修饰符,告诉编译器p是一个指针变量。
  • 使用时:*p,这里的*是解引用操作符,表示访问p所指向的内存单元。

看下面几种写法,初学者经常会犯晕:

int *p1, *p2; // p1 和 p2 都是 int* 类型 int* p3, p4; // 只有 p3 是 int*,p4 只是普通 int!

第二行在代码规范里是强烈不建议使用的,因为视觉上容易误以为p4也是指针。如果非要在一行里声明多个指针,请老老实实把*加到每个变量名前,或者干脆分行写。我在实际写代码时,习惯把*跟变量名靠在一起,写成int *p,这样在阅读时更容易区分声明和解引用。

2.4 指针类型与指针本身占用空间

指针变量存放的是地址,地址本质上是一个无符号整数。在32位系统上,地址用4字节表示,指针变量占4字节;在64位系统上,地址用8字节表示,指针变量占8字节。这一点跟指针所指向的类型无关,int *p、char *cp、double *dp在同一个平台上的大小是一样的,你可以用sizeof(int *)、sizeof(char *)验证。

但是,指针指向的类型决定了指针加减整数时的步长。举个例子:

int a[5] = {1, 2, 3, 4, 5}; int *p = &a[0]; p + 1; // 地址值增加 sizeof(int) = 4

而如果是char型指针:

char s[] = "hello"; char *cp = &s[0]; cp + 1; // 地址值增加 sizeof(char) = 1

指针加1,并不是在数字上简单加1,而是向后移动“一个指向类型大小”的距离。这个设计是为了让指针运算能够正确跨越数组元素。后面讲数组与指针时会专门展开,这里先记住结论:指针的算术运算基于指向类型的大小,而不是固定字节数。

3. 指针变量怎么用:取地址、解引用、修改目标

3.1 完整的操作流程

一个指针从诞生到退出,通常经历三个步骤:

  1. 声明指针变量,明确它的指向类型。
  2. 给指针赋值,让它指向一个合法的内存地址。
  3. 通过解引用*或指针运算来访问目标数据。

如果只定义指针而不赋值,这个指针就是一个“野指针”,它里面存的是一个不确定的垃圾值。万一你拿着这个垃圾值去解引用,轻则读到错误数据,重则直接让程序崩溃(段错误)。所以强烈建议:定义指针的同事就初始化,暂时没有目标就置成NULL:

int *p = NULL;

NULL在C语言中一般定义为((void*)0),表示空指针,也就是“不指向任何合法内存地址”。对空指针解引用是非法操作,但至少程序能稳定地报错,而不是随机崩溃,排查起来容易得多。

再看一个综合示例,演示如何通过指针修改外部变量:

#include <stdio.h> void swap(int *x, int *y) { int temp = *x; *x = *y; *y = temp; } int main(void) { int a = 3, b = 5; printf("交换前: a = %d, b = %d\n", a, b); swap(&a, &b); printf("交换后: a = %d, b = %d\n", a, b); return 0; }

这里的swap函数接收两个int*类型的参数,在函数内部通过*x、*y直接操作调用者main中的a和b。如果改成传值:

void swap(int x, int y) { /* 交换 x 和 y 的副本 */ }

那外面的a、b根本不会变。这就是指针在函数传参中的典型作用:让被调函数能修改调用者的局部变量。

3.2 指针的初始化与赋值细节

初始化有两种常见方式:

int a = 100; int *p1 = &a; // 方式一:声明时直接初始化 int *p2; p2 = &a; // 方式二:先声明,后赋值

两种方式都正确,关键是要保证赋值运算符右边是一个“类型匹配的地址”。如果你把一个int变量的地址赋给char *指针,编译器通常会给警告,因为类型不匹配,后续解引用时会按错误的方式解释内存数据。强制类型转换可以绕过警告,但除非你非常清楚自己在做什么(比如进行底层内存操作),否则不要在普通业务代码里这么搞。

还有一个很容易犯的错:把一个普通整数的值当成地址赋给指针。比如:

int *p = 100; // 错误!这是把整数100当地址

在C语言中,这种写法会得到编译警告,运行起来大概率崩溃,因为没有哪个正常的操作系统会让你直接访问地址0x64处的内存。只有极少数场景(比如单片机裸机开发、驱动中访问特定寄存器)才会直接把固定地址赋给指针,业务开发中严禁这样写。

3.3 解引用的注意事项

解引用一个指针时,编译器会根据指针类型去读取相应字节的数据。如果你用int *去解引用,它就读取4字节;用double *就读取8字节。所以解引用的前提是:指针必须指向合法且已分配的内存区域。

典型错误是“悬空指针”。什么是悬空指针?指针指向的内存已经被释放(比如调用free之后),但指针变量中保存的地址值并没有改变。这时候再解引用,访问到的可能是一片被系统回收的内存,数据随时可能被别的程序改写,表现就是“有时候对,有时候错,阴魂不散”。所以每次free完之后,建议立刻把指针置为NULL:

free(p); p = NULL;

这样一来,后续如果不小心再次使用p,至少是空指针崩溃,容易察觉到问题出在哪里,而不是进入一种时序依赖的玄学故障。

4. 指针与数组:剪不断理还乱的组合

4.1 数组名与首元素地址

数组名在大多数表达式中会隐式转换为“指向首元素的指针”。看代码:

int arr[5] = {10, 20, 30, 40, 50}; int *p = arr; // 等价于 int *p = &arr[0]; printf("%d\n", *p); // 输出 10 printf("%d\n", *(p + 2)); // 输出 30

这里的arr拿到的是数组首元素的地址,也就是&arr[0]。p + 2在数值上是“p所存地址 + 2 * sizeof(int)”,指向了arr[2],所以解引用得到 30。

这届新手容易把“数组名是首地址”理解成“数组名就是指针变量”,这是一个很大的误区。数组名是一个常量地址,你不能给数组名重新赋值:

int arr[5]; arr++; // 错误!数组名不是可修改的左值

而指针变量是可以被重新赋值的。更好的理解方式:数组名相当于一块固定内存区域的“门牌号”,你不能把门牌号摘下来挂到别处,但指针变量就像你写在便签上的“地址备忘”,随时可以改成别的地址。

4.2 下标与指针的等价转换

C语言中,下标访问arr[i]本质上就是*(arr + i)。编译器会把下标表达式翻译成指针加法再解引用。所以下面的写法都是等价的:

arr[2] // 数组形式 *(arr + 2) // 指针形式 2[arr] // 虽然合法,但没人会这么写,也不建议

了解这个等价关系很重要,因为在很多实际代码里,你看到的一长串指针表达式,其实就是一个数组访问。反过来,你能用指针灵活地遍历数组:

int arr[5] = {1, 2, 3, 4, 5}; int *p = arr; for (int i = 0; i < 5; i++) { printf("%d ", *p); p++; }

这里p++是“指针自增”,每次向后移动一个int的距离。用循环一路走下去,就能把数组里的所有元素打印出来。相比用下标arr[i],指针版本在某些场景下效率更高(因为避免了反复计算地址偏移量),现代编译器的优化能力很强,两者实际性能差别往往可以忽略,更推荐用清晰的数组下标写法,除非你在处理非常底层的数据流。

4.3 字符串与字符指针

字符数组与字符指针的关系是最容易让人迷惑的地方。看两个例子:

char str1[] = "hello"; // 字符数组,内容可以修改 char *str2 = "hello"; // 字符串字面量,通常存储在只读区

第一行定义了一个字符数组,里面逐个拷贝了'h' 'e' 'l' 'l' 'o' '\0',你可以通过str1[0] = 'H'修改它。第二行定义了一个指针str2,它指向字符串字面量“hello”所在的内存地址,而这个字符串字面量在很多编译器里放在只读数据段,如果你尝试str2[0] = 'H',程序可能会崩溃或产生未定义行为。

不少写C的人在这上面踩过坑。我刚开始学的时候也犯过错:

char *p = "hello"; p[0] = 'H'; // 直接导致运行时错误

正确做法是:如果你需要可修改的字符串,用字符数组;如果只是读取和传递字符串,用字符指针完全没问题。函数形参写成char *str或char str[]在语法上是等价的,但本质上接收的都是一个地址。

4.4 指针数组与数组指针

这两个概念名字相近,含义完全不同,容易搞混:

  • 指针数组:是一个数组,数组里的每个元素都是指针。写法:int *arr[5];
  • 数组指针:是一个指针,它指向一个数组。写法:int (*ptr)[5];

括号至关重要。int *arr[5]因为[]的优先级高于*,所以arr先跟[]结合,是一个包含5个int *的数组。而int (*ptr)[5]中ptr先跟*结合,是一个指针,指向一个有5个int的数组。

可以用一段代码来验证:

#include <stdio.h> int main(void) { int a = 1, b = 2, c = 3; int *arr[3] = {&a, &b, &c}; // 指针数组 for (int i = 0; i < 3; i++) { printf("%d ", *arr[i]); } int matrix[2][3] = {{1, 2, 3}, {4, 5, 6}}; int (*ptr)[3] = matrix; // 数组指针,指向一行(3个int) printf("\n%d\n", (*ptr)[1]); // 输出 2,第一行的第二个元素 return 0; }

数组指针在二维数组传参、以行视角操作矩阵时非常有用。如果你不想一开始就陷入这两者的细节,先把重点放在“指针是一维的地址变量”这个认知上,二维及以上场景可以等基础牢固了再深入。

5. 函数与指针:传参、返回与函数指针

5.1 函数形参中的指针

前面已经演示了通过指针实现交换变量。这里再总结一下传指针的几类典型场景:

  • 需要修改实参本身的值。
  • 实参是体积较大的结构体,希望避免值拷贝的开销。
  • 需要让函数返回多个结果,其中一个结果通过返回值给出,其他结果通过指针参数带出。
  • 函数需要操作动态分配的内存块。

看一个通过指针参数“带出”多个返回值的例子:

#include <stdio.h> void calc(int a, int b, int *sum, int *diff) { *sum = a + b; *diff = a - b; } int main(void) { int s = 0, d = 0; calc(7, 2, &s, &d); printf("sum = %d, diff = %d\n", s, d); return 0; }

很多人一开始用不好这种写法,是因为思维还停留在“函数只能有一个返回值”上面,总觉得把结果通过参数传回来很别扭。但只要理解了指针是“地址的搬运工”,你就会发现这种“输出参数”的方式在C语言里无处不在,比如标准库的scanf,第二个参数以后全部是地址:

int age; scanf("%d", &age);

scanf能修改你传入的变量的原因,正是因为它在内部通过指针写入了你提供的地址对应的内存。

5.2 函数指针:把函数当作数据

函数也有地址,也可以被指针指向。定义一个函数指针的格式稍微抽象一点:

返回类型 (*指针名)(参数列表);

例如:

int add(int a, int b) { return a + b; } int main(void) { int (*fp)(int, int) = add; // fp 指向 add 函数 printf("%d\n", fp(3, 4)); // 调用 add(3, 4),输出 7 return 0; }

函数指针最大的价值在于实现回调机制、构造跳转表。比如你写一个通用的排序函数,排序规则由调用者通过函数指针传入,这样同一个排序逻辑就能排序整数、浮点数、学生结构体、员工结构体,而不需要为每种类型写一遍排序。很多图形界面库、事件驱动框架,底层都是靠函数指针把用户的处理逻辑挂接到框架的调用点上。

初学阶段不要求精通函数指针,但至少要知道有这个东西,否则日后看库源码时容易一头雾水。函数指针的解析顺序完全可以套用“从内到外、先右后左”的规则去拆解,这里不展开,先混个脸熟。

6. 指针运算与多级指针的进阶理解

6.1 指针的算术运算

指针支持的算术运算非常有限,无非就是:

  • 指针加整数:向后移动若干个元素。
  • 指针减整数:向前移动若干个元素。
  • 两个同类型指针相减:得到它们之间相差的元素个数,结果是ptrdiff_t类型。

其他的什么指针相乘、指针相加都是非法操作。原因很简单:地址加上地址没有任何物理意义,就像你不能把两个门牌号相加得到一个有意义的新门牌号。

有一个非常实用的场景:用指针求数组长度。

int arr[] = {1, 2, 3, 4, 5}; int len = &arr[4] - &arr[0] + 1; // 5

更通用的写法是用指向末尾的“尾后指针”&arr[5]:

int len = &arr[5] - arr;

这里&arr[5]虽然超出了数组最后一个元素的下标范围,但在C语言中,这种“尾后地址”在指针运算里是允许计算的(但不能解引用)。这种做法在遍历时非常常见,比如 STL 风格的区间[begin, end),就是用一个头指针和一个尾后指针来表示待处理范围。

6.2 多级指针:指向指针的指针

当指针变量本身也是一个变量时,它也有地址,于是可以用另一个指针去指向它。这就是二级指针:

int a = 10; int *p = &a; int **pp = &p; printf("%d\n", **pp); // 输出 10

对于**pp的理解可以分两步拆:*pp得到p(也就是&a),再对这个结果执行*,就得到a的值。所以**pp等价于*p,也等价于a。

二级指针最常见的应用场景是:在函数内部修改一级指针本身。假如你要写一个函数,让指针p指向一块新建的动态内存,那么形参必须是二级指针:

#include <stdio.h> #include <stdlib.h> void allocInt(int **pp) { *pp = (int*)malloc(sizeof(int)); if (*pp == NULL) { return; } **pp = 88; } int main(void) { int *p = NULL; allocInt(&p); printf("%d\n", *p); // 输出 88 free(p); p = NULL; return 0; }

这里allocInt(&p)把p的地址传进去,函数内部通过*pp就能修改外部指针变量p的值,让它指向 malloc 分配出的内存。如果只传一级指针allocInt(p),函数内部修改的只是形参自己的拷贝,外面的p依然还是NULL,后面调用就会出错。

再往上还有三级指针、四级指针,但实际工程中最多用到二级指针,再高级别的指针往往意味着设计过于复杂,需要重构。认识一下三级指针int ***ppp只是“指向二级指针的指针”,但千万不要以“用更多星为荣”,代码的可读性远比“炫技”重要。

6.3 指针与动态内存分配

动态内存分配是指针存在的最大理由之一。C语言标准库提供了malloc、calloc、realloc、free这套接口:

int *arr = (int*)malloc(10 * sizeof(int)); if (arr == NULL) { // 分配失败,要做错误处理 }

用malloc分配的内存位于堆区,它的生命周期不受函数返回影响,必须手动通过free释放。这里有几个铁律:

  1. malloc之后一定要检查返回值是否为NULL,尤其是分配大块内存时。
  2. free只能释放通过malloc系列函数分配的指针,不能释放栈上变量的地址。
  3. 同一块内存不能释放两次,否则会产生“double free”错误。
  4. 释放后置空指针,避免悬空指针。

你可以用valgrind这类工具来检测内存泄漏和非法访问。写复杂一点的项目时,每次malloc都要想清楚谁负责释放,以及所有退出路径上是否都能正确释放。很多线上内存泄漏问题,查到最后都是某一个分支忘了free。

动态内存的使用场景非常多:程序运行时才知道需要多大的数组、需要构建链表树图等数据结构、需要跨函数返回一块新创建的内存。没有动态内存,C语言就只能做静态规划,非常受限。

7. 常见问题与排查技巧实录

7.1 段错误(Segmentation fault)十有八九是指针闯祸

初学指针时,遇到最多的运行错误就是段错误。段错误的本质是程序访问了没有权限访问的内存区域。常见触发原因:

  • 对空指针或野指针解引用。
  • 指针越界访问,比如访问数组下标-1或超过长度。
  • 函数返回了局部变量的地址,调用者再去解引用。
  • 释放内存之后继续访问。

排查手段:先用调试器(比如gdb)跑一下,看崩溃位置在哪一行;启动AddressSanitizer(-fsanitize=address)编译,可以立刻报告非法访问点;再不行就缩小范围,注释掉部分逻辑,二分定位。

7.2 指针类型不匹配引发的怪问题

下面这种写法隐患很大:

int a = 0x12345678; char *p = (char*)&a; printf("%x\n", (unsigned char)*p);

在小端机器上,你看到的是0x78(低位字节先存),在大端机器上则是0x12(高位字节先存)。这种跨端问题在串口通信、文件格式解析里经常冒出来。解决办法是明确使用整型读写,或者自己实现字节序转换函数。普通应用开发,不要用char *去强戳int变量的字节表示,除非你就是想分析内存布局。

7.3 函数返回局部变量地址的严重错误

int *badFunc(void) { int value = 42; return &value; // value 是局部变量,函数结束后就失效 }

这个函数返回之后,value所在的内存已经不属于当前函数栈帧,但地址值还留在指针里。这时候调用者解引用,得到的数据是不确定的。虽然偶尔“碰巧”还能读出来,但一旦函数调用链更深,这块栈空间就会被复用,数据就被覆盖。

正确做法:把结果存到调用者提供的指针所指向的内存中,或者用malloc创建堆内存后返回,但要记得释放。我在实际项目里看到过好几个人因为这种写法,出现“时好时坏”的随机 bug,查了很久才发现是这里的问题。

7.4 常见陷阱速查表

陷阱类型危险写法推荐做法
野指针int *p; *p = 100;初始化置NULL或赋合法地址
悬空指针free(p);后再*pfree(p); p = NULL;再判断空
越界访问arr[5]访问int arr[5]通过数组长度检查后再访问
类型不匹配int *p = &floatVar;保持类型一致,必要时显式转换并说明原因
返回局部变量地址return &localVar;用输出参数或动态内存
重复释放free(p); free(p);释放后立即置空
没有释放内存malloc后不free梳理所有权,确保释放一次
使用未初始化的指针直接解引用随机地址声明即初始化

7.5 排查工具与方法

如果你用的是 GCC 系工具链,建议每次编译偷偷加上:

gcc -Wall -Wextra -g -fsanitize=address yourfile.c -o yourprogram

-Wall -Wextra会让你看到大多数类型问题警告;-fsanitize=address是内存错误检测神器,越界、释放后用、内存泄漏都能直接报出来。正规开发时也建议在 CI 里跑一遍valgrind:

valgrind --leak-check=full ./yourprogram

它能告诉你哪一行malloc的内存没有被释放,极大提高排查效率。

8. 自己动手实验:5个指针练习建议

纸上得来终觉浅,指针这东西光看不练永远学不会。下面给几道由易到难的练习题,照着写一遍再运行,比背十遍概念有用得多。

  1. 写一个函数,接收两个整型指针,把两个指针指向的变量值互换(要求不能修改函数签名里两个指针的指向,只能通过解引用交换内容)。
  2. 通过指针遍历一个int数组,用“尾后指针”作为循环结束条件,打印出所有元素。
  3. 定义一个包含5个char*的指针数组,初始化成5个不同长度的字符串,写循环打印每个字符串及其长度。
  4. 用malloc动态创建一个n个元素的double数组,写一个函数接收数组指针和长度,返回数组所有元素之和,最后释放内存。
  5. 实现一个简化版strlen,函数原型写为int my_strlen(const char *s);,体会使用const char *作为只读形参的意义。

第5题里提到的const很重要。当函数不打算修改指针指向的内容时,形参应该写成const char *s,这样既能接收字符串常量,也能避免误修改。这个习惯能帮你写出接口更安全、意图更清晰的代码。

我当年学指针时,最深的体会就是:不要去背“*是取内容,&是取地址”这种口诀,而是盯着内存地址的变化去理解。写几行程序,打印出变量地址、指针地址、指针的指针地址,看着地址慢慢变远,自然就明白了。用printf的%p格式把每一层关系都打印出来,这是最直观的学习方式。

至于后面更复杂的:指向二维数组的指针、函数指针数组、链表中的指针操作、与结构体结合等等,都是在“指针保存地址”这个核心概念上叠加出来的花样。把今天这篇里的基本概念和常见坑吃透,后续进阶就会顺畅很多。如果还有想不通的细节,建议回到内存模型去推演,实在不行画一张“内存格子图”,把每个变量占的格子、指针保存的编号都画出来,比干想有效得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询