1. 地址
计算机中的地址(address)一般指代内存中的某一个存储单元的地址,可以简单理解为某一个内存存储单元的编号。
**地址是内存中存储数据的位置的唯一标识。**对于储存在内存中的变量,只要得知这个变量的地址,即可非常快速地拿到它的值,并对其进行修改等操作。
指向内存中的某一块地址的一个特殊的变量叫做指针。
2. 取地址的含义
取地址符号为&,可以使用&拿到一个变量的地址。
在printf()函数中,使用%p可以打印一个地址类型的数据。
为了规范,发生强制类型转换的地方一般要写上转换后的数据类型,(void*)代表将变量强制转换为指针类型的数据。
例如一个快递员找目标住户的例子:
1 |
|
通过打印信息可以很明显的看出,一个数组中的所有元素在内存中都是紧挨着的,不会中断。
由于数组中元素的类型为uint32_t,因此每个元素占四个字节,也就是所有元素的地址之间也相隔四个字节。
3. 指针
大部分程序中只是用到了用来指向某一个地址的指针,一般不需要得知具体的地址。
指针是一种特殊的变量,操作的是较为底层的地址,非常强大。
指针不存具体的数值,只存储数据的地址,就像快递员的地图上对每个客户住址的标记(地址)。
上一节说过,用取地址符&可以拿到某一个变量的地址,那么该如何将这些地址储存为指针呢?
答案是使用指针类型,使用常用的数据类型,在变量名前面加*即可,代表指针变量,例如int *ptr_number = &number,这个指针中存储了变量number的地址。
使用保存的这个地址时,直接使用指针变量的变量名ptr_number即可,不需要再加*或&,例如控制台打印这个地址:
1 | printf("变量 number 的地址 ptr_number 是:%p\n", (void*)ptr_number); |
作为区别,*ptr_number是对ptr_number指向的地址进行解引用,获取该地址存储的值。也就是说,*ptr_number的值就是number的值,具体会在下一节介绍。
虽然相比正常的int类型只加了一个*,但指针的数据类型不是int类型,而是int*整型指针类型,其他的类型同理。
指针类型写为
int *ptr_number = &number只是习惯,也可以写为int* ptr_number = &number,这种写法体现了这个指针的类型为int*。
4. 指针与修改
*是C的解引用(dereference)操作符:它把一个指针(地址)转换为该地址处的对象。简单来说,当有以下定义时:
1 | int number = 123; |
可以有以下特性:
&是取地址运算符:&number得到变量number的地址,类型为int*;ptr_number的类型为int*,保存了number的地址;*ptr_number表示“位于ptr_number所指地址的那个int对象”,类型为int。因此*ptr_number和number指向同一块内存——读取时值相同,同时写入*ptr_number会改变number的值。
也就是下面三个printf语句的输出值相同:
1 | printf("number = %d\n", number); |
可以通过指针修改变量的值,例如:
1 | int number = 123; |
5. 指针星号的企业风格规范以及容易引发的问题
定义指针时,有两种常见的写法:
int* p:微软写法,强调p这个变量是一个int*(整型指针)类型的变量
int *p:强调这个变量实际上是一个指针
这两种定义方式都可以,但较为规范且易于理解的写法应该第一种int* p。
值得注意的是,当想要在一个语句中同时定义两个指针时:
1 | // 错误写法: |
因此,应该尽可能避免在一个语句中同时定义多个变量,防止出现类似的隐性问题。
6. 指针的意义与作用究竟是什么:外部服务操作
指针存在的意义在于:希望外部能够提供更好的服务给我,而我只需要给他地址,他就可以帮我解决一切的困扰,减少我亲自操作的麻烦。
简单来说,Windows系统中的快捷方式就是一种指针的应用,快捷方式是一个指向真实文件或文件夹路径的指针,指针不需要管点击后文件如何打开,以及程序是如何运行的,它只需要指向一个地址即可。
7. 野指针初步介绍
野指针:指向了一个无效的内存地址或者是已经释放的内存地址的指针。
非常危险!
野指针会访问一个不可描述的内存空间,会导致不可预测的行为。
访问或操作野指针可能会导致拿到奇怪的数据或者对错误地址的数据执行了一些操作,会导致无法预测的结果。
程序中必须对野指针做判断和处理。
实际上,比较高级的编译器会自动识别野指针并报错。
8. 空指针初步介绍
空指针:通常指没有指向任何有效内存地址的指针。
C语言或其他高级语言中常见的“空指针异常”,就是指此处的空指针处理出错。
C语言中允许先创建空指针后赋地址,例如:
1 | char* ptr = NULL; |
9. 空指针的初始化
指针初始化时可以设置它为空指针,即赋值为NULL,后续可以将一个地址写入空指针。
在Visual Studio中,打印空指针的地址会得到全0的地址,尝试对空指针所指向的数据进行解引用处理时,会导致运行时错误。
因此,程序中需要对可能出现空指针的情况做判断处理,防止出现以外解引用空指针导致的错误。
1 |
|
10. 从代码上尝试认识野指针
野指针不像空指针可以直接定义,空指针只需要做好判断和确保赋地址即可,野指针是一个代码中可能出现的问题,需要去处理它。
下面是一个导致野指针出现的示例,仅供参考:
1 | uint32_t number_1 = 100; |
11. 数组的首地址与指针的算数运算
可以使用加减法对指针进行运算,从而拿到想要的值,最典型的用法就是在数组中。
由于数组中的所有元素在内存中都是连续的,因此数组中每个元素的地址也是连续的,因此定义一个数组的地址时,直接将数组赋给指针,不需要有取地址符(&),此时这个指针的值就是这个数组的首地址,即数组中第一个元素的地址。例如:
1 | // 数组在内存中是连续存储的 |
可以使用sizeof()函数计算数组的大小:
1 | // size_t 是用于表示对象大小的无符号整数类型 |
指针加减法不会按字节增加,而是按元素大小增加,指针变化1,实际上的地址是变化了sizeof(数组的数据类型)个字节。
指针的加法:
1 | printf("使用指针加法访问数组元素:\n"); |
指针的减法:
1 | printf("使用指针减法访问数组元素:\n"); |
使用指针减法计算数组距离:
1 | uint32_t* start_ptr = &numbers[0]; |
可以使用指针的运算来遍历打印数组中的每个元素:
1 | printf("数组元素:{ "); |
12. 指针的算数运算与比较运用
可以直接使用外部指针遍历数组:
1 | printf("指针遍历数组: { "); |
使用指针实现数组反向遍历:
1 | printf("反向遍历数组元素: { "); |
使用指针运算访问数组中的特定元素:
1 | uint32_t offset = 3; |
指针之间的比较:
1 | uint32_t* middle_ptr = start_ptr + size / 2; |
13. 再探size_t与数组与指针的使用
size_t是一个无符号整数类型,它专门用来表示大小、长度和索引,特别定义这样一种数据类型可以提高程序在不同平台之间跨平台的可移植性和安全性。
1 | uint32_t numbers[] = { 10, 20 , 30, 40, 50 ,60, 70, 80, 90, 100 }; |
可以使用指针的运算配合指针的解引用来修改数组中元素的值:
1 | // 打印修改前的数组元素 |
14. 案例:指针查找特定元素的索引并返回
要求:使用指针计算出一个特定数组中某一个元素的下标索引值并返回
代码实现:
1 |
|
我们可以将在数组中寻找某一个元素并返回它的下标索引值的功能整合成一个函数:
1 |
|
关于如何向函数中传入指针,会在之后几节中学到。
15. 指针访问多维数组
使用指针同样可以访问多维数组,其定义和访问方式需要注意:
1 | uint32_t matrix[3][4] = { |
定义二维数组的指针
定义二维数组的指针时,必须同时传入二维数组一行的元素个数,这样在使用指针时可以类似于访问二维数组:
- 指针的算数在第一层按数组行为单位移动,以上面的数组为例,每次移动时,地址偏移
4×sizeof(uint32_t) = 16字节 - 地址第二层按元素为单位移动
即:
1 | matrix → uint32_t(*)[4] // 数组名退化为指向首行的指针 |
用指针访问二维数组
用指针访问二维数组时,可以直接使用ptr[i][j]这种类似数组的形式解引用指针,得到地址指向的值,达到与*(*(ptr + i) + j)一样的效果,本质上编译器自动完成了解引用。
在C语言中,下标运算符[]本质上就是解引用操作的语法糖。
C语言中的数组下标运算符[]的定义是:a[b] ≡ *(a + b),这是语言规范规定的等价关系。
由此,更为细致地,可以分步展开ptr[i][j]:
第一步,展开第一层ptr[i]:
1 | ptr[i] ≡ *(ptr + i) |
ptr是uint32_t(*)[4]类型(指向uint32_t[4]数组的指针)ptr + i按数组大小偏移,指向第i行*(ptr + i)解引用得到类型uint32_t[4](一个包含4个元素的数组)- 数组在表达式中会退化为指向首元素的指针,所以结果是
uint32_t*
第二步,展开第二层[j]:
1 | ptr[i][j] ≡ (*(ptr + i))[j] ≡ *(*(ptr + i) + j) |
(*(ptr + i))[j]中,[j]再次应用规则a[b] ≡ *(a + b)- 最终得到
*(*(ptr + i) + j),返回类型是uint32_t(一个元素的值)
所以,[]运算符每次使用都隐式执行一次解引用操作,所以不需要手动写*解引用操作符。
因此,ptr[i][j]和*(*(ptr + i) + j)两种写法的底层机制完全一致,都是地址偏移+解引用,而类似数组的写法更易读,建议使用。
数组和指针的区别
需要注意的是,数组在大多数情况下会退化为指向首元素的指针,但数组和指针有本质区别。
数组是对象,指针是变量,使用sizeof()计算字节数时就可以看到明显差别:
1 | uint32_t array[10]; // 数组:分配 40 字节内存(10 × 4 字节) |
数组取地址
对于取地址运算符&,数组取地址之后得到的是指向整个数组的指针:
1 | uint32_t array[10]; |
array和&array的区别:
array和&array的值相同(都是数组首地址),但类型不同&array指向的是"整个数组",+1会跨过整个数组array退化后指向"首元素",+1只跨过一个元素
16. 指针数组
指针同样可以写成数组,一个指针数组内可以储存多个指针。
将上一节中定义多维数组的指针中uint32_t(*ptr)[4]的括号去掉,即可创建一个指针数组。
1 | uint32_t class_1[] = { 1001, 1002, 1003 }; |
指针数组中指针元素指向数组时,使用指针访问数组中的元素时可以使用类似二维数组的方法。
17. 函数的值传递与地址引用传递
在学到指针之前,声明定义一个函数时,向函数中传入的是一个值,这种方式被称为函数的值传递(Pass by value)。
例如一个实现将一个变量的值加10的函数:
1 |
|
可以注意到,由于变量作用域的问题,我们无法在函数中直接修改变量的值,只能将修改后的值返回,并在main函数中实现变量修改。
向函数中传入一个值,相当于将这个值拷贝到这个函数自己的作用域中的一个变量中,在函数中修改这个值,对原值没有影响。
而指针储存了变量在内存中的地址,如果函数拿到了这个地址,就可以实现跨作用域修改变量的值,这种方法被称为地址引用传递(Pass by Reference)。
此时,可以直接在函数中利用指针修改变量值,函数返回值为空即可:
1 |
|
使用地址引用传递可以跨函数跨作用域修改操作值,提供特定功能的外部服务。这也是指针通常在函数中使用频繁的原因。
18. 案例:员工薪资系统:指针作为函数返回值
指针可以作为函数的传入值,也可以作为函数的返回值。
要求:制作一个简易的员工薪资管理系统,假设一共有五名员工,需要实现的功能包括:
- 给所有员工批量涨工资;
- 可以批量打印所有员工的薪资;
- 查找最高薪资的员工,并给这名员工奖励5000元;
- 计算并给所有员工发年终奖(假设年终奖为涨薪最高的员工的薪资除以10)。
代码实现:
1 |
|
在C语言的函数定义中,如果想向函数参数中传入一个数组,最终都会被编译器自动转换退化为指针。例如:
1
2
3
4
5 // 以下四种写法在函数参数中完全等价
void func1(uint32_t arr[]);
void func1(uint32_t arr[5]); // [5] 被忽略
void func1(uint32_t arr[999]); // [999] 也被忽略
void func1(uint32_t *arr); // 最终都变成这个写成数组的形式易于阅读,写成指针的形式更强调这个变量是一个指针,用哪种都可以。
19. 游戏案例:指针的练习
要求:制作一个简单的游戏系统,包含以下功能:
- 增加经验:模拟玩家完成一些游戏任务后增加经验;
- 提升等级:每当玩家达到100经验值时可以升一级,最高十级;
- 获得宝藏提示:每当玩家成功升一级,可以获得一个每级固定的与宝藏相关的提示,一共十条提示。
代码实现:
1 |
|
关于使用
char*定义字符串的内容,会在后面字符串的章节学到。
20. 练习:更新分数
要求:有两名游戏玩家,需要对他们的游戏得分进行一些操作:
- 改变(增加或减少)某位玩家的分数;
- 比较两位玩家的分数,拿到较高分数的玩家的得分;
- 让某一位玩家的分数变为两倍。
代码实现:
1 |
|
21. 游戏案例:收藏奖杯
要求:一个游戏,可以达成一些成就,最多能获得十个成就,需要有以下功能:
- 增加成就数量,将达成的成就储存下来;
- 打印当前已获得的所有成就。
代码实现:
1 |
|
22. 第八章结束语
至此,指针的基础知识已介绍结束。
其他有关指针的深入使用,例如:指针操作字符串、指针在结构体中的应用、内存的动态分配、多级指针等内容,将在后续章节中介绍。
本文链接: https://hanqingjiang.com/2025/12/24/20251224_C_pointer/
版权声明: 本作品采用 CC BY-NC-SA 4.0 进行许可。转载请注明出处!
