news 2026/10/19 4:23:58

C语言中指针的理解(2)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C语言中指针的理解(2)

一.const关键字

1.1 const修饰变量

先说一个最简单的道理:普通变量是可以随便改的。

int a=10; a=20;

在 C 语言关键字中,const 就是变量的一把“锁”。有时候,我们希望一个变量“定死了”,谁也不许它改。这时候就可以请出 const。

const int n = 0; n = 20; // 报错!n 被 const 锁住了,语法上不允许改

note:n 本质上还是个变量,只是 const 给它套了一层“语法上的锁”。你在代码里写 n = 20,编译器会直接拦下来,说不符合规则。

那这个锁真的打不开吗?也不是。如果你拿到 n 的地址,绕过去改,还是能改的,示例如下:

const int n = 0;//n前面有const,n无法修改 int* p = &n; // 拿到 n 的地址 *p = 20; // 绕过了 const,把 n 改成了 20

程序跑出来,n 真的从 0 变成了 20。可是这合理吗?const 的初衷就是“不许改”,你拿个指针就把它改了,那这把锁不是白上了吗?所以,要锁就锁彻底——const 不光能锁变量,还能锁指针本身。关键看它站在 * 的哪一边,意思完全不同:

写法*左边*右边
int*p没上锁没上锁
const int*p指针指向的内容不能变指针本身可以变
int* const p指针指向的内容可以变指针本身不能变
const int*const p指针指向的内容不能变指针本身不能变
1.2 const效果举例

下面我用一个有趣的例子来帮助大家理解并记住 const 在不同位置上的效果。

(1) 普通关系int *p

男友的钱想花就花:*p = 0

男友想换就换:p = &m

想花就花,想换就换,没有任何限制。

(2)const int *p

女生给自己立了条规矩:恋爱可以谈,但我不花男朋友的钱。

花男友的钱:*p = 0不能发生,n 那 10 块不能让你花光

换男友:p = &m可以,随时能换,换到 m 之后照样不花 m 的钱

花心但独立,只图感情、不动你的钱。

(3)int * const p

这次女生立的规矩是:认定一个男友,就绝不换人。

换男友:p = &m不能发生,认准 n 就是 n

花男友的钱:*p = 0可以,只要 n 愿意给你花

专一,认定了就跟你,但钱照花。

④const int * const p

花男友的钱:*p不能发生

换男友:p=&其它不能发生

既不能花他的钱,也不许提分手。表面上像 "渣男的要求",其实是女生自己立了个又抠又不让分手的规矩。

二.野指针

野指针的指向是“不可知”的——随机的、不正确的、没有任何限制。拿这种指针去读写,就像在黑夜里乱开枪,随时可能崩。

2.1 野指针如何产生

(1)指针未初始化

int *p; // 局部变量,没初始化,里面存的是个随机值 *p = 20; // 往一个不知道的地址里写东西 → 野指针

(2)越界访问

int arr[10] = {0}; int *p = &arr[0]; for (int i = 0; i <= 11; i++) { *(p++) = i; // 数组只有 10 个元素,却写到第 12 个位置了,数组越界产生野指针 }

(3)指针指向的空间被释放了

int* test() { int n = 100; // 局部变量,函数一结束就“没了” return &n; // 返回它的地址,等函数返回后这个地址已经无效 } int main() { int *p = test(); // p 指向的是一块已经失效的空间 printf("%d\n", *p); // 访问一个已经不属于你的地方产生野指针 return 0; }
2.2 如何避免野指针的产生

我把野指针比喻成“野狗”,野狗放任不管是非常危险的,所以我们常用这几种方法限制这条“野狗”。

(1)初始化指针(把狗拴起来)

如果明确知道指向哪,直接给地址;不知道指向哪,就给它 NULL,NULL 就是 C 语言里定义的一个常量,值是 0。地址 0 是不能用的,你读写它,编译器就会报错——等于提前把“危险”拦在了门外。

举例如下:

int num = 10; int *p1 = &num; // 知道指向哪,直接给地址 int *p2 = NULL; // 不知道,先拴住,赋 NULL

(2)不要越界

程序申请了多少空间,指针就只能访问多少。超出范围就是越界,越界就是往别人家里跑。

(3)不用了及时置NULL,用之前检查一下

拴起来的狗,你也不能去逗它。指针也是:不用了先 p = NULL,用之前先判断 if (p != NULL),是 NULL 就不去使用。

p = NULL; // 用完了,拴起来 // ... 过了一阵 p = &arr[0]; // 重新让它指回数组开头 if (p != NULL) { // 用之前先检查 // 放心使用 }

(4)不要返回局部变量的地址

局部变量随函数结束就消失了,把它的地址带出来,就是一个野指针。所以像上面那个 test() 函数,就是反面教材。

三.assert关键字

3.1 断言
assert.h 头文件定义了宏 assert(),用于在运行时确保程序符合指定条件,如果不符合,就报
错终止运行。这个宏常常被称为“断言”。
assert(p != NULL);

这行代码在运行时验证 p 是否不等于 NULL。如果是,程序继续跑;如果不是,程序直接停下来,还帮你把出错的文件名和行号都打印出来。排查问题的时候,这很高效且安全。

3.2 assert断言的优缺点

好处如下:

(1)自动标识出问题的文件和行号

(2)可以不改代码就开启/关闭断言——只要在 #include <assert.h> 前面定义一个 NDEBUG 宏即可。

举例:

#define NDEBUG #include <assert.h>

重新编译后,所有 assert() 就都被“屏蔽”了。哪天又出问题,把 #define NDEBUG 注释掉再编译,断言就又回来了。

它的缺点也实在:

每次检查都要花一点运行时间。所以常规做法是——Debug(调试版)开着,方便排查;Release关掉,保证用户使用时不拖慢程序。在 VS 这类 IDE 里,Release 版本默认就把断言优化掉了,两全其美。

四.传址调用

4.1 什么是传址调用

顾名思义,传址调用就是将地址作为参数传递给函数,再完成功能。

4.2 传址调用实现变量交换

我们之前在关于函数的学习中学习了传值调用,在学习了指针后我们就可以使用传址调用了。

还记得我们之前在学习函数时留下的一个问题吗?如果用函数来实现两个变量的交换该怎么实现呢?

有的人会这样实现:

void Swap1(int x, int y) { int tmp = x; x = y; y = tmp; } int main() { int a, b; scanf("%d%d", &a, &b); Swap1(a, b); printf("a=%d b=%d", a, b); return 0; }

是不是看起来没有什么问题,创建中间变量然后交换,但是当程序运行起来就会发现结果完全是错的,x,y根本没有交换。

输出结果:

那问题出在哪里呢?

还记得我们在函数的学习中说过:“我们在调试的时候可以观察到,x 和 y 确实得到了 a 和 b 的值,但是 x 和 y 的地址和 a 和 b 的地址是不一样的,所以我们可以理解为形参是实参的一份临时拷贝。”

main 里创建了 a、b,各有自己的地址; 调用 Swap1 时,把 a、b 的值复制给了形参 x、y; 但 x、y 是另一块独立的内存空间,跟 a、b 没关系。 所以 Swap1 里面换的只是 x 和 y 这对“复印件”,原件 a、b 纹丝不动。结论就是:实参传给形参时,形参会单独创建一份临时空间来接收,改形参不影响实参。这就是“传值调用”的坑。

如果要实现用函数对变量进行交换就要用到传址调用,我们把a,b的地址传进去,让函数通过地址直接去操作 main 里的 a 和 b

代码如下:

void Swap2(int *px, int *py) { int tmp = *px; // *px 拿到 a 的真实值 *px = *py; // 直接把 a 改成 b 的值 *py = tmp; // 把 b 改成 tmp } int main() { int a = 0, b = 0; scanf("%d %d", &a, &b); printf("交换前: a=%d b=%d\n", a, b); Swap2(&a, &b); // 传的是地址! printf("交换后: a=%d b=%d\n", a, b); return 0; }

输出结果:

这次输入 10 20,交换后就是 20 10。因为 Swap2 拿到的是 a、b 的地址,*px、*py 直接操作的就是 main 里那两块内存。这种把地址传给函数的调用方式,就叫传址调用。

4.3 传址调用的使用场景

函数只需要用到主调函数里变量的值来算东西我们就用传值调用,函数内部要修改主调函数里的变量必须用传址调用。

五.数组名的实际意义

5.1 数组名与地址

之前我们用&arr[0]拿到数组第一个元素的地址,但其实数组名本来就是地址,而且是数组首元素的地址。来做个测试:

int main() { int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; printf("&arr[0] = %p\n", &arr[0]); printf("arr = %p\n", arr); return 0; }

输出结果:

从输出结果可见,arr 打印出的地址与 &arr[0] 打印出的地址一模一样,所以数组名代表着数组首元素的地址。

那么这时有人会问,那这个代码是怎么回事?

int main() { int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; printf("%d\n", sizeof(arr)); return 0; }

按之前所说,数组名代表首元素地址,输出结果应该是4或8呀,这个40是从哪里来的呢?

其实数组名代表首元素地址有两个特殊情况:

(1)sizeof(数组名):sizeof 中单独放数组名不能放其他的东西,这里的数组名表示整个数组,算的是整个数组的大小,单位是字节。

int main() { int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; printf("%d\n", sizeof(arr)); printf("%d\n", sizeof(arr + 0)); return 0; }

这就是一种反面例子,在我们的认知里sizeof(arr)=sizeof(arr+0),但编译器不会这么想,所以,sizeof 中单独放数组名不能放其他的东西,这里的数组名才表示整个数组。

(2)&数组名:这里的数组名表示整个数组,取出的是整个数组的地址。

note:整个数组的地址和数组首元素的地址是有区别的。

除此之外,任何地方使用数组名,数组名都表示首元素的地址。

5.2 数组名的理解与使用

大家看一下的例子,想一想输出结果应该是什么?

int main() { int a[] = { 1,2,3,4 }; printf("%d\n", sizeof(a)); printf("%d\n", sizeof(a + 0)); printf("%d\n", sizeof(*a)); printf("%d\n", sizeof(a + 1)); printf("%d\n", sizeof(a[1])); printf("%d\n", sizeof(&a)); printf("%d\n", sizeof(*&a)); printf("%d\n", sizeof(&a + 1)); printf("%d\n", sizeof(&a[0])); printf("%d\n", sizeof(&a[0] + 1)); return 0; }

我来给大家一一解答:

printf("%d\n", sizeof(a));

对于这个代码,a为数组名,只有数组名单独在sizeof中,故a为整个数组,我们来看看输出结果。

没错!数组中有4个整形数据,每个为4字节,4*4=16。

printf("%d\n", sizeof(a + 0));

对于这个代码,a为数组名,sizeof中是a+0,并不是单独的数组名,所以a不代表整个数组,而是代表首元素地址。让我们来看看输出结果。

由于我的环境是x64,指针所占内存为8字节。看来我们的分析没有问题。

printf("%d\n", sizeof(*a));

对于这个代码,a为数组名,sizeof中为*a不是单纯的数组名,所以a为首元素地址,*a为解引用首元素地址,故求的是a中的首元素所占内存大小。让我们看看输出结果。

int类型的整整型数据所占内存大小为4字节。看来我们的分析没有问题。

printf("%d\n", sizeof(a + 1));

对于这个代码,a为数组名,sizeof中为a+1,不是单纯的数组名,所以a为首元素地址。a+1表示第二个元素的地址,所求为第二个元素的地址所占内存的大小。让我们看看输出结果。

由于我的环境是x64,指针所占内存为8字节。看来我们的分析没有问题。

printf("%d\n", sizeof(a[1]));

对于这个代码,数组名是a,sizeof中为a[1],不是单纯的数组名,a[1]代表下标为1的元素,所求为第二个元素所占内存的大小。让我们看看输出结果。

int类型的整整型数据所占内存大小为4字节。看来我们的分析没有问题。

printf("%d\n", sizeof(&a));

对于这个代码,数组名为a,sizeof中为&a并不是单纯的数组名,并且&后只有单纯的数组名,所以&a代表的是整个数组的地址,故所求为整个数组的地址所占内存的大小。让我们看看输出结果。

由于我的环境是x64,指针所占内存为8字节。看来我们的分析没有问题。

printf("%d\n", sizeof(*&a));

对于这个代码,数组名为a,sizeof中为*&a并不是单纯的数组名,并且&后只有单纯的数组名,所以&a代表的是整个数组的地址,*&a是解引用整个数组的地址为整个数组,故所求为整个数组所占内存的大小。让我们看看输出结果。

int类型的整整型数据所占内存大小为4字节。看来我们的分析没有问题。

printf("%d\n", sizeof(&a + 1));

对于这个代码,数组名为a,sizeof中为&a+1并不是单纯的数组名,并且&后只有单纯的数组名,所以&a代表整个数组的地址,&a+1为向后跨越一个数组的大小后的地址,故所求为该地址在内存中的大小。让我们看看输出结果。

由于我的环境是x64,指针所占内存为8字节。看来我们的分析没有问题。

printf("%d\n", sizeof(&a[0]));

对于这个代码,数组名为a,sizeof中为&a[0]并不是单纯的数组名,并且&后不是单纯的数组名而是a[0]代表数组的首元素,则&a[0]为数组首元素地址,故所求为数组的首元素地址所占内存的大小。让我们看看输出结果。

由于我的环境是x64,指针所占内存为8字节。看来我们的分析没有问题。

printf("%d\n", sizeof(&a[0] + 1));

对于这个代码,数组名为a,sizeof中为&a[0]+1并不是单纯的数组名,并且&后不是单纯的数组名,而是a[0]代表数组的首元素,则&a[0]+1代表首元素地址向后挪一个元素的地址,也就是数组第二个元素前的地址,故所求为第二个元素前的地址在内存中的大小。让我们看看输出结果。

由于我的环境是x64,指针所占内存为8字节。看来我们的分析没有问题。

六.指针访问数组

6.1 指针和数组下标的关系

有了前面的知识,再结合数组的特点,我们就可以很方便地用指针访问数组了,指针和数组下标是密不可分的“一家人”。

int arr[10] = {0}; int sz = sizeof(arr)/sizeof(arr[0]); int* p = arr; // 数组名就是首元素地址,可以直接赋值给指针 for (int i = 0; i < sz; i++) scanf("%d", p+i); // 也可以写 scanf("%d", arr+i); for (int i = 0; i < sz; i++) printf("%d ", *(p+i));

这个代码搞明白后,再试一下:数组名 arr 是首元素地址,可以赋值给 p,所以 arr 和 p 在这里是等价的。既然 arr[i] 可以访问数组元素,那 p[i] 是不是也可以?

// 把输出里的 *(p+i) 换成 p[i] for (int i = 0; i < sz; i++) printf("%d ", p[i]);

同样能正常打印。所以本质上 p[i] 是等价于 *(p+i) 的。同理 arr[i] 也等价于 *(arr+i)——数组元素的访问,在编译器处理时,都是先转换成“首元素的地址 + 偏移量”求出元素地址,再解引用去访问的。

七.一维数组传参的本质

7.1 示例:利用函数求元素个数

我们将通过以下这个例子来理解一维数组传参的本质。

void test(int arr[]) { int sz2 = sizeof(arr)/sizeof(arr[0]); printf("sz2 = %d\n", sz2); } int main() { int arr[10] = {1,2,3,4,5,6,7,8,9,10}; int sz1 = sizeof(arr)/sizeof(arr[0]); printf("sz1 = %d\n", sz1); // 10 test(arr); // sz2 = 1 return 0; }

我们可见,函数内部没有正确获得元素个数!那是怎么一回事呢?

这就是数组传参的本质:数组名是首元素的地址,数组传参时传的就是首元素的地址,所以形参本质上应该用指针来接收。函数内部写sizeof(arr),算的是一个地址的大小(单位字节),而不是数组的大小。正是因为形参本质是指针,所以在函数内部没办法求出数组元素个数。

所以形参写成数组形式或指针形式都可以,本质都一样。

八.二级指针

8.1 什么是二级指针

通俗的讲,二级指针就是指针的指针,我们知道指针变量也是变量,那么二级指针就是存着这一个指针的地址的指针变量。

8.2 二级指针的写法
int a = 10; int *pa = &a; // 一级指针,存 a 的地址 int **ppa = &pa; // 二级指针,存 pa 的地址

内存里大概是这样的:a 存的是值 10,pa 存的是 a 的地址,ppa 存的是 pa 的地址。 二级指针的运算有两层: *ppa:通过对 ppa 中的地址解引用,找到的是 pa。比如 *ppa = &b; 就等价于 pa = &b; **ppa:先通过 *ppa 找到 pa,再对 pa 解引用(*pa),找到的是 a。比如 **ppa = 30; 等价于 *pa = 30; 也等价于 a = 30。

九.指针数组

9.1 指针数组是什么

在讲解指针数组之前我想问大家,指针数组是指针还是数组?你可以先思考一下再来看下面的讲解。

我们类比一下:整型数组是存放整型的数组,字符数组是存放字符的数组。那指针数组呢?

它是存放指针的数组,本质上是个数组。 比如 int* arr[5],它是一个有 5 个元素的数组,每个元素都是一个 int* 类型的指针,用来存放地址。指针数组的每个元素是地址,又可以指向一块区域。

9.2 用指针数组模拟二维数组

有了指针数组,我们就能用它来“模拟”二维数组。思路是:先把几个一维数组的地址装进一个指针数组里。大家可以先思考一下怎么来编写这个代码以达到我们模拟的目的。

int arr1[] = {1,2,3,4,5}; int arr2[] = {2,3,4,5,6}; int arr3[] = {3,4,5,6,7}; // 数组名是首元素地址,类型是 int*,可以存放在 parr 数组中 int* parr[3] = {arr1, arr2, arr3}; for (int i = 0; i < 3; i++) { for (int j = 0; j < 5; j++) { printf("%d ", parr[i][j]); } printf("\n"); }

parr[i] 是访问 parr 数组的元素,parr[i] 找到的元素指向了一个整型一维数组,parr[i][j] 就是那个一维数组里的元素。这样就用指针数组模拟出了二维数组的效果。 但要注意,它并非完全是二维数组,因为每一行(也就是 arr1、arr2、arr3)在内存里并不是连续存放的。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/19 4:23:50

Linux时间同步全解析:从NTP原理到chrony落地与排查

时间同步这个话题&#xff0c;几乎每个做运维或者后端开发的同行都会碰上几回。别看它原理上讲起来就是"对个表"&#xff0c;可真到生产环境里&#xff0c;因为时钟漂移导致日志错乱、认证失败、分布式事务时间戳打架的案例&#xff0c;我这些年见过太多了。很多项目…

作者头像 李华
网站建设 2026/10/19 4:23:28

open-code-review:打造高效代码审查流程的实践指南

了解&#xff0c;我会严格按要求输出一篇围绕“open-code-review”的独立完整博文&#xff0c;开头直接切入主题&#xff0c;全文按编号章节展开&#xff0c;无AI模板感、无元信息&#xff0c;主体充分展开实操细节、经验干货与原理拆解。下面直接交付正文。前几天团队里一个同…

作者头像 李华
网站建设 2026/10/19 4:23:05

深度学习结合ISP仿真:去噪与白平衡联合重建实践

做图像信号处理仿真这些年&#xff0c;一个很明显的感受是&#xff1a;传统的规则化ISP管线越来越“卷不动”了。前面这个系列写到第16篇&#xff0c;大部分内容都在聊去噪、去马赛克、白平衡、色彩校正、锐化这些经典模块&#xff0c;每个模块都有成熟算法&#xff0c;参数靠经…

作者头像 李华
网站建设 2026/10/19 4:23:03

AI论文写作工具实战:千笔工具如何拆解学术拖延与写作流程

我见过太多朋友&#xff0c;电脑里躺着十几个文档&#xff0c;标题从“毕业论文初稿”改到“最终版3.0”&#xff0c;但打开率最高的其实是微信。拖延写论文这事儿&#xff0c;几乎成了大学生和研究生的集体记忆。后来我接触了不少AI写作工具&#xff0c;也深度用过几款号称能治…

作者头像 李华
网站建设 2026/10/19 4:22:38

open-code-review 实践:把代码审查变成可配置、可度量的工程规范

代码审查这件事&#xff0c;大部分团队都做过&#xff0c;但真正做好的没几个。我见过太多项目把 Code Review 当成发布流程里的一个强制关卡&#xff0c;PR 一提交&#xff0c;reviewer 随手点个 Approve&#xff0c;连 diff 都没仔细看&#xff1b;也见过那种“大型代码走查会…

作者头像 李华
网站建设 2026/10/19 4:22:15

汉语言专业写毕业论文,AI 到底怎么选?从“网络流行语研究”说起 ✍️

先说一个汉语言专业同学很容易遇到的毕业论文场景&#xff1a; 选题大致是《近五年汉语网络流行语的语义衍生与传播机制研究》&#xff0c;需要从微博、B站弹幕、短视频评论等渠道收集语料&#xff0c;分析“文学”“搭子”“情绪价值”等表达的语义变化&#xff0c;还要梳理国…

作者头像 李华